@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.
Files changed (45) hide show
  1. package/README.md +39 -12
  2. package/dist/check/index.d.ts +4 -1
  3. package/dist/check/index.js +9 -8
  4. package/dist/check/shadow.d.ts +3 -1
  5. package/dist/check/shadow.js +53 -5
  6. package/dist/cli.js +2 -2
  7. package/dist/compiler/check-unit.d.ts +26 -2
  8. package/dist/compiler/compile.d.ts +4 -0
  9. package/dist/compiler/compile.js +314 -30
  10. package/dist/compiler/diagnostic-docs.js +20 -5
  11. package/dist/compiler/diagnostics.d.ts +3 -0
  12. package/dist/compiler/diagnostics.js +3 -0
  13. package/dist/compiler/expr.d.ts +24 -0
  14. package/dist/compiler/expr.js +53 -8
  15. package/dist/compiler/index.d.ts +3 -2
  16. package/dist/compiler/index.js +70 -16
  17. package/dist/compiler/vocabulary.d.ts +1 -0
  18. package/dist/compiler/vocabulary.js +9 -3
  19. package/dist/each.d.ts +3 -0
  20. package/dist/each.js +121 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/ir.d.ts +30 -1
  23. package/dist/listbox.d.ts +3 -0
  24. package/dist/listbox.js +66 -0
  25. package/dist/lsp/typescript.js +1 -1
  26. package/dist/lsp/workspace.js +3 -2
  27. package/dist/paginate.js +18 -4
  28. package/dist/project.d.ts +6 -0
  29. package/dist/project.js +22 -19
  30. package/dist/runtime/app.d.ts +35 -1
  31. package/dist/runtime/app.js +178 -40
  32. package/dist/runtime/config.d.ts +10 -3
  33. package/dist/runtime/config.js +10 -3
  34. package/dist/runtime/request.d.ts +1 -1
  35. package/dist/runtime/request.js +6 -1
  36. package/dist/runtime/store.d.ts +7 -4
  37. package/dist/runtime/store.js +154 -41
  38. package/dist/sse.js +8 -5
  39. package/dist/testing/index.d.ts +2 -0
  40. package/dist/testing/index.js +17 -2
  41. package/dist/vite.js +4 -2
  42. package/docs/diagnostics.md +35 -5
  43. package/html-custom-data.json +46 -2
  44. package/llms.txt +112 -6
  45. 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.1.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",