@tiaforge-ui/sinter 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -2
  3. package/dist/cache.d.ts +5 -0
  4. package/dist/cache.js +95 -0
  5. package/dist/check/index.d.ts +82 -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 +47 -0
  10. package/dist/check/shadow.js +245 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +67 -0
  13. package/dist/compiler/check-unit.d.ts +97 -0
  14. package/dist/compiler/check-unit.js +3 -0
  15. package/dist/compiler/compile.d.ts +62 -0
  16. package/dist/compiler/compile.js +1133 -0
  17. package/dist/compiler/diagnostic-docs.d.ts +10 -0
  18. package/dist/compiler/diagnostic-docs.js +137 -0
  19. package/dist/compiler/diagnostics.d.ts +56 -0
  20. package/dist/compiler/diagnostics.js +76 -0
  21. package/dist/compiler/expr.d.ts +89 -0
  22. package/dist/compiler/expr.js +838 -0
  23. package/dist/compiler/index.d.ts +21 -0
  24. package/dist/compiler/index.js +106 -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 +38 -0
  29. package/dist/dev.d.ts +10 -0
  30. package/dist/dev.js +92 -0
  31. package/dist/index.d.ts +7 -0
  32. package/dist/index.js +4 -0
  33. package/dist/ir.d.ts +127 -0
  34. package/dist/ir.js +121 -0
  35. package/dist/lsp/quickfix.d.ts +15 -0
  36. package/dist/lsp/quickfix.js +58 -0
  37. package/dist/lsp/rpc.d.ts +31 -0
  38. package/dist/lsp/rpc.js +120 -0
  39. package/dist/lsp/server.d.ts +36 -0
  40. package/dist/lsp/server.js +281 -0
  41. package/dist/lsp/spec-location.d.ts +9 -0
  42. package/dist/lsp/spec-location.js +42 -0
  43. package/dist/lsp/typescript.d.ts +77 -0
  44. package/dist/lsp/typescript.js +242 -0
  45. package/dist/lsp/workspace.d.ts +56 -0
  46. package/dist/lsp/workspace.js +227 -0
  47. package/dist/optimistic.d.ts +3 -0
  48. package/dist/optimistic.js +15 -0
  49. package/dist/paginate.d.ts +3 -0
  50. package/dist/paginate.js +184 -0
  51. package/dist/persist.d.ts +3 -0
  52. package/dist/persist.js +55 -0
  53. package/dist/project.d.ts +36 -0
  54. package/dist/project.js +100 -0
  55. package/dist/runtime/app.d.ts +125 -0
  56. package/dist/runtime/app.js +1183 -0
  57. package/dist/runtime/config.d.ts +53 -0
  58. package/dist/runtime/config.js +118 -0
  59. package/dist/runtime/env.d.ts +18 -0
  60. package/dist/runtime/env.js +23 -0
  61. package/dist/runtime/helpers.d.ts +9 -0
  62. package/dist/runtime/helpers.js +55 -0
  63. package/dist/runtime/request.d.ts +23 -0
  64. package/dist/runtime/request.js +93 -0
  65. package/dist/runtime/security.d.ts +12 -0
  66. package/dist/runtime/security.js +34 -0
  67. package/dist/runtime/store.d.ts +95 -0
  68. package/dist/runtime/store.js +142 -0
  69. package/dist/sse.d.ts +7 -0
  70. package/dist/sse.js +206 -0
  71. package/dist/testing/index.d.ts +71 -0
  72. package/dist/testing/index.js +345 -0
  73. package/dist/vite.d.ts +11 -0
  74. package/dist/vite.js +94 -0
  75. package/dist/ws.d.ts +18 -0
  76. package/dist/ws.js +57 -0
  77. package/docs/diagnostics.md +273 -0
  78. package/html-custom-data.json +234 -0
  79. package/llms.txt +531 -0
  80. package/package.json +90 -4
