@tiaforge-ui/sinter 0.1.0 → 0.3.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/README.md +39 -12
- package/dist/check/index.d.ts +4 -1
- package/dist/check/index.js +9 -8
- package/dist/check/shadow.d.ts +3 -1
- package/dist/check/shadow.js +53 -5
- package/dist/cli.js +2 -2
- package/dist/compiler/check-unit.d.ts +26 -2
- package/dist/compiler/compile.d.ts +4 -0
- package/dist/compiler/compile.js +314 -30
- package/dist/compiler/diagnostic-docs.js +20 -5
- package/dist/compiler/diagnostics.d.ts +3 -0
- package/dist/compiler/diagnostics.js +3 -0
- package/dist/compiler/expr.d.ts +24 -0
- package/dist/compiler/expr.js +53 -8
- package/dist/compiler/index.d.ts +3 -2
- package/dist/compiler/index.js +70 -16
- package/dist/compiler/vocabulary.d.ts +1 -0
- package/dist/compiler/vocabulary.js +9 -3
- package/dist/each.d.ts +3 -0
- package/dist/each.js +121 -0
- package/dist/index.d.ts +1 -1
- package/dist/ir.d.ts +30 -1
- package/dist/listbox.d.ts +3 -0
- package/dist/listbox.js +66 -0
- package/dist/lsp/typescript.js +1 -1
- package/dist/lsp/workspace.js +3 -2
- package/dist/paginate.js +18 -4
- package/dist/project.d.ts +6 -0
- package/dist/project.js +22 -19
- package/dist/runtime/app.d.ts +35 -1
- package/dist/runtime/app.js +178 -40
- package/dist/runtime/config.d.ts +10 -3
- package/dist/runtime/config.js +10 -3
- package/dist/runtime/request.d.ts +1 -1
- package/dist/runtime/request.js +6 -1
- package/dist/runtime/store.d.ts +7 -4
- package/dist/runtime/store.js +154 -41
- package/dist/sse.js +8 -5
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +17 -2
- package/dist/vite.js +4 -2
- package/docs/diagnostics.md +35 -5
- package/html-custom-data.json +46 -2
- package/llms.txt +112 -6
- package/package.json +6 -1
package/llms.txt
CHANGED
|
@@ -7,6 +7,7 @@ Package: @tiaforge-ui/sinter. Every directive is a `data-sn-*` attribute or an `
|
|
|
7
7
|
## Attributes
|
|
8
8
|
|
|
9
9
|
- `data-sn-state` (object literal): declares local reactive state for the element and its subtree
|
|
10
|
+
- `data-sn-let` (object literal {name: expression}): derived names for the element and its subtree, computed where they are read; later names may use earlier ones
|
|
10
11
|
- `data-sn-text` (value expression): sets textContent; never HTML
|
|
11
12
|
- `data-sn-attr` (object literal {href: …}): literal keys; null/false removes, true sets empty; URL values are checked
|
|
12
13
|
- `data-sn-class` (object literal {unread: …}): toggles each class
|
|
@@ -18,26 +19,31 @@ Package: @tiaforge-ui/sinter. Every directive is a `data-sn-*` attribute or an `
|
|
|
18
19
|
- `data-sn-key` (value expression): row identity; defaults to the resource key or object identity
|
|
19
20
|
- `data-sn-if` (value expression): renders a <template> while truthy
|
|
20
21
|
- `data-sn-else`: on a <template> directly after an if template
|
|
22
|
+
- `data-sn-listbox` (id of the suggestion list, or empty): on a text input: arrow keys mark a [role=option] of that list (empty: the nearest [role=listbox] around the input), Enter or Tab clicks it, Escape fires `dismiss`
|
|
21
23
|
- `data-sn-persist` (storage key): persists the model or state in localStorage under sn:
|
|
22
24
|
- `data-sn-get` (URL template): GET action; latest wins
|
|
23
25
|
- `data-sn-post` (URL template): POST action
|
|
24
26
|
- `data-sn-put` (URL template): PUT action
|
|
25
27
|
- `data-sn-patch` (URL template): PATCH action
|
|
26
28
|
- `data-sn-delete` (URL template): DELETE action
|
|
29
|
+
- `data-sn-call` (action call): local action: calls a function from the actions module, e.g. pgp.decrypt(msg)
|
|
27
30
|
- `data-sn-body` (value expression): JSON request body; overrides form serialization
|
|
28
31
|
- `data-sn-trigger` (DOM event name): overrides the default trigger
|
|
29
|
-
- `data-sn-debounce` (milliseconds): delays the trigger, resets on each event
|
|
32
|
+
- `data-sn-debounce` (milliseconds): delays the trigger, resets on each event; without a method, delays the element's data-sn-on-* handlers
|
|
30
33
|
- `data-sn-concurrency` (block | queue | latest): mutations only; default block for forms, queue otherwise
|
|
31
34
|
- `data-sn-optimistic` (handler expression): patch/upsert/remove calls applied before the request, rolled back on failure
|
|
32
35
|
- `data-sn-then` (handler expression): runs after a 2xx response; $result = parsed body
|
|
33
|
-
- `data-sn-catch` (handler expression): runs after an error; $error = {status, body, message}
|
|
36
|
+
- `data-sn-catch` (handler expression): runs after an error; $error = {status, body, message, detail}
|
|
37
|
+
- `data-sn-confirm` (value expression): asks this question before the action runs (configure({ confirm }), window.confirm by default); no question when falsy
|
|
38
|
+
- `data-sn-encoding` (json | form): form: the body (data-sn-body, arrays as repeated keys, or the form) goes as application/x-www-form-urlencoded
|
|
34
39
|
- `data-sn-path` (URL patterns /a/:b …): on <html>: the path shapes this page is served for; `:name` segments appear read-only in $url
|
|
35
40
|
- `data-sn-data` (resource name): on <script type="application/json"> in <head>: initial data for a resource
|
|
36
41
|
- `data-sn-on-<event>` (handler expression): plain event handler, no request; e.g. data-sn-on-click
|
|
42
|
+
- `data-sn-debounce-<event>` (milliseconds): delays only that event's data-sn-on-<event> handler (it runs with the last event); e.g. data-sn-debounce-input
|
|
37
43
|
|
|
38
44
|
## Elements
|
|
39
45
|
|
|
40
|
-
- `<sn-resource>` `name`, `src`, `key`, `items`, `total`, `order`, `when`, `paginate`, `page-size`, `size-param`, `cursor`, `direction-param`, `page-param`, `type`, `cache`: a named, page-wide cache of server data; `cache` (or `cache="10m"`) shows the data from the last page load at once, then revalidates
|
|
46
|
+
- `<sn-resource>` `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`: a named, page-wide cache of server data; `cache` (or `cache="10m"`) shows the data from the last page load at once, then revalidates
|
|
41
47
|
- `<sn-sync>` `src`: one server event stream
|
|
42
48
|
- `<sn-event>` `name`, `resource`, `op`, `item`, `set`, `when`, `fallback`, `type`: maps one server event to a change message; child of <sn-sync>
|
|
43
49
|
- `<sn-include>` `src`: inlines a partial at build time
|
|
@@ -59,11 +65,11 @@ Package: @tiaforge-ui/sinter. Every directive is a `data-sn-*` attribute or an `
|
|
|
59
65
|
|
|
60
66
|
## Store functions
|
|
61
67
|
|
|
62
|
-
`refresh()`, `upsert()`, `patch()`, `remove()`, `page()`, `close()`, `reset()`, `emit()`: `refresh('name')`, `upsert('name', item)`, `patch('name', item, {…})`, `remove('name', item)`, `page('name', 'next' | 'prev' | 'first' | 'last' | n)`, `close()` the nearest dialog, `reset()` the nearest form, `emit('event', detail)`.
|
|
68
|
+
`refresh()`, `upsert()`, `patch()`, `remove()`, `page()`, `showModal()`, `close()`, `reset()`, `emit()`: `refresh('name')`, `upsert('name', item)`, `patch('name', item, {…})`, `remove('name', item)`, `page('name', 'next' | 'prev' | 'first' | 'last' | n)`, `showModal('id')` opens a dialog by its literal id, `close()` the nearest dialog or `close('id')` that one, `reset()` the nearest form, `emit('event', detail)`.
|
|
63
69
|
|
|
64
70
|
## Diagnostics
|
|
65
71
|
|
|
66
|
-
`SN101 unknown-attribute`, `SN102 wrong-element`, `SN103 missing-key`, `SN104 unsupported-model`, `SN201 unknown-identifier`, `SN202 forbidden-global`, `SN203 side-effect`, `SN204 optimistic-scope`, `SN205 resource-mutation`, `SN206 unknown-namespace-member`, `SN207 unsafe-sink`, `SN208 type-error`, `SN301 unknown-resource`, `SN302 duplicate-resource`, `SN303 unknown-meta-field`, `SN304 keyset-jump`, `SN305 include-not-found`, `SN306 include-cycle`, `SN307 openapi-invalid`, `SN308 unknown-schema-field`, `SN401 parse-error`, `SN501 unused-resource`, `SN502 non-reactive-value`, `SN503 data-bound-id`, `SN504 untyped-resource`, `SN505 unknown-query-param`, `SN506 untyped-event`.
|
|
72
|
+
`SN101 unknown-attribute`, `SN102 wrong-element`, `SN103 missing-key`, `SN104 unsupported-model`, `SN201 unknown-identifier`, `SN202 forbidden-global`, `SN203 side-effect`, `SN204 optimistic-scope`, `SN205 resource-mutation`, `SN206 unknown-namespace-member`, `SN207 unsafe-sink`, `SN208 type-error`, `SN209 name-conflict`, `SN301 unknown-resource`, `SN302 duplicate-resource`, `SN303 unknown-meta-field`, `SN304 keyset-jump`, `SN305 include-not-found`, `SN306 include-cycle`, `SN307 openapi-invalid`, `SN308 unknown-schema-field`, `SN309 unknown-dialog`, `SN310 unknown-action`, `SN401 parse-error`, `SN501 unused-resource`, `SN502 non-reactive-value`, `SN503 data-bound-id`, `SN504 untyped-resource`, `SN505 unknown-query-param`, `SN506 untyped-event`.
|
|
67
73
|
|
|
68
74
|
## Recipes
|
|
69
75
|
|
|
@@ -502,6 +508,106 @@ test('the next page load shows the cached list while it revalidates', async () =
|
|
|
502
508
|
});
|
|
503
509
|
```
|
|
504
510
|
|
|
511
|
+
### Local actions: hash a file, then upload it
|
|
512
|
+
|
|
513
|
+
```html
|
|
514
|
+
<section data-sn-state="{ file: null, url: '', error: '' }">
|
|
515
|
+
<sn-resource name="digest" call="sha256(file)" when="file"></sn-resource>
|
|
516
|
+
|
|
517
|
+
<input type="file" data-sn-on-change="file = $el.files[0] ?? null; url = ''">
|
|
518
|
+
<p data-sn-show="$meta.digest.loading">Hashing…</p>
|
|
519
|
+
<code data-sn-text="digest"></code>
|
|
520
|
+
|
|
521
|
+
<button data-sn-call="upload(file, digest)" data-sn-attr="{disabled: !digest}"
|
|
522
|
+
data-sn-then="url = $result.url; error = ''" data-sn-catch="error = $error.message">Upload</button>
|
|
523
|
+
<a data-sn-show="url" data-sn-attr="{href: url}">Uploaded</a>
|
|
524
|
+
<p role="alert" data-sn-text="error"></p>
|
|
525
|
+
</section>
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
```js
|
|
529
|
+
// src/sinter.actions.js: functions pages call with data-sn-call and <sn-resource call>.
|
|
530
|
+
// The last argument is sinter's context; with JSDoc types, `sinter check` checks the calls.
|
|
531
|
+
/** @import { CallContext } from '@tiaforge-ui/sinter' */
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* The file's SHA-256 as hex; choosing another file aborts the running hash through ctx.signal.
|
|
535
|
+
* @param {File} file
|
|
536
|
+
* @param {CallContext} ctx
|
|
537
|
+
* @returns {Promise<string>}
|
|
538
|
+
*/
|
|
539
|
+
export async function sha256(file, ctx) {
|
|
540
|
+
const bytes = await file.arrayBuffer();
|
|
541
|
+
ctx.signal.throwIfAborted();
|
|
542
|
+
const hash = await crypto.subtle.digest('SHA-256', bytes);
|
|
543
|
+
return [...new Uint8Array(hash)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* ctx.request goes through the app's middleware (headers, 401 handling, allowed origins).
|
|
548
|
+
* @param {File} file
|
|
549
|
+
* @param {string} digest
|
|
550
|
+
* @param {CallContext} ctx
|
|
551
|
+
* @returns {Promise<{ url: string }>}
|
|
552
|
+
*/
|
|
553
|
+
export async function upload(file, digest, ctx) {
|
|
554
|
+
const form = new FormData();
|
|
555
|
+
form.append('file', file);
|
|
556
|
+
form.append('sha256', digest);
|
|
557
|
+
return /** @type {Promise<{ url: string }>} */ (ctx.request('/api/uploads', { method: 'POST', body: form }));
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
import { expect, test } from 'vitest';
|
|
563
|
+
import { checkPage } from '@tiaforge-ui/sinter/check';
|
|
564
|
+
import { render, mockRequests } from '@tiaforge-ui/sinter/testing';
|
|
565
|
+
import * as actions from './local-actions.actions.js';
|
|
566
|
+
import source from './local-actions.actions.js?raw';
|
|
567
|
+
import page from './local-actions.html?raw';
|
|
568
|
+
|
|
569
|
+
const DIGEST = 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'; // sha256('abc')
|
|
570
|
+
|
|
571
|
+
function choose(input: HTMLInputElement, file: File) {
|
|
572
|
+
Object.defineProperty(input, 'files', { value: [file], configurable: true });
|
|
573
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
test('hashes the chosen file, then uploads it with its digest', async () => {
|
|
577
|
+
const requests = mockRequests({ 'POST /api/uploads': { url: '/files/1' } });
|
|
578
|
+
const { root, settle } = await render(page, { actions, requests });
|
|
579
|
+
const button = root.querySelector('button')!;
|
|
580
|
+
expect(button.disabled).toBe(true);
|
|
581
|
+
|
|
582
|
+
choose(root.querySelector('input')!, new File(['abc'], 'abc.txt'));
|
|
583
|
+
await settle();
|
|
584
|
+
expect(root.querySelector('code')!.textContent).toBe(DIGEST);
|
|
585
|
+
expect(button.disabled).toBe(false);
|
|
586
|
+
|
|
587
|
+
button.click();
|
|
588
|
+
await settle();
|
|
589
|
+
expect((requests.calls[0]!.body as FormData).get('sha256')).toBe(DIGEST);
|
|
590
|
+
expect(root.querySelector('a')!.getAttribute('href')).toBe('/files/1');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test('a failed upload shows the server message', async () => {
|
|
594
|
+
const requests = mockRequests({ 'POST /api/uploads': { status: 413, body: { message: 'File too large' } } });
|
|
595
|
+
const { root, settle } = await render(page, { actions, requests });
|
|
596
|
+
choose(root.querySelector('input')!, new File(['abc'], 'abc.txt'));
|
|
597
|
+
await settle();
|
|
598
|
+
root.querySelector('button')!.click();
|
|
599
|
+
await settle();
|
|
600
|
+
expect(root.querySelector('[role=alert]')!.textContent).toBe('File too large');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test('sinter check: arguments are checked against the actions', () => {
|
|
604
|
+
const files = { 'src/sinter.actions.js': source };
|
|
605
|
+
expect(checkPage(page, { files })).toEqual([]);
|
|
606
|
+
const [d] = checkPage(page.replace('upload(file, digest)', 'upload(file)'), { files });
|
|
607
|
+
expect([d!.code, d!.message]).toEqual(['SN208', expect.stringContaining("is not assignable to type '[file: File, digest: string]'")]);
|
|
608
|
+
});
|
|
609
|
+
```
|
|
610
|
+
|
|
505
611
|
## Type checking
|
|
506
612
|
|
|
507
613
|
`npx sinter check` type-checks every expression with the project's TypeScript (pages and options come from the Vite config). `sinter({ typecheck: true })` runs it in `vite build`.
|
|
@@ -525,7 +631,7 @@ Each open EventSource holds one HTTP connection, and HTTP/1.1 allows 6 per origi
|
|
|
525
631
|
|
|
526
632
|
Write a test next to the markup. `@tiaforge-ui/sinter/testing` compiles the page with the real compiler and mounts it:
|
|
527
633
|
|
|
528
|
-
- `render(html, { requests, streams, url, locale, now, helpers, files, storage, session })` → `{ root, app, flush, settle, advance, navigations, window }`. Pass one `memoryStorage()` as `session` to two renders to test what carries over between page loads (the `cache` attribute).
|
|
634
|
+
- `render(html, { requests, streams, url, locale, now, helpers, actions, files, storage, session })` → `{ root, app, flush, settle, advance, navigations, window }`. Pass one `memoryStorage()` as `session` to two renders to test what carries over between page loads (the `cache` attribute).
|
|
529
635
|
- `mockRequests({ 'GET /api/x': json | {status, body} | (call) => … })`: records `calls`; responses are held until `settle()`; an unmatched request fails the test and lists the routes.
|
|
530
636
|
- `mockStream()`: `emit(name, payload)`, `drop()`, `connected`. A stream URL with no mock uses the transport from `configure({ transport })`, so a custom transport can be tested over a fake connection (see the SignalR recipe).
|
|
531
637
|
- `flush()` runs the scheduler (optimistic changes are visible), `settle()` finishes requests, queues and debounces, `advance(ms)` moves sinter's clock.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiaforge-ui/sinter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Bind JSON APIs to plain HTML: keyed resources, SSE sync, template bindings and optimistic mutations, compiled at build time.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tia Heinrich",
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"./ws": "./dist/ws.js",
|
|
37
37
|
"./optimistic": "./dist/optimistic.js",
|
|
38
38
|
"./paginate": "./dist/paginate.js",
|
|
39
|
+
"./each": "./dist/each.js",
|
|
39
40
|
"./persist": "./dist/persist.js",
|
|
41
|
+
"./listbox": "./dist/listbox.js",
|
|
40
42
|
"./cache": "./dist/cache.js",
|
|
41
43
|
"./compiler": "./dist/compiler/index.js",
|
|
42
44
|
"./vite": "./dist/vite.js",
|
|
@@ -57,11 +59,14 @@
|
|
|
57
59
|
"build": "tsc -p tsconfig.build.json",
|
|
58
60
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
59
61
|
"test": "vitest run",
|
|
62
|
+
"bench": "vitest run --config bench/runtime/vitest.config.ts",
|
|
60
63
|
"size": "npm run build && node scripts/size.mjs",
|
|
61
64
|
"docs": "UPDATE_DOCS=1 vitest run test/docs.test.ts",
|
|
65
|
+
"release": "node scripts/release.mjs",
|
|
62
66
|
"prepublishOnly": "rm -rf dist && npm run build && npm test"
|
|
63
67
|
},
|
|
64
68
|
"dependencies": {
|
|
69
|
+
"@sveltejs/acorn-typescript": "^1.0.13",
|
|
65
70
|
"acorn": "^8.19.0",
|
|
66
71
|
"acorn-walk": "^8.3.5",
|
|
67
72
|
"parse5": "^8.1.0",
|