@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,22 @@
|
|
|
1
|
+
import { compileParts, type CompileOptions, type CompileResult, type CompiledParts, type HelperManifest } from './compile.js';
|
|
2
|
+
export type { CompileOptions, CompileResult, CompiledParts, HelperManifest };
|
|
3
|
+
export type { Diagnostic } from './diagnostics.js';
|
|
4
|
+
export type { CheckUnit } from './check-unit.js';
|
|
5
|
+
export { formatDiagnostic, docsUrl, CODES } from './diagnostics.js';
|
|
6
|
+
export { DIAGNOSTIC_DOCS, type DiagnosticDoc } from './diagnostic-docs.js';
|
|
7
|
+
export { compileParts };
|
|
8
|
+
export declare const PACKAGE = "@tiaforge-ui/sinter";
|
|
9
|
+
/** Compiles annotated HTML into a rewritten page and its page module. Pure and deterministic. */
|
|
10
|
+
export declare function compile(html: string, options: CompileOptions): CompileResult;
|
|
11
|
+
export declare function pageModule(parts: CompiledParts, options: Pick<CompileOptions, 'helpers' | 'dev' | 'config' | 'actions'>): string;
|
|
12
|
+
/**
|
|
13
|
+
* Reads a helpers module's export names statically. `export * as m from './x.js'`
|
|
14
|
+
* becomes a namespace whose members are read from `x.js` the same way. `export * from './y.js'`
|
|
15
|
+
* re-exports (e.g. paraglide's message index) are followed, relative paths only.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readHelperManifest(source: string, options: {
|
|
18
|
+
module: string;
|
|
19
|
+
file: string;
|
|
20
|
+
readFile: (path: string) => string;
|
|
21
|
+
resolve?: (spec: string, from: string) => string | null;
|
|
22
|
+
}): HelperManifest;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as acorn from 'acorn';
|
|
2
|
+
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
|
3
|
+
import { compileParts } from './compile.js';
|
|
4
|
+
export { formatDiagnostic, docsUrl, CODES } from './diagnostics.js';
|
|
5
|
+
export { DIAGNOSTIC_DOCS } from './diagnostic-docs.js';
|
|
6
|
+
export { compileParts };
|
|
7
|
+
export const PACKAGE = '@tiaforge-ui/sinter';
|
|
8
|
+
/** Compiles annotated HTML into a rewritten page and its page module. Pure and deterministic. */
|
|
9
|
+
export function compile(html, options) {
|
|
10
|
+
const parts = compileParts(html, options);
|
|
11
|
+
const out = { html: parts.html, module: pageModule(parts, options), diagnostics: parts.diagnostics, files: parts.files };
|
|
12
|
+
if (parts.check)
|
|
13
|
+
out.check = parts.check;
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export function pageModule(parts, options) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
const core = ['mount', '$k', ...parts.helpers.builtin];
|
|
19
|
+
lines.push(`import { ${core.join(', ')} } from '${PACKAGE}';`);
|
|
20
|
+
for (const f of parts.features)
|
|
21
|
+
lines.push(`import ${f} from '${PACKAGE}/${f}';`);
|
|
22
|
+
if (parts.helpers.user.length && options.helpers.module) {
|
|
23
|
+
lines.push(`import { ${parts.helpers.user.join(', ')} } from ${JSON.stringify(options.helpers.module)};`);
|
|
24
|
+
}
|
|
25
|
+
for (const [ns, members] of Object.entries(parts.namespaces)) {
|
|
26
|
+
const mod = options.helpers.namespaces[ns].module;
|
|
27
|
+
lines.push(`import { ${members.map((m) => `${m} as ${ns}$${m}`).join(', ')} } from ${JSON.stringify(mod)};`);
|
|
28
|
+
}
|
|
29
|
+
// actions (`data-sn-call`): imported as `$a$name` / `$a$ns$member`, only those the page calls
|
|
30
|
+
const acts = options.actions;
|
|
31
|
+
if (acts?.module) {
|
|
32
|
+
const direct = parts.calls.filter((c) => !c.includes('.'));
|
|
33
|
+
if (direct.length)
|
|
34
|
+
lines.push(`import { ${direct.map((c) => `${c} as $a$${c}`).join(', ')} } from ${JSON.stringify(acts.module)};`);
|
|
35
|
+
const byNs = new Map();
|
|
36
|
+
for (const c of parts.calls.filter((x) => x.includes('.'))) {
|
|
37
|
+
const [ns, member] = c.split('.');
|
|
38
|
+
byNs.set(ns, [...(byNs.get(ns) ?? []), member]);
|
|
39
|
+
}
|
|
40
|
+
for (const [ns, members] of byNs) {
|
|
41
|
+
lines.push(`import { ${members.map((x) => `${x} as $a$${ns}$${x}`).join(', ')} } from ${JSON.stringify(acts.namespaces[ns].module)};`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (options.config)
|
|
45
|
+
lines.push(`import ${JSON.stringify(options.config)};`);
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(`const ir = ${JSON.stringify(parts.ir)};`);
|
|
48
|
+
lines.push('const fns = [');
|
|
49
|
+
for (const f of parts.fns)
|
|
50
|
+
lines.push(` ${f},`);
|
|
51
|
+
lines.push('];');
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push(`mount(document, { ir, fns, features: [${parts.features.join(', ')}] });`);
|
|
54
|
+
return lines.join('\n') + '\n';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reads a helpers module's export names statically. `export * as m from './x.js'`
|
|
58
|
+
* becomes a namespace whose members are read from `x.js` the same way. `export * from './y.js'`
|
|
59
|
+
* re-exports (e.g. paraglide's message index) are followed, relative paths only.
|
|
60
|
+
*/
|
|
61
|
+
export function readHelperManifest(source, options) {
|
|
62
|
+
const manifest = { module: options.module, names: [], namespaces: {} };
|
|
63
|
+
const resolve = options.resolve ?? ((spec, from) => (spec.startsWith('.') ? joinPath(from, spec) : null));
|
|
64
|
+
const read = (file) => {
|
|
65
|
+
try {
|
|
66
|
+
return exportsOf(options.readFile(file), file);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/** the module's own value exports plus those of its `export *` re-exports, transitively */
|
|
73
|
+
const valueNames = (file, exports, seen) => {
|
|
74
|
+
const names = new Set(exports.names);
|
|
75
|
+
for (const spec of exports.reexports) {
|
|
76
|
+
const path = spec.startsWith('.') ? resolve(spec, file) : null;
|
|
77
|
+
if (!path || seen.has(path))
|
|
78
|
+
continue;
|
|
79
|
+
seen.add(path);
|
|
80
|
+
const sub = read(path);
|
|
81
|
+
if (sub)
|
|
82
|
+
for (const n of valueNames(path, sub, seen))
|
|
83
|
+
names.add(n);
|
|
84
|
+
}
|
|
85
|
+
return [...names].sort();
|
|
86
|
+
};
|
|
87
|
+
const top = exportsOf(source, options.file);
|
|
88
|
+
manifest.names = valueNames(options.file, top, new Set([options.file]));
|
|
89
|
+
for (const [ns, spec] of top.namespaces) {
|
|
90
|
+
const path = resolve(spec, options.file);
|
|
91
|
+
const exports = path ? read(path) : null;
|
|
92
|
+
const members = path && exports ? valueNames(path, exports, new Set([path])) : [];
|
|
93
|
+
const modSpec = spec.startsWith('.') ? joinPath(options.module, spec) : spec;
|
|
94
|
+
manifest.namespaces[ns] = { module: modSpec, members };
|
|
95
|
+
}
|
|
96
|
+
return manifest;
|
|
97
|
+
}
|
|
98
|
+
/** acorn extended with TypeScript syntax, for `.ts` helper and namespace modules */
|
|
99
|
+
let tsParser = null;
|
|
100
|
+
function parseModule(source, file) {
|
|
101
|
+
const options = { ecmaVersion: 'latest', sourceType: 'module' };
|
|
102
|
+
if (!/\.[cm]?tsx?$/.test(file))
|
|
103
|
+
return acorn.parse(source, options);
|
|
104
|
+
tsParser ??= acorn.Parser.extend(tsPlugin());
|
|
105
|
+
return tsParser.parse(source, options);
|
|
106
|
+
}
|
|
107
|
+
/** Value exports only: type-only exports (`export type`, `export interface`, `export { type X }`) are skipped. */
|
|
108
|
+
function exportsOf(source, file) {
|
|
109
|
+
const ast = parseModule(source, file);
|
|
110
|
+
const names = [];
|
|
111
|
+
const namespaces = [];
|
|
112
|
+
const reexports = [];
|
|
113
|
+
const typeOnly = (n) => n.exportKind === 'type';
|
|
114
|
+
for (const st of ast.body) {
|
|
115
|
+
if (typeOnly(st))
|
|
116
|
+
continue;
|
|
117
|
+
if (st.type === 'ExportNamedDeclaration') {
|
|
118
|
+
const d = st.declaration;
|
|
119
|
+
if (d?.type === 'FunctionDeclaration' && d.id)
|
|
120
|
+
names.push(d.id.name);
|
|
121
|
+
else if (d?.type === 'ClassDeclaration' && d.id)
|
|
122
|
+
names.push(d.id.name);
|
|
123
|
+
else if (d?.type === 'TSEnumDeclaration')
|
|
124
|
+
names.push(d.id.name);
|
|
125
|
+
else if (d?.type === 'VariableDeclaration') {
|
|
126
|
+
for (const v of d.declarations)
|
|
127
|
+
if (v.id.type === 'Identifier')
|
|
128
|
+
names.push(v.id.name);
|
|
129
|
+
}
|
|
130
|
+
for (const s of st.specifiers) {
|
|
131
|
+
if (typeOnly(s))
|
|
132
|
+
continue;
|
|
133
|
+
const exported = s.exported.type === 'Identifier' ? s.exported.name : String(s.exported.value);
|
|
134
|
+
if (exported !== 'default')
|
|
135
|
+
names.push(exported);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (st.type === 'ExportAllDeclaration' && st.exported) {
|
|
139
|
+
const ns = st.exported.type === 'Identifier' ? st.exported.name : String(st.exported.value);
|
|
140
|
+
namespaces.push([ns, String(st.source.value)]);
|
|
141
|
+
}
|
|
142
|
+
else if (st.type === 'ExportAllDeclaration') {
|
|
143
|
+
reexports.push(String(st.source.value));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { names: [...new Set(names)].sort(), namespaces, reexports };
|
|
147
|
+
}
|
|
148
|
+
function joinPath(from, spec) {
|
|
149
|
+
const dir = from.slice(0, from.lastIndexOf('/') + 1);
|
|
150
|
+
const out = [];
|
|
151
|
+
for (const seg of (dir + spec).split('/')) {
|
|
152
|
+
if (seg === '.')
|
|
153
|
+
continue;
|
|
154
|
+
if (seg === '..')
|
|
155
|
+
out.pop();
|
|
156
|
+
else
|
|
157
|
+
out.push(seg);
|
|
158
|
+
}
|
|
159
|
+
return out.join('/');
|
|
160
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { UrlTemplate } from '../ir.js';
|
|
2
|
+
export interface OpenApiSpec {
|
|
3
|
+
/** absolute path, for messages */
|
|
4
|
+
file: string;
|
|
5
|
+
/** the parsed document (OpenAPI 3.0 or 3.1) */
|
|
6
|
+
doc: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface OpenApiMatch {
|
|
9
|
+
spec: number;
|
|
10
|
+
/** the spec's path key, e.g. `/articles/{slug}` */
|
|
11
|
+
path: string;
|
|
12
|
+
/** schema of the response body, or null when the operation has no JSON 2xx response */
|
|
13
|
+
body: unknown;
|
|
14
|
+
/** names of the query parameters the operation declares */
|
|
15
|
+
query: string[];
|
|
16
|
+
/** schema of the JSON request body, or null */
|
|
17
|
+
request: unknown;
|
|
18
|
+
}
|
|
19
|
+
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
20
|
+
export type Json = Record<string, unknown>;
|
|
21
|
+
/** Follows `$ref`s inside the document (`#/components/…`); external refs resolve to null. */
|
|
22
|
+
export declare function deref(doc: unknown, v: unknown, seen?: number): Json | null;
|
|
23
|
+
export declare function matchResource(specs: OpenApiSpec[], src: UrlTemplate): OpenApiMatch | {
|
|
24
|
+
closest: string | null;
|
|
25
|
+
};
|
|
26
|
+
/** The operation a request with this method and URL template calls. */
|
|
27
|
+
export declare function matchOperation(specs: OpenApiSpec[], src: UrlTemplate, method: Method): OpenApiMatch | {
|
|
28
|
+
closest: string | null;
|
|
29
|
+
};
|
|
30
|
+
/** Properties an object schema has, through `allOf`, `oneOf` and `anyOf`. */
|
|
31
|
+
export declare function properties(doc: unknown, schema: unknown, seen?: number): Map<string, unknown>;
|
|
32
|
+
/** Walks a dotted path (`data.items`) into a schema: the schema there, or the segment that is missing. */
|
|
33
|
+
export declare function walk(doc: unknown, schema: unknown, path: string): {
|
|
34
|
+
schema: unknown;
|
|
35
|
+
} | {
|
|
36
|
+
missing: string;
|
|
37
|
+
have: string[];
|
|
38
|
+
};
|
|
39
|
+
/** Query parameter names written literally in a URL template's query string. */
|
|
40
|
+
export declare function queryNames(src: UrlTemplate): string[];
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { suggest } from './diagnostics.js';
|
|
2
|
+
const INTERP = '\u0000';
|
|
3
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
4
|
+
/** Follows `$ref`s inside the document (`#/components/…`); external refs resolve to null. */
|
|
5
|
+
export function deref(doc, v, seen = 0) {
|
|
6
|
+
if (!isObj(v))
|
|
7
|
+
return null;
|
|
8
|
+
if (typeof v.$ref !== 'string')
|
|
9
|
+
return v;
|
|
10
|
+
if (seen > 32 || !v.$ref.startsWith('#/'))
|
|
11
|
+
return null;
|
|
12
|
+
let cur = doc;
|
|
13
|
+
for (const raw of v.$ref.slice(2).split('/')) {
|
|
14
|
+
const key = raw.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
15
|
+
cur = isObj(cur) ? cur[key] : undefined;
|
|
16
|
+
}
|
|
17
|
+
return deref(doc, cur, seen + 1);
|
|
18
|
+
}
|
|
19
|
+
/** `/api/x/{…}?a={…}` as text, interpolations replaced by a marker. */
|
|
20
|
+
function flatten(src) {
|
|
21
|
+
const text = src.map((p) => (typeof p === 'string' ? p : INTERP)).join('');
|
|
22
|
+
const q = text.indexOf('?');
|
|
23
|
+
return q < 0 ? { path: text, query: '' } : { path: text.slice(0, q), query: text.slice(q + 1) };
|
|
24
|
+
}
|
|
25
|
+
function serverPrefixes(doc) {
|
|
26
|
+
const servers = Array.isArray(doc.servers) && doc.servers.length ? doc.servers : [{ url: '/' }];
|
|
27
|
+
return servers.filter(isObj).map((s) => {
|
|
28
|
+
const vars = isObj(s.variables) ? s.variables : {};
|
|
29
|
+
const url = String(s.url ?? '/').replace(/\{([^}]+)\}/g, (_, n) => String(vars[n]?.default ?? ''));
|
|
30
|
+
return url.replace(/\/+$/, '');
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const segments = (p) => p.split('/').filter((s, i, a) => !(s === '' && (i === 0 || i === a.length - 1)));
|
|
34
|
+
/** Score of a spec path against the request path segments, or -1. Literal matches beat parameters. */
|
|
35
|
+
function score(specPath, segs) {
|
|
36
|
+
const want = segments(specPath);
|
|
37
|
+
if (want.length !== segs.length)
|
|
38
|
+
return -1;
|
|
39
|
+
let n = 0;
|
|
40
|
+
for (let i = 0; i < want.length; i++) {
|
|
41
|
+
const w = want[i], s = segs[i];
|
|
42
|
+
if (!w.includes('{')) {
|
|
43
|
+
if (w !== s)
|
|
44
|
+
return -1;
|
|
45
|
+
n += 2;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (w.replace(/\{[^}]+\}/g, INTERP) === s) {
|
|
49
|
+
n += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (s.includes(INTERP))
|
|
53
|
+
return -1;
|
|
54
|
+
const re = new RegExp('^' + w.split(/\{[^}]+\}/).map((x) => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[^/]+') + '$');
|
|
55
|
+
if (!re.test(s))
|
|
56
|
+
return -1;
|
|
57
|
+
n += 1;
|
|
58
|
+
}
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
61
|
+
/** The request path after the server prefix, for each spec whose servers it starts with. */
|
|
62
|
+
function relativePaths(doc, path) {
|
|
63
|
+
const out = [];
|
|
64
|
+
const origin = /^https?:\/\/[^/]+/i.exec(path)?.[0] ?? '';
|
|
65
|
+
for (const prefix of serverPrefixes(doc)) {
|
|
66
|
+
let rest = null;
|
|
67
|
+
if (/^https?:\/\//i.test(prefix))
|
|
68
|
+
rest = path.toLowerCase().startsWith(prefix.toLowerCase()) ? path.slice(prefix.length) : null;
|
|
69
|
+
else {
|
|
70
|
+
const p = path.slice(origin.length);
|
|
71
|
+
rest = p.startsWith(prefix) ? p.slice(prefix.length) : null;
|
|
72
|
+
}
|
|
73
|
+
if (rest !== null && (rest === '' || rest.startsWith('/')))
|
|
74
|
+
out.push(rest);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
export function matchResource(specs, src) {
|
|
79
|
+
return matchOperation(specs, src, 'get');
|
|
80
|
+
}
|
|
81
|
+
/** The operation a request with this method and URL template calls. */
|
|
82
|
+
export function matchOperation(specs, src, method) {
|
|
83
|
+
const { path } = flatten(src);
|
|
84
|
+
let best = null;
|
|
85
|
+
specs.forEach((spec, i) => {
|
|
86
|
+
if (!isObj(spec.doc) || !isObj(spec.doc.paths))
|
|
87
|
+
return;
|
|
88
|
+
for (const rest of relativePaths(spec.doc, path)) {
|
|
89
|
+
const segs = segments(rest);
|
|
90
|
+
for (const [key, item] of Object.entries(spec.doc.paths)) {
|
|
91
|
+
if (!isObj(item) || !isObj(item[method]))
|
|
92
|
+
continue;
|
|
93
|
+
const s = score(key, segs);
|
|
94
|
+
if (s >= 0 && (!best || s > best.score))
|
|
95
|
+
best = { spec: i, path: key, score: s };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (!best) {
|
|
100
|
+
const want = path.replace(/^https?:\/\/[^/]+/i, '').split(INTERP).join('{}');
|
|
101
|
+
const keys = specs.flatMap((s) => (isObj(s.doc) && isObj(s.doc.paths) ? Object.keys(s.doc.paths) : []));
|
|
102
|
+
const norm = new Map(keys.map((k) => [k.replace(/\{[^}]+\}/g, '{}'), k]));
|
|
103
|
+
// the request path still carries the server prefix; compare its tails too
|
|
104
|
+
const tails = want.split('/').map((_, i, a) => '/' + a.slice(i).join('/'));
|
|
105
|
+
let close;
|
|
106
|
+
for (const t of tails)
|
|
107
|
+
if ((close = suggest(t, norm.keys())))
|
|
108
|
+
break;
|
|
109
|
+
return { closest: close ? norm.get(close) : null };
|
|
110
|
+
}
|
|
111
|
+
const { spec, path: key } = best;
|
|
112
|
+
const doc = specs[spec].doc;
|
|
113
|
+
const item = deref(doc, doc.paths[key]);
|
|
114
|
+
const op = deref(doc, item[method]);
|
|
115
|
+
const params = [...(Array.isArray(item.parameters) ? item.parameters : []), ...(Array.isArray(op.parameters) ? op.parameters : [])]
|
|
116
|
+
.map((p) => deref(doc, p)).filter((p) => !!p && p.in === 'query').map((p) => String(p.name));
|
|
117
|
+
return { spec, path: key, body: responseSchema(doc, op), query: params, request: jsonSchema(doc, deref(doc, op.requestBody)) };
|
|
118
|
+
}
|
|
119
|
+
function responseSchema(doc, op) {
|
|
120
|
+
const responses = deref(doc, op.responses);
|
|
121
|
+
if (!responses)
|
|
122
|
+
return null;
|
|
123
|
+
const key = Object.keys(responses).filter((k) => /^2(\d\d|XX)$/i.test(k)).sort()[0];
|
|
124
|
+
return jsonSchema(doc, key ? deref(doc, responses[key]) : null);
|
|
125
|
+
}
|
|
126
|
+
/** The JSON schema of a response or request body object. */
|
|
127
|
+
function jsonSchema(doc, res) {
|
|
128
|
+
const content = res && isObj(res.content) ? res.content : null;
|
|
129
|
+
if (!content)
|
|
130
|
+
return null;
|
|
131
|
+
const media = Object.keys(content).find((m) => /^application\/json\b/i.test(m)) ?? Object.keys(content).find((m) => /\+json\b/i.test(m));
|
|
132
|
+
const m = media ? deref(doc, content[media]) : null;
|
|
133
|
+
return m?.schema ?? null;
|
|
134
|
+
}
|
|
135
|
+
/** Properties an object schema has, through `allOf`, `oneOf` and `anyOf`. */
|
|
136
|
+
export function properties(doc, schema, seen = 0) {
|
|
137
|
+
const out = new Map();
|
|
138
|
+
const s = deref(doc, schema);
|
|
139
|
+
if (!s || seen > 16)
|
|
140
|
+
return out;
|
|
141
|
+
if (isObj(s.properties))
|
|
142
|
+
for (const [k, v] of Object.entries(s.properties))
|
|
143
|
+
out.set(k, v);
|
|
144
|
+
for (const key of ['allOf', 'oneOf', 'anyOf']) {
|
|
145
|
+
if (Array.isArray(s[key]))
|
|
146
|
+
for (const sub of s[key])
|
|
147
|
+
for (const [k, v] of properties(doc, sub, seen + 1))
|
|
148
|
+
if (!out.has(k))
|
|
149
|
+
out.set(k, v);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
/** Walks a dotted path (`data.items`) into a schema: the schema there, or the segment that is missing. */
|
|
154
|
+
export function walk(doc, schema, path) {
|
|
155
|
+
let cur = schema;
|
|
156
|
+
for (const seg of path.split('.')) {
|
|
157
|
+
const props = properties(doc, cur);
|
|
158
|
+
if (!props.has(seg))
|
|
159
|
+
return { missing: seg, have: [...props.keys()] };
|
|
160
|
+
cur = props.get(seg);
|
|
161
|
+
}
|
|
162
|
+
return { schema: cur };
|
|
163
|
+
}
|
|
164
|
+
/** Query parameter names written literally in a URL template's query string. */
|
|
165
|
+
export function queryNames(src) {
|
|
166
|
+
const { query } = flatten(src);
|
|
167
|
+
if (!query)
|
|
168
|
+
return [];
|
|
169
|
+
return query.split('&').map((kv) => kv.split('=')[0]).filter((k) => k && !k.includes(INTERP)).map((k) => {
|
|
170
|
+
try {
|
|
171
|
+
return decodeURIComponent(k);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return k;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface AttrDoc {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
doc: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const ATTRIBUTES: AttrDoc[];
|
|
7
|
+
export declare const ON_DOC: AttrDoc;
|
|
8
|
+
export declare const ELEMENTS: Array<{
|
|
9
|
+
name: string;
|
|
10
|
+
attrs: string[];
|
|
11
|
+
doc: string;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The whole attribute and element vocabulary, with one-line descriptions.
|
|
2
|
+
// The compiler, llms.txt and html-custom-data.json are all built from it.
|
|
3
|
+
export const ATTRIBUTES = [
|
|
4
|
+
{ name: 'state', value: 'object literal', doc: 'declares local reactive state for the element and its subtree' },
|
|
5
|
+
{ name: 'text', value: 'value expression', doc: 'sets textContent; never HTML' },
|
|
6
|
+
{ name: 'attr', value: 'object literal {href: …}', doc: 'literal keys; null/false removes, true sets empty; URL values are checked' },
|
|
7
|
+
{ name: 'class', value: 'object literal {unread: …}', doc: 'toggles each class' },
|
|
8
|
+
{ name: 'style', value: "object literal {'--color': …}", doc: 'sets each property with style.setProperty' },
|
|
9
|
+
{ name: 'show', value: 'value expression', doc: 'toggles the hidden attribute' },
|
|
10
|
+
{ name: 'prop', value: 'object literal', doc: 'sets JS properties; custom elements only' },
|
|
11
|
+
{ name: 'model', value: 'state path', doc: 'two-way binding for form controls' },
|
|
12
|
+
{ name: 'for', value: 'item in list | (item, i) in list', doc: 'repeats a <template> per item' },
|
|
13
|
+
{ name: 'key', value: 'value expression', doc: 'row identity; defaults to the resource key or object identity' },
|
|
14
|
+
{ name: 'if', value: 'value expression', doc: 'renders a <template> while truthy' },
|
|
15
|
+
{ name: 'else', value: '', doc: 'on a <template> directly after an if template' },
|
|
16
|
+
{ name: 'persist', value: 'storage key', doc: 'persists the model or state in localStorage under sn:' },
|
|
17
|
+
{ name: 'get', value: 'URL template', doc: 'GET action; latest wins' },
|
|
18
|
+
{ name: 'post', value: 'URL template', doc: 'POST action' },
|
|
19
|
+
{ name: 'put', value: 'URL template', doc: 'PUT action' },
|
|
20
|
+
{ name: 'patch', value: 'URL template', doc: 'PATCH action' },
|
|
21
|
+
{ name: 'delete', value: 'URL template', doc: 'DELETE action' },
|
|
22
|
+
{ name: 'call', value: 'action call', doc: 'local action: calls a function from the actions module, e.g. pgp.decrypt(msg)' },
|
|
23
|
+
{ name: 'body', value: 'value expression', doc: 'JSON request body; overrides form serialization' },
|
|
24
|
+
{ name: 'trigger', value: 'DOM event name', doc: 'overrides the default trigger' },
|
|
25
|
+
{ name: 'debounce', value: 'milliseconds', doc: 'delays the trigger, resets on each event; without a method, delays the element\'s data-sn-on-* handlers' },
|
|
26
|
+
{ name: 'concurrency', value: 'block | queue | latest', doc: 'mutations only; default block for forms, queue otherwise' },
|
|
27
|
+
{ name: 'optimistic', value: 'handler expression', doc: 'patch/upsert/remove calls applied before the request, rolled back on failure' },
|
|
28
|
+
{ name: 'then', value: 'handler expression', doc: 'runs after a 2xx response; $result = parsed body' },
|
|
29
|
+
{ name: 'catch', value: 'handler expression', doc: 'runs after an error; $error = {status, body, message}' },
|
|
30
|
+
{ name: 'path', value: 'URL patterns /a/:b …', doc: 'on <html>: the path shapes this page is served for; `:name` segments appear read-only in $url' },
|
|
31
|
+
{ name: 'data', value: 'resource name', doc: 'on <script type="application/json"> in <head>: initial data for a resource' },
|
|
32
|
+
];
|
|
33
|
+
export const ON_DOC = { name: 'on-<event>', value: 'handler expression', doc: 'plain event handler, no request; e.g. data-sn-on-click' };
|
|
34
|
+
export const ELEMENTS = [
|
|
35
|
+
{ name: 'sn-resource', attrs: ['name', 'src', 'call', 'each', 'key', 'items', 'total', 'order', 'when', 'paginate', 'page-size', 'size-param', 'cursor', 'direction-param', 'direction-values', 'page-param', 'offset-param', 'type', 'cache'], doc: 'a named, page-wide cache of server data; `cache` (or `cache="10m"`) shows the data from the last page load at once, then revalidates' },
|
|
36
|
+
{ name: 'sn-sync', attrs: ['src'], doc: 'one server event stream' },
|
|
37
|
+
{ name: 'sn-event', attrs: ['name', 'resource', 'op', 'item', 'set', 'when', 'fallback', 'type'], doc: 'maps one server event to a change message; child of <sn-sync>' },
|
|
38
|
+
{ name: 'sn-include', attrs: ['src'], doc: 'inlines a partial at build time' },
|
|
39
|
+
];
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Feature } from './runtime/app.js';
|
|
2
|
+
/**
|
|
3
|
+
* The HTTP version the origin of `url` was reached over, from Resource Timing: the page's
|
|
4
|
+
* navigation for a same-origin URL, else the latest request to that origin. An open stream has
|
|
5
|
+
* no entry of its own until it ends. Null when unknown (no request yet, or a cross-origin
|
|
6
|
+
* server without `Timing-Allow-Origin`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function protocolOf(url: string, base: string, perf?: Pick<Performance, 'getEntriesByType'> | undefined): string | null;
|
|
9
|
+
declare const dev: Feature;
|
|
10
|
+
export default dev;
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Messages for the core's warning codes; `%` takes the next argument. */
|
|
2
|
+
const MESSAGES = {
|
|
3
|
+
1: 'duplicate `data-sn="%"`; not bound',
|
|
4
|
+
2: 'missing `data-sn="%"`; not bound',
|
|
5
|
+
3: '`data-sn="%"` is on <%>, expected <%>; not bound',
|
|
6
|
+
4: 'embedded data for `%` must be a <script> in <head>; ignored',
|
|
7
|
+
5: 'embedded data for `%` is not valid JSON; ignored',
|
|
8
|
+
6: 'binding `%` on <%> threw: %',
|
|
9
|
+
7: 'refused URL in `%` on <%>: %',
|
|
10
|
+
8: 'refused style value for `%`: %',
|
|
11
|
+
9: 'model on <%> could not be set: %',
|
|
12
|
+
10: 'a `data-sn-for` over primitive values needs `data-sn-key`',
|
|
13
|
+
11: 'duplicate key in `data-sn-for`: %',
|
|
14
|
+
12: 'resource `%`: `items` path `%` is not an array',
|
|
15
|
+
13: 'resource `%`: duplicate key `%`; the last one wins',
|
|
16
|
+
14: 'resource `%`: item is missing key field `%`',
|
|
17
|
+
15: 'state `%` changed outside sinter: use `update()`',
|
|
18
|
+
16: 'unhandled error%: %',
|
|
19
|
+
17: 'resource `%`: src could not be resolved: %',
|
|
20
|
+
18: 'stream `%` is served over HTTP/1.1, where browsers allow 6 connections per origin: with more than 5 tabs of this app open, pages stop loading. Serve it over HTTP/2 (any TLS reverse proxy such as nginx or Caddy), or use the WebSocket transport',
|
|
21
|
+
19: 'resource `%`: % KB of data is over the page-load cache limit of 256 KB per entry; not cached',
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The HTTP version the origin of `url` was reached over, from Resource Timing: the page's
|
|
25
|
+
* navigation for a same-origin URL, else the latest request to that origin. An open stream has
|
|
26
|
+
* no entry of its own until it ends. Null when unknown (no request yet, or a cross-origin
|
|
27
|
+
* server without `Timing-Allow-Origin`).
|
|
28
|
+
*/
|
|
29
|
+
export function protocolOf(url, base, perf = globalThis.performance) {
|
|
30
|
+
let origin;
|
|
31
|
+
try {
|
|
32
|
+
origin = new URL(url, base).origin;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const entries = [...(perf?.getEntriesByType?.('navigation') ?? []), ...(perf?.getEntriesByType?.('resource') ?? []).slice().reverse()];
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
let o;
|
|
40
|
+
try {
|
|
41
|
+
o = new URL(e.name).origin;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (o === origin && e.nextHopProtocol)
|
|
47
|
+
return e.nextHopProtocol;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function deepFreeze(v) {
|
|
52
|
+
if (v && typeof v == 'object' && !Object.isFrozen(v)) {
|
|
53
|
+
Object.freeze(v);
|
|
54
|
+
for (const k of Object.keys(v))
|
|
55
|
+
deepFreeze(v[k]);
|
|
56
|
+
}
|
|
57
|
+
return v;
|
|
58
|
+
}
|
|
59
|
+
const dev = {
|
|
60
|
+
name: 'dev',
|
|
61
|
+
install(app) {
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
app.dev = true;
|
|
64
|
+
app.warn = (code, ...args) => {
|
|
65
|
+
let i = 0;
|
|
66
|
+
const text = typeof code == 'string' ? code : MESSAGES[code].replace(/%/g, () => String(args[i++]).replace(/\x1f/g, ','));
|
|
67
|
+
const msg = `sinter: ${text}`;
|
|
68
|
+
if (seen.has(msg))
|
|
69
|
+
return;
|
|
70
|
+
seen.add(msg);
|
|
71
|
+
(code == 16 ? console.error : console.warn)(msg, ...args.slice(i));
|
|
72
|
+
};
|
|
73
|
+
app.prep = deepFreeze;
|
|
74
|
+
app.streamOpened = (url) => {
|
|
75
|
+
if (/^http\/1\./i.test(protocolOf(url, app.env.href()) ?? ''))
|
|
76
|
+
app.warn(18, url);
|
|
77
|
+
};
|
|
78
|
+
const json = (o) => { try {
|
|
79
|
+
return JSON.stringify(o);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return '';
|
|
83
|
+
} };
|
|
84
|
+
let last = [];
|
|
85
|
+
app.beforeFlush.push((dirty) => app.states.forEach(([sid, obj], i) => {
|
|
86
|
+
if (json(obj) != last[i] && !dirty.some((p) => p[0] == '$s' + sid))
|
|
87
|
+
app.warn(15, Object.keys(obj).join('`, `'));
|
|
88
|
+
}));
|
|
89
|
+
app.afterFlush.push(() => { last = app.states.map(([, obj]) => json(obj)); });
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
export default dev;
|
package/dist/each.d.ts
ADDED