@@ -0,0 +1,53 @@
1
+ import { type Env } from './env.js';
2
+ import type { Transport } from './app.js';
3
+ export type Middleware = (request: Request, next: (request: Request) => Promise<Response>) => Promise<Response>;
4
+ export interface Config {
5
+ request: Middleware[];
6
+ sessionProbe: string | null;
7
+ onError: ((error: SinterError, element: Element | null) => void) | null;
8
+ locale: {
9
+ get(): string;
10
+ set(locale: string): void;
11
+ } | null;
12
+ urlSchemes: string[];
13
+ origins: string[];
14
+ /** the transport for a `<sn-sync>` stream URL; `undefined` (or no function) means EventSource */
15
+ transport: ((url: string) => Transport | undefined) | null;
16
+ env: Env;
17
+ }
18
+ export declare const config: Config;
19
+ export declare function configure(options: Partial<Omit<Config, 'env'>> & {
20
+ env?: Partial<Env>;
21
+ }): void;
22
+ /** `$error`: a non-2xx response, a network error (status 0) or a refused request. */
23
+ export declare class SinterError extends Error {
24
+ readonly status: number;
25
+ readonly body: unknown;
26
+ constructor(status: number, body: unknown, message: string);
27
+ }
28
+ /** page-load cache entries (`cache` on `<sn-resource>`), in sessionStorage */
29
+ export declare const CACHE_PREFIX = "sn:c:";
30
+ /** Removes the page-load cache; sinter calls it on every 401. */
31
+ export declare function clearCached(env?: Env): void;
32
+ /** Removes every persisted value (`sn:` keys) and the page-load cache, e.g. on logout. */
33
+ export declare function clearPersisted(): void;
34
+ export declare function credentials(mode: RequestCredentials): Middleware;
35
+ /** Adds a header to same-origin requests, or to requests to `options.origin` only. */
36
+ export declare function header(name: string, value: string, options?: {
37
+ origin?: string;
38
+ }): Middleware;
39
+ export declare function onUnauthorized(handler: (request: Request, response: Response) => Promise<Response>): Middleware;
40
+ export interface LoginRedirectOptions {
41
+ to: string;
42
+ returnUrl?: {
43
+ allow: string[];
44
+ param?: string;
45
+ };
46
+ }
47
+ /**
48
+ * Stores the return URL (same-origin, allowlisted prefixes only), clears
49
+ * persisted state, navigates to login and never resolves.
50
+ */
51
+ export declare function loginRedirect(options: LoginRedirectOptions): () => Promise<Response>;
52
+ /** The return URL stored by loginRedirect, if it is same-origin and allowlisted; removes it. */
53
+ export declare function takeReturnUrl(allow: string[]): string | null;
@@ -0,0 +1,118 @@
1
+ // App-wide configuration and the request middleware helpers. Configuration
2
+ // comes only from configure() in JavaScript, never from the DOM.
3
+ import { defaultEnv } from './env.js';
4
+ export const config = {
5
+ request: [], sessionProbe: null, onError: null, locale: null, urlSchemes: [], origins: [], transport: null, env: defaultEnv,
6
+ };
7
+ export function configure(options) {
8
+ const { env, ...rest } = options;
9
+ Object.assign(config, rest);
10
+ if (env)
11
+ config.env = { ...config.env, ...env };
12
+ }
13
+ /** `$error`: a non-2xx response, a network error (status 0) or a refused request. */
14
+ export class SinterError extends Error {
15
+ status;
16
+ body;
17
+ constructor(status, body, message) {
18
+ super(message);
19
+ this.status = status;
20
+ this.body = body;
21
+ this.name = 'SinterError';
22
+ }
23
+ }
24
+ const PERSIST_PREFIX = 'sn:';
25
+ const RETURN_KEY = 'sn:return';
26
+ /** page-load cache entries (`cache` on `<sn-resource>`), in sessionStorage */
27
+ export const CACHE_PREFIX = 'sn:c:';
28
+ function removeKeys(s, prefix) {
29
+ if (!s)
30
+ return;
31
+ try {
32
+ const keys = [];
33
+ for (let i = 0; i < s.length; i++) {
34
+ const k = s.key(i);
35
+ if (k && k.startsWith(prefix))
36
+ keys.push(k);
37
+ }
38
+ for (const k of keys)
39
+ s.removeItem(k);
40
+ }
41
+ catch { /* storage failures are ignored */ }
42
+ }
43
+ /** Removes the page-load cache; sinter calls it on every 401. */
44
+ export function clearCached(env = config.env) {
45
+ removeKeys(env.session(), CACHE_PREFIX);
46
+ }
47
+ /** Removes every persisted value (`sn:` keys) and the page-load cache, e.g. on logout. */
48
+ export function clearPersisted() {
49
+ removeKeys(config.env.storage(), PERSIST_PREFIX);
50
+ clearCached();
51
+ }
52
+ // ---- middleware
53
+ export function credentials(mode) {
54
+ return (req, next) => next(new Request(req, { credentials: mode }));
55
+ }
56
+ /** Adds a header to same-origin requests, or to requests to `options.origin` only. */
57
+ export function header(name, value, options = {}) {
58
+ return (req, next) => {
59
+ const target = new URL(req.url).origin;
60
+ const allowed = options.origin ? target === new URL(options.origin).origin : target === new URL(config.env.href()).origin;
61
+ if (!allowed)
62
+ return next(req);
63
+ const headers = new Headers(req.headers);
64
+ headers.set(name, value);
65
+ return next(new Request(req, { headers }));
66
+ };
67
+ }
68
+ export function onUnauthorized(handler) {
69
+ return async (req, next) => {
70
+ const res = await next(req);
71
+ return res.status === 401 ? handler(req, res) : res;
72
+ };
73
+ }
74
+ /**
75
+ * Stores the return URL (same-origin, allowlisted prefixes only), clears
76
+ * persisted state, navigates to login and never resolves.
77
+ */
78
+ export function loginRedirect(options) {
79
+ return () => {
80
+ const env = config.env;
81
+ const here = new URL(env.href());
82
+ const path = here.pathname + here.search + here.hash;
83
+ let target = options.to;
84
+ if (options.returnUrl && options.returnUrl.allow.some((p) => here.pathname.startsWith(p))) {
85
+ try {
86
+ env.session()?.setItem(RETURN_KEY, path);
87
+ }
88
+ catch { /* ignored */ }
89
+ if (options.returnUrl.param) {
90
+ const u = new URL(options.to, here);
91
+ u.searchParams.set(options.returnUrl.param, path);
92
+ target = u.origin === here.origin ? u.pathname + u.search + u.hash : options.to;
93
+ }
94
+ }
95
+ clearPersisted();
96
+ env.navigate(target);
97
+ return new Promise(() => { });
98
+ };
99
+ }
100
+ /** The return URL stored by loginRedirect, if it is same-origin and allowlisted; removes it. */
101
+ export function takeReturnUrl(allow) {
102
+ const env = config.env;
103
+ let v = null;
104
+ try {
105
+ const s = env.session();
106
+ v = s?.getItem(RETURN_KEY) ?? null;
107
+ s?.removeItem(RETURN_KEY);
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ if (!v || !v.startsWith('/') || v.startsWith('//'))
113
+ return null;
114
+ const u = new URL(v, env.href());
115
+ if (u.origin !== new URL(env.href()).origin)
116
+ return null;
117
+ return allow.some((p) => u.pathname.startsWith(p)) ? u.pathname + u.search + u.hash : null;
118
+ }
@@ -0,0 +1,18 @@
1
+ export interface Env {
2
+ now(): number;
3
+ setTimeout(fn: () => void, ms: number, kind?: 'debounce' | 'timeout' | 'backoff' | 'clock'): unknown;
4
+ clearTimeout(handle: unknown): void;
5
+ queueMicrotask(fn: () => void): void;
6
+ /** the only way sinter navigates */
7
+ navigate(url: string): void;
8
+ fetch(request: Request): Promise<Response>;
9
+ storage(): Storage | null;
10
+ session(): Storage | null;
11
+ /** current location (href) */
12
+ href(): string;
13
+ replaceState(url: string): void;
14
+ random(): number;
15
+ /** add a window-level listener (pagehide, pageshow, popstate); returns the remover */
16
+ listen(type: string, fn: (e: Event) => void): () => void;
17
+ }
18
+ export declare const defaultEnv: Env;
@@ -0,0 +1,23 @@
1
+ // Everything sinter needs from the outside world, in one injectable object, so
2
+ // the testing module can drive time, scheduling, navigation and fetch exactly.
3
+ const g = globalThis;
4
+ const tryGet = (f) => { try {
5
+ return f();
6
+ }
7
+ catch {
8
+ return null;
9
+ } };
10
+ export const defaultEnv = {
11
+ now: Date.now,
12
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
13
+ clearTimeout: (h) => clearTimeout(h),
14
+ queueMicrotask: (fn) => queueMicrotask(fn),
15
+ navigate: (url) => g.location.assign(url),
16
+ fetch: (req) => fetch(req),
17
+ storage: () => tryGet(() => g.localStorage),
18
+ session: () => tryGet(() => g.sessionStorage),
19
+ href: () => g.location.href,
20
+ replaceState: (url) => g.history.replaceState(g.history.state, '', url),
21
+ random: Math.random,
22
+ listen: (type, fn) => (g.addEventListener(type, fn), () => g.removeEventListener(type, fn)),
23
+ };
@@ -0,0 +1,9 @@
1
+ type Style = 'full' | 'long' | 'medium' | 'short';
2
+ export declare function date(v: unknown, style?: Style): string;
3
+ export declare function time(v: unknown, style?: Style): string;
4
+ export declare function datetime(v: unknown, style?: Style): string;
5
+ /** "3 days ago", relative to `now` (default: the current time). */
6
+ export declare function relative(v: unknown, now?: unknown): string;
7
+ export declare function number(v: unknown, opts?: Intl.NumberFormatOptions): string;
8
+ export declare function bytes(v: unknown): string;
9
+ export {};
@@ -0,0 +1,55 @@
1
+ // Built-in helpers. Each reads the current locale, so bindings that call them
2
+ // re-run when `$locale` changes.
3
+ import { config } from './config.js';
4
+ const locale = () => config.locale?.get();
5
+ const toDate = (v) => {
6
+ if (v == null || v === '')
7
+ return null;
8
+ const d = v instanceof Date ? v : new Date(v);
9
+ return Number.isNaN(d.getTime()) ? null : d;
10
+ };
11
+ export function date(v, style = 'medium') {
12
+ const d = toDate(v);
13
+ return d ? new Intl.DateTimeFormat(locale(), { dateStyle: style }).format(d) : '';
14
+ }
15
+ export function time(v, style = 'short') {
16
+ const d = toDate(v);
17
+ return d ? new Intl.DateTimeFormat(locale(), { timeStyle: style }).format(d) : '';
18
+ }
19
+ export function datetime(v, style = 'medium') {
20
+ const d = toDate(v);
21
+ return d ? new Intl.DateTimeFormat(locale(), { dateStyle: style, timeStyle: 'short' }).format(d) : '';
22
+ }
23
+ const UNITS = [
24
+ ['year', 31536000], ['month', 2592000], ['week', 604800], ['day', 86400], ['hour', 3600], ['minute', 60], ['second', 1],
25
+ ];
26
+ /** "3 days ago", relative to `now` (default: the current time). */
27
+ export function relative(v, now = Date.now()) {
28
+ const d = toDate(v), n = toDate(now);
29
+ if (!d || !n)
30
+ return '';
31
+ const diff = (d.getTime() - n.getTime()) / 1000;
32
+ const fmt = new Intl.RelativeTimeFormat(locale(), { numeric: 'auto' });
33
+ for (const [unit, secs] of UNITS) {
34
+ if (Math.abs(diff) >= secs || unit === 'second')
35
+ return fmt.format(Math.round(diff / secs), unit);
36
+ }
37
+ return '';
38
+ }
39
+ export function number(v, opts) {
40
+ if (v == null || v === '' || Number.isNaN(Number(v)))
41
+ return '';
42
+ return new Intl.NumberFormat(locale(), opts).format(Number(v));
43
+ }
44
+ const BYTE_UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'];
45
+ export function bytes(v) {
46
+ let n = Number(v);
47
+ if (v == null || Number.isNaN(n))
48
+ return '';
49
+ let i = 0;
50
+ while (Math.abs(n) >= 1024 && i < BYTE_UNITS.length - 1) {
51
+ n /= 1024;
52
+ i++;
53
+ }
54
+ return new Intl.NumberFormat(locale(), { style: 'unit', unit: BYTE_UNITS[i], unitDisplay: 'short', maximumFractionDigits: i ? 1 : 0 }).format(n);
55
+ }
@@ -0,0 +1,23 @@
1
+ import { SinterError, type Middleware } from './config.js';
2
+ import type { Env } from './env.js';
3
+ export declare const TIMEOUT = 30000;
4
+ export interface SendInit {
5
+ method?: string;
6
+ /** JSON-serialized unless FormData */
7
+ body?: unknown;
8
+ headers?: Record<string, string>;
9
+ signal?: AbortSignal | undefined;
10
+ }
11
+ export interface RequestLayer {
12
+ /** Sends a request and returns the parsed body; rejects with SinterError. */
13
+ send(url: string, init?: SendInit): Promise<unknown>;
14
+ /** Sends a request and returns the raw response; network errors reject with SinterError(0). */
15
+ raw(url: string, init?: SendInit): Promise<Response>;
16
+ /** Requests in flight. */
17
+ readonly inflight: number;
18
+ }
19
+ export declare const isAbort: (e: unknown) => boolean;
20
+ export declare const toError: (e: unknown) => SinterError;
21
+ export declare function createRequestLayer(env: Env, chain: Middleware[]): RequestLayer;
22
+ /** 204 or empty → null; JSON content type → parsed; anything else → text. */
23
+ export declare function parseBody(res: Response): Promise<unknown>;
@@ -0,0 +1,93 @@
1
+ // The request layer: every request (resources, actions, the session probe,
2
+ // islands) goes through one middleware chain, then fetch with a timeout and
3
+ // one retry for idempotent requests on network errors.
4
+ import { clearCached, config, SinterError } from './config.js';
5
+ export const TIMEOUT = 30_000;
6
+ export const isAbort = (e) => e?.name == 'AbortError';
7
+ export const toError = (e) => (e instanceof SinterError ? e : new SinterError(0, null, e?.message || 'network error'));
8
+ export function createRequestLayer(env, chain) {
9
+ let inflight = 0;
10
+ const terminal = async (req) => {
11
+ const signal = req.signal, idempotent = /^(GET|HEAD|OPTIONS|PUT|DELETE)$/.test(req.method);
12
+ for (let retried = 0;; retried++) {
13
+ const c = new AbortController(), abort = () => c.abort(signal.reason);
14
+ signal.aborted ? abort() : signal.addEventListener('abort', abort);
15
+ const timer = env.setTimeout(() => c.abort(), TIMEOUT, 'timeout');
16
+ try {
17
+ const res = await env.fetch(new Request(idempotent ? req.clone() : req, { signal: c.signal }));
18
+ // the session is gone: cached data must not outlive it, whatever the app does next
19
+ if (res.status == 401)
20
+ clearCached(env);
21
+ return res;
22
+ }
23
+ catch (e) {
24
+ if (signal.aborted)
25
+ throw signal.reason ?? e;
26
+ if (c.signal.aborted)
27
+ throw new SinterError(0, null, 'request timed out');
28
+ if (!idempotent || retried || isAbort(e))
29
+ throw toError(e);
30
+ }
31
+ finally {
32
+ env.clearTimeout(timer);
33
+ signal.removeEventListener('abort', abort);
34
+ }
35
+ }
36
+ };
37
+ const run = chain.reduceRight((next, mw) => (r) => mw(r, next), terminal);
38
+ async function raw(url, init = {}) {
39
+ const target = new URL(url, env.href());
40
+ if (target.origin != new URL(env.href()).origin && !config.origins.some((o) => new URL(o).origin == target.origin)) {
41
+ throw new SinterError(0, null, `request to ${target.origin} refused: origin not allowed`);
42
+ }
43
+ const method = (init.method ?? 'GET').toUpperCase(), headers = new Headers(init.headers);
44
+ let body = init.body;
45
+ if (body == null || method == 'GET')
46
+ body = undefined;
47
+ else if (!(body instanceof FormData)) {
48
+ body = JSON.stringify(body);
49
+ headers.set('Content-Type', 'application/json');
50
+ }
51
+ headers.has('Accept') || headers.set('Accept', 'application/json');
52
+ // mutations outlive the page: a navigation right after a click must not cancel the write
53
+ // (keepalive bodies are limited to 64 KB; larger ones and uploads go without it)
54
+ const keepalive = method != 'GET' && method != 'HEAD' && (body == null || (typeof body == 'string' && body.length < 60_000));
55
+ inflight++;
56
+ try {
57
+ const res = await run(new Request(target, { method, headers, body: body ?? null, credentials: 'same-origin', signal: init.signal ?? null, keepalive }));
58
+ // also a 401 that middleware answered itself (fetch above clears before onUnauthorized runs)
59
+ if (res.status == 401)
60
+ clearCached(env);
61
+ return res;
62
+ }
63
+ catch (e) {
64
+ throw isAbort(e) ? e : toError(e);
65
+ }
66
+ finally {
67
+ inflight--;
68
+ }
69
+ }
70
+ async function send(url, init) {
71
+ const res = await raw(url, init), body = await parseBody(res);
72
+ if (!res.ok) {
73
+ const b = body, m = typeof body == 'string' ? body.length < 200 && body.trim() : b?.message ?? b?.error;
74
+ throw new SinterError(res.status, body, typeof m == 'string' && m ? m : `${res.status} ${res.statusText}`.trim());
75
+ }
76
+ return body;
77
+ }
78
+ return { send, raw, get inflight() { return inflight; } };
79
+ }
80
+ /** 204 or empty → null; JSON content type → parsed; anything else → text. */
81
+ export async function parseBody(res) {
82
+ const text = res.status == 204 ? '' : await res.text();
83
+ if (!text)
84
+ return null;
85
+ if (!/[/+]json\b/i.test(res.headers.get('Content-Type') ?? ''))
86
+ return text;
87
+ try {
88
+ return JSON.parse(text);
89
+ }
90
+ catch {
91
+ throw new SinterError(res.status, text, 'invalid JSON');
92
+ }
93
+ }
@@ -0,0 +1,12 @@
1
+ /** Keys that could reach prototypes; dropped from input and refused in computed access. */
2
+ export declare const BAD_KEY: RegExp;
3
+ /** Guard for computed member access in expressions: `obj[k]` compiles to `obj[$k(k)]`. */
4
+ export declare const $k: <K>(key: K) => K;
5
+ /** Allowed schemes: http(s), mailto, tel and configured extras; javascript:, vbscript: and data: never. */
6
+ export declare const schemesOf: (extra: string[]) => Set<string>;
7
+ /** Whether `value` may be written to attribute `name` of `el`; parsed by the browser's own URL parser. */
8
+ export declare function urlAllowed(el: Element, name: string, value: string, base: string, schemes: Set<string>): boolean;
9
+ /** Style values that could load URLs. */
10
+ export declare const STYLE_BAD: RegExp;
11
+ /** Encodes one interpolated URL value. */
12
+ export declare const encodeSegment: (v: unknown) => string;
@@ -0,0 +1,34 @@
1
+ // Runtime checks for the DOM sinks that cannot be closed at build time.
2
+ /** Keys that could reach prototypes; dropped from input and refused in computed access. */
3
+ export const BAD_KEY = /^(constructor|__proto__|prototype)$/;
4
+ /** Guard for computed member access in expressions: `obj[k]` compiles to `obj[$k(k)]`. */
5
+ export const $k = (key) => {
6
+ if (BAD_KEY.test(key))
7
+ throw TypeError(`sinter: [${String(key)}] is not allowed`);
8
+ return key;
9
+ };
10
+ const URL_ATTR = /^(href|src|action|formaction|poster|cite|data|ping|xlink:href)$/;
11
+ /** Allowed schemes: http(s), mailto, tel and configured extras; javascript:, vbscript: and data: never. */
12
+ export const schemesOf = (extra) => new Set(['http:', 'https:', 'mailto:', 'tel:', ...extra
13
+ .map((s) => s.toLowerCase().replace(/:?$/, ':'))
14
+ .filter((s) => !/^(javascript|vbscript|data):$/.test(s))]);
15
+ /** Whether `value` may be written to attribute `name` of `el`; parsed by the browser's own URL parser. */
16
+ export function urlAllowed(el, name, value, base, schemes) {
17
+ const ok = (v, attr) => {
18
+ try {
19
+ const p = new URL(v, base).protocol;
20
+ return schemes.has(p) || (/^(data|blob):$/.test(p) && attr == 'src' && /^(img|video|audio|source)$/.test(el.localName));
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ };
26
+ name = name.toLowerCase();
27
+ if (name.endsWith('srcset'))
28
+ return value.split(',').every((c) => ok(c.trim().split(/\s+/)[0], 'src'));
29
+ return !URL_ATTR.test(name) || ok(value, name);
30
+ }
31
+ /** Style values that could load URLs. */
32
+ export const STYLE_BAD = /url\(|image(-set)?\(|\\/i;
33
+ /** Encodes one interpolated URL value. */
34
+ export const encodeSegment = (v) => encodeURIComponent(String(v ?? '')).replace(/^\.\.?$/, (d) => d.replace(/\./g, '%2E'));
@@ -0,0 +1,95 @@
1
+ export type Change<T = any> = {
2
+ resource: string;
3
+ op: 'upsert';
4
+ item: T;
5
+ } | {
6
+ resource: string;
7
+ op: 'patch';
8
+ key: string;
9
+ fields: Partial<T>;
10
+ } | {
11
+ resource: string;
12
+ op: 'remove';
13
+ key: string;
14
+ } | {
15
+ resource: string;
16
+ op: 'refetch';
17
+ };
18
+ export type Op = {
19
+ op: 'upsert';
20
+ item: any;
21
+ } | {
22
+ op: 'patch';
23
+ key: string;
24
+ fields: any;
25
+ } | {
26
+ op: 'remove';
27
+ key: string;
28
+ };
29
+ export interface Meta {
30
+ loading: boolean;
31
+ error: {
32
+ status: number;
33
+ body: unknown;
34
+ message: string;
35
+ } | null;
36
+ loadedAt: number | null;
37
+ pending: number;
38
+ total: number | null;
39
+ page?: number;
40
+ pages?: number | null;
41
+ from?: number | null;
42
+ to?: number | null;
43
+ hasPrev?: boolean;
44
+ hasNext?: boolean;
45
+ }
46
+ export interface ResourceOptions {
47
+ key: string[] | null;
48
+ order: string | null;
49
+ locale?: () => string | undefined;
50
+ warn?: (code: number, ...args: unknown[]) => void;
51
+ /** applied to every stored item (dev builds freeze them) */
52
+ prep?: <T>(item: T) => T;
53
+ }
54
+ /** The public store handle (`app.resource(name)`), plus the hooks loaders and features use. */
55
+ export interface Resource<T extends object = any> {
56
+ readonly name: string;
57
+ readonly meta: Meta;
58
+ readonly key: string[] | null;
59
+ readonly isCollection: boolean;
60
+ get(key: string | T): T | undefined;
61
+ has(key: string | T): boolean;
62
+ all(): readonly T[];
63
+ /** what expressions see: the ordered array, or the single object (null when absent) */
64
+ value(): readonly T[] | T | null;
65
+ replace(items: T[] | T | null): void;
66
+ upsert(item: T): void;
67
+ patch(key: string | T, fields: Partial<T>): void;
68
+ remove(key: string | T): void;
69
+ refetch(): Promise<void>;
70
+ subscribe(fn: (change: Change<T>) => void): () => void;
71
+ keyOf(item: string | T): string;
72
+ /**
73
+ * Loads in flight: writes made while a load runs are replayed on top of its response,
74
+ * so a stale response cannot undo them. `startLoad` returns a mark for `endLoad`.
75
+ */
76
+ startLoad(): number;
77
+ endLoad(mark: number, loaded: boolean): void;
78
+ addLayer(ops: Op[]): number;
79
+ dropLayer(id: number): void;
80
+ setTotal(total: number | null): void;
81
+ /** what the server last confirmed, without optimistic layers (in arrival order) */
82
+ base(): {
83
+ items: T[] | T | null;
84
+ total: number | null;
85
+ };
86
+ /** while set, upsert/patch/remove are recorded instead of applied */
87
+ recording: Op[] | null;
88
+ loader: {
89
+ refetch(): Promise<void>;
90
+ } | null;
91
+ onDirty(what: 'data' | 'meta'): void;
92
+ }
93
+ /** Copies own enumerable fields, dropping keys that could pollute prototypes. */
94
+ export declare function clean<T>(obj: T): T;
95
+ export declare function createResource<T extends object = any>(name: string, o: ResourceOptions): Resource<T>;