@nativele/ts-dom 0.4.0 → 0.4.6

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/CHANGELOG.md CHANGED
@@ -10,6 +10,54 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
10
10
  - Adding a lint rule is a minor change; lint rules are warnings or opt-in and do not count as breaking.
11
11
  - Before every release run `npm run verify:pack` (`publint` and `attw`), `tsc`, the unit tests, the browser tests, `npm run smoke` and `npm run smoke:no-bundler`; all of them run in `.github/workflows/ci.yml`.
12
12
 
13
+ ## 0.4.6
14
+
15
+ Released together with 0.4.1 to 0.4.5, which were not published on their own.
16
+
17
+ - `ts-dom rules --explain=<id>` explains any rule id: why it exists (tied to the principle it protects), a wrong example and the fix. `ts-dom rules --json` prints every rule as data. Every rule id has an explanation; a test keeps them in step.
18
+ - The README's Checks section says which tool checks what: a formatter such as oxfmt for well-formed markup and layout (it reports misnested tags that the library's parser would silently repair), `ts-dom gen` for the template rules, `tsc` for names and types, lint for usage, data flow and accessibility, `ts-dom doctor` for the set-up, and `ts-dom check` for all of them at once.
19
+
20
+ ## 0.4.5
21
+
22
+ Not published on its own; it ships with 0.4.6.
23
+
24
+ - Every rule id of `unused-markup`, `data-flow` and `a11y` is also a lint rule of its own (`ts-dom/unused-ref`, `ts-dom/missing-handler`, ...), so a project can enable rules one by one with their own severity. The three group rules stay.
25
+ - `missing-handler` offers a suggestion that adds the method to the component class.
26
+ - In the editor, the Oxc extension for VS Code (`oxlint --lsp`) shows the findings and offers the quick fix; `oxlint --fix-suggestions` applies it from the command line. Both are covered by tests that run the oxlint shipped with Vite+.
27
+ - Each template is analyzed once per file text and template text, however many rules ask for it.
28
+
29
+ ## 0.4.4
30
+
31
+ Not published on its own; it ships with 0.4.6.
32
+
33
+ - `ts-dom doctor [dirs...]` checks the project set-up: a tsconfig with `strict`, the DOM lib, a module resolution other than `classic` and an `include` covering the template folders (read through `extends`: relative paths, packages and arrays; a solution-style tsconfig through its references); the runtime package being resolvable; templates imported as `./x.html` instead of `./x.html.js`; stale generated files; the Vite and oxlint plugins being configured; and a formatter (oxfmt, Prettier) set up while templates keep their whitespace. Page entries and `dist` are not counted as templates, and a folder that does not exist is reported. Stable rule ids, a `Fix:` per issue, `--json`, exit code 1 on errors.
34
+
35
+ ## 0.4.3
36
+
37
+ Not published on its own; it ships with 0.4.6.
38
+
39
+ - `ts-dom check [dirs...]` runs everything in one pass for agents and CI: template diagnostics, generated files (regenerated, or reported as stale with `--no-write`), the project's own `tsc` (TypeScript 5, 6 or 7), and the lint rules (`--a11y` adds accessibility). TypeScript errors from the template contract point at the template line and column (`missing-binding`, `missing-handler`), item component problems get their own rule ids, and errors that only follow from a broken class (the ones naming that class) are left out. A tag defined twice is reported, and a tsc that does not run to completion (no tsconfig, a bad option) is reported as `tsc-failed` instead of passing silently. A solution-style tsconfig (`"files": []` with `references`) is checked through its referenced projects, a folder that does not exist is reported as `dir-not-found`, and every plain tsc error carries a `Fix:` hint. `--json` prints the issues as data; `--strict` fails on warnings.
40
+ - The README, the rules for AI agents and `llms.txt` use `ts-dom check` as the command to run after a change.
41
+
42
+ ## 0.4.2
43
+
44
+ Not published on its own; it ships with 0.4.6.
45
+
46
+ - `ts-dom gen --check` writes nothing and lists generated files that are missing, outdated or orphaned (a deleted template) and the errors that would make `ts-dom gen` fail, with exit code 1 if there are any; `--json` prints them as data. `ts-dom gen` removes an orphaned generated module. A module generated with whitespace `collapse` records it, and a later `gen`, `gen --check`, `check` or `doctor` without the option keeps that mode (`--whitespace=preserve` switches back).
47
+
48
+ ## 0.4.1
49
+
50
+ Not published on its own; it ships with 0.4.6.
51
+
52
+ - `npm run bench` measures lists of 1,000 to 10,000 items in real Chrome (create, update 10%, reverse, remove half, append) for component items, plain elements and a direct DOM baseline; the README's Performance section shows the results and when to choose which.
53
+ - Assigning `items` sets the prop of a new item before its element goes into the document, so the item component is connected with its item already set and renders once instead of first rendering without it. `replace()` and `upsert()` keep inserting first.
54
+ - README: a fourth example, a list of components (add, remove, sort, toggle, events up from the item), the one-line rule for choosing between `items =` and `replace()`, and how moving an item affects `connected()` and subscriptions with and without `moveBefore`. The introduction and the package description say what the library is built for: type safety and AI coding agents, which can correct their own mistakes from `tsc` errors and the `Fix:` hints of every diagnostic.
55
+ - `state`, `props`, the `view` passed to `render` and the argument of `derive` functions are typed `DeepReadonly` (class instances with private members keep their own type) (exported, with `PropAccessors`): changing an array or object inside them in place is a `tsc` error, since the list and child props compare by reference and would not update. `set()` accepts read-only values. A parent can still assign a child's props.
56
+
57
+ ### Breaking changes
58
+
59
+ - Code that annotates `render`'s `view`, or a helper that takes the state, with mutable array types (`Item[]`) must use read-only ones (`readonly Item[]` or `DeepReadonly<State>`), and code that changes state objects in place no longer compiles.
60
+
13
61
  ## 0.4.0
14
62
 
15
63
  ### Lists of components
package/README.md CHANGED
@@ -4,7 +4,9 @@ A TypeScript-driven, HTML-first UI library.
4
4
 
5
5
  Nativele publishes this library as `@nativele/ts-dom` (import it from `@nativele/ts-dom`); its command line tool is `ts-dom`.
6
6
 
7
- A small library (prototype) for managing HTML with TypeScript. HTML describes the structure and "where data goes", TypeScript owns behavior and state, and the DOM is the single source of truth for what is rendered.
7
+ A lightweight UI library: HTML describes the structure and where data goes, TypeScript owns behavior and state, and the native DOM renders it, with no virtual DOM and no template expressions. It is built for type safety and for working with AI agents: `tsc` checks every name the HTML uses, and every diagnostic says how to fix it. Complex UI is built by composing native custom elements.
8
+
9
+ **Nativele is made for AI coding agents.** An agent writes a template and a class, runs `ts-dom check`, and corrects its own mistakes from the type errors: a name the HTML uses that the class does not have, an event handler that does not exist, list items of the wrong shape. There is no template language to learn and no hidden reactivity to reason about, so what the agent reads in the HTML and the class is all that happens. Every diagnostic from `ts-dom gen` and `ts-dom lint` names its rule and ends with a `Fix:` hint the agent can apply, [`llms.txt`](llms.txt) describes the whole API on one page, and [Rules for AI Agents](#rules-for-ai-agents) is a list you can paste into a project's agent instructions.
8
10
 
9
11
  English | [繁體中文](README.zh-TW.md)
10
12
 
@@ -47,7 +49,7 @@ Six things cover most components:
47
49
  Paste these into your project's agent instructions (`AGENTS.md`, `CLAUDE.md` or similar):
48
50
 
49
51
  - Write the `.html` and the `.ts` of a component together; never edit `x.html.ts` or `ts-dom-tags.ts`, which `ts-dom gen` writes.
50
- - After changing any `.html`, run `npx ts-dom gen && npx tsc`, then fix every error the types report (a missing name in `state`, a missing handler method) instead of casting it away.
52
+ - After changing any `.html` or component, run `npx ts-dom check` (add `--json` to read the result as data). It regenerates the generated files, runs `tsc`, maps the template contract errors back to the template line and column, and runs the lint rules. Fix every issue it reports, following its `Fix:` hint, instead of casting a type error away.
51
53
  - Import templates as `./x.html.js`.
52
54
  - No expressions in HTML: put computed values in `derive`, and change state only with `this.set()`.
53
55
  - Every diagnostic from `ts-dom gen` and `ts-dom lint` ends with a `Fix:` hint; follow it. `npx ts-dom rules` lists all rule ids, and `npx ts-dom lint --a11y` also checks accessibility.
@@ -62,7 +64,7 @@ npx ts-dom gen # generate x.html.ts (fully typed) for every x.html; run it
62
64
  npx tsc # type-check and compile; no Vite or other bundler needed
63
65
  ```
64
66
 
65
- The three examples below are real, runnable files in `examples/readme/` (type-checked, linted, and tested).
67
+ The four examples below are real, runnable files in `examples/readme/` (type-checked, linted, and tested).
66
68
 
67
69
  ### Example 1: Counter (state, derived values, events, data-if)
68
70
 
@@ -147,7 +149,7 @@ export class TodoList extends Component(tpl, {
147
149
  }
148
150
 
149
151
  // Automatic binding handles {{ remaining }}; the list is filled here.
150
- override render(view: { items: Item[] }, changed: ReadonlySet<string>): void {
152
+ override render(view: { items: readonly Item[] }, changed: ReadonlySet<string>): void {
151
153
  if (!changed.has("items")) return;
152
154
  this.lists.items.replace(
153
155
  view.items,
@@ -229,6 +231,97 @@ define("order-line", OrderLine);
229
231
 
230
232
  The data flow goes in only one direction: the parent's state → the child's props (`this.refs.qty.value = ...`); the child's request → event → the parent's handler method.
231
233
 
234
+ ### Example 4: A list of components (`items =`, props down, events up)
235
+
236
+ When an item has anything of its own (state, events, a nested list), make it a component and fill the list with one assignment. The item component receives its task as a prop and sends changes up as events:
237
+
238
+ ```html
239
+ <!-- task-row.html -->
240
+ <label><input type="checkbox" data-ref="done" data-on-change="toggle" /> {{ title }}</label>
241
+ <button type="button" data-on-click="drop">Remove</button>
242
+ ```
243
+
244
+ ```ts
245
+ // task-row.ts
246
+ import { Component, define, event } from "@nativele/ts-dom";
247
+ import tpl from "./task-row.html.js";
248
+
249
+ export interface Task {
250
+ id: number;
251
+ title: string;
252
+ done: boolean;
253
+ }
254
+
255
+ /** One item of the list: it receives its task as a prop and asks the parent for changes through events. */
256
+ export class TaskRow extends Component(tpl, {
257
+ props: { task: null as Task | null },
258
+ derive: { title: (s) => s.task?.title ?? "" },
259
+ emits: { "task-toggle": event(), "task-remove": event() },
260
+ }) {
261
+ override render(view: { task: Task | null }): void {
262
+ const done = view.task?.done ?? false;
263
+ if (this.refs.done.checked !== done) this.refs.done.checked = done;
264
+ }
265
+ toggle(): void {
266
+ this.emit("task-toggle");
267
+ }
268
+ drop(): void {
269
+ this.emit("task-remove");
270
+ }
271
+ }
272
+ define("task-row", TaskRow);
273
+ ```
274
+
275
+ The parent names, on the `<template data-each>`, the field that identifies an item (`data-key`) and the prop that receives it (`data-prop`), and assigns the whole list in `render`:
276
+
277
+ ```html
278
+ <!-- task-board.html -->
279
+ <form data-on-submit="add">
280
+ <input data-ref="title" aria-label="New task" />
281
+ <button type="submit">Add</button>
282
+ </form>
283
+ <button type="button" data-on-click="sortByTitle">Sort by title</button>
284
+ <ul>
285
+ <template data-each="tasks" data-key="id" data-prop="task">
286
+ <task-row role="listitem" data-on-task-toggle="toggleTask" data-on-task-remove="removeTask"></task-row>
287
+ </template>
288
+ </ul>
289
+ ```
290
+
291
+ ```ts
292
+ // task-board.ts
293
+ import { Component, define } from "@nativele/ts-dom";
294
+ import type { EventHit } from "@nativele/ts-dom";
295
+ import type { Task } from "./task-row.js";
296
+ import tpl from "./task-board.html.js";
297
+
298
+ export class TaskBoard extends Component(tpl, { state: { tasks: [] as Task[], nextId: 1 } }) {
299
+ add(e: Event): void {
300
+ e.preventDefault();
301
+ const title = this.refs.title.value.trim();
302
+ if (!title) return;
303
+ this.set((s) => ({ tasks: [...s.tasks, { id: s.nextId, title, done: false }], nextId: s.nextId + 1 }));
304
+ this.refs.title.value = "";
305
+ }
306
+ // A changed task is a new object: the list sets the prop only on items that are not the same object as before.
307
+ toggleTask(_e: Event, hit: EventHit): void {
308
+ this.set((s) => ({ tasks: s.tasks.map((t) => (t.id === hit.key ? { ...t, done: !t.done } : t)) }));
309
+ }
310
+ removeTask(_e: Event, hit: EventHit): void {
311
+ this.set((s) => ({ tasks: s.tasks.filter((t) => t.id !== hit.key) }));
312
+ }
313
+ sortByTitle(): void {
314
+ this.set((s) => ({ tasks: [...s.tasks].sort((a, b) => a.title.localeCompare(b.title)) }));
315
+ }
316
+ override render(view: { tasks: readonly Task[] }, changed: ReadonlySet<string>): void {
317
+ if (changed.has("tasks")) this.lists.tasks.items = view.tasks;
318
+ }
319
+ }
320
+ define("task-board", TaskBoard);
321
+ ```
322
+
323
+ Adding, removing, sorting and toggling all go through `this.set()`; the list keeps the element of each task that stays (so a checkbox keeps its focus), creates one for a new task, removes the ones that are gone, and sets `task` only on items that are a new object. `tsc` checks that the assigned items match the `task` prop of `<task-row>` and have an `id`.
324
+
232
325
  ## Design Principles
233
326
 
234
327
  - HTML is the single source of structure.
@@ -287,6 +380,8 @@ Whitespace in text is kept as written. To collapse each run of whitespace into o
287
380
 
288
381
  During development, use `npx ts-dom gen --watch` to keep watching `.html` files, together with `tsc --watch`.
289
382
 
383
+ `npx ts-dom gen --check` writes nothing: it lists generated files that are missing, outdated (their template or a `define()` changed) or orphaned (their template was deleted), and exits with code 1 if there are any (`--json` for machine-readable output). Use it in CI if your project commits the generated files, or to confirm they are fresh before reading type errors.
384
+
290
385
  In your code, import templates with a `.js` suffix (standard NodeNext style; TS maps it to the generated `x.html.ts`):
291
386
 
292
387
  ```ts
@@ -339,6 +434,10 @@ lint: {
339
434
  },
340
435
  ```
341
436
 
437
+ Each rule id is also a rule of its own, so a project can enable rules one by one and give each its own severity, for example `"ts-dom/unused-ref": "warn"` or `"ts-dom/missing-handler": "error"` instead of the whole `ts-dom/unused-markup` group. `ts-dom rules` lists the ids.
438
+
439
+ **In the editor.** Install the Oxc extension for VS Code (it runs `oxlint --lsp`) and keep the plugin in your oxlint config: the findings show on the template's import line as you type, and `missing-handler` offers a quick fix that adds the method to the component class. From the command line, `oxlint --fix-suggestions` applies the same fix. A fix can only change the file being linted, so problems that live in the HTML (an unused `data-ref`, broken markup) are reported but not fixed there; for markup structure and layout use a formatter such as oxfmt.
440
+
342
441
  Without oxlint, the same checks are available through the CLI:
343
442
 
344
443
  ```sh
@@ -347,6 +446,8 @@ npx ts-dom lint --a11y # additionally check ARIA / accessibility (warnings)
347
446
  npx ts-dom lint --strict # warnings also make the exit code 1 (errors always do)
348
447
  npx ts-dom lint --json # output JSON: { "issues": [{ file, line, col, severity, rule, message }] }
349
448
  npx ts-dom rules # list all rule ids and descriptions
449
+ npx ts-dom rules --explain=unused-ref # why the rule exists, a wrong example and the fix
450
+ npx ts-dom rules --json # every rule as data: { id, group, description, why, bad, good }
350
451
  ```
351
452
 
352
453
  Every message has the same format. It explains **what is wrong, why, and how to fix it**, and includes the line and column in the HTML:
@@ -358,7 +459,7 @@ a.html:3:1 warning [unknown-prop] <x-kid lable> is not a declared prop of <x-kid
358
459
 
359
460
  Exit codes: 0 = no problems, 1 = there are errors (or warnings when `--strict` is used). Messages from the oxlint plugin start with `./x.html.js:line:column [rule id] message` and are reported on the `import` line.
360
461
 
361
- Recommended check workflow: `ts-dom gen && tsc && ts-dom lint`.
462
+ Recommended check workflow: `ts-dom check` (it runs `gen`, `tsc` and `lint` in one pass; see "Checks").
362
463
 
363
464
  ## How Types Are Generated
364
465
 
@@ -511,6 +612,8 @@ Design trade-offs:
511
612
  - `set` may be called inside `render`; it is processed in another round after the current one ends. If it has not stabilized after 50 consecutive rounds, a "suspected infinite loop" error is thrown.
512
613
  - When `state` is not used, the original `refs`, `text`, `show`, `bind`, and `createStore` work as before, fully backward compatible.
513
614
 
615
+ **Read-only all the way down**: `state`, `props`, the `view` passed to `render` and the argument of each `derive` function are typed `DeepReadonly`: arrays and objects inside them cannot be changed in place. `this.state.items[0].done = true` or `this.state.items.push(x)` is a `tsc` error, because an object changed in place keeps its identity and the list and child props, which compare by reference, would not update. Pass new objects instead: `this.set((s) => ({ items: s.items.map((i) => (i.id === id ? { ...i, done: true } : i)) }))`. When you annotate `render`'s `view` or a helper that takes the state, use read-only types (`readonly Item[]`, or `DeepReadonly<State>` from `@nativele/ts-dom`).
616
+
514
617
  ## Events: data-on-*
515
618
 
516
619
  HTML writes only the event name and the method name; the logic is in TS:
@@ -646,15 +749,24 @@ The example [examples/signup](examples/signup/signup-form.ts) does exactly this,
646
749
 
647
750
  ## Performance
648
751
 
649
- Measurement environment: real Chrome (WSL), one text hole per item, counting only script execution time (excluding layout and paint), a single measurement, so treat it as an order-of-magnitude reference.
650
-
651
- | List size | Initial creation | Identical data | Change 1 item | Reverse order |
652
- |---|---|---|---|---|
653
- | 1,000 | about 11 ms | about 0.6 ms | about 0.4 ms | about 1.3 ms |
654
- | 5,000 | about 23–34 ms | about 2 ms | about 0.9 ms | about 6–8 ms |
655
- | 10,000 | about 65 ms | about 9 ms | about 2 ms | about 19 ms |
656
-
657
- "Identical data" is the cost of `render` re-running the whole list every time: text nodes are not written when their value has not changed, so it is cheap. The budget for one frame is about 16 ms, so re-running tens of thousands of items on every keystroke gets tight; use `changed` to skip, use `skipUnchanged`, or split the list into smaller ones. Event delegation does not add listeners as the number of items grows.
752
+ `npm run bench` runs a large list benchmark in real Chrome and prints this table. Each item shows one text; the times are the median of 5 runs and count script execution only (no layout or paint). They were measured on one machine (Chrome on WSL), so read them as proportions, not absolute numbers. "Direct DOM" is a hand-written keyed list with plain DOM calls, the floor any library adds to.
753
+
754
+ | Approach | Items | Create | Update 10% | Reverse | Remove half | Append 1,000 |
755
+ |---|---|---|---|---|---|---|
756
+ | Components (`items =`) | 1,000 | 15.4 ms | 0.5 ms | 1.8 ms | 1.1 ms | 14.6 ms |
757
+ | Components (`items =`) | 5,000 | 80.5 ms | 1.6 ms | 10 ms | 5.4 ms | 17.6 ms |
758
+ | Components (`items =`) | 10,000 | 160.6 ms | 3.2 ms | 21 ms | 11.3 ms | 16 ms |
759
+ | Plain elements (`replace()`) | 1,000 | 2.8 ms | 0.1 ms | 0.7 ms | 0.2 ms | 2.9 ms |
760
+ | Plain elements (`replace()`) | 5,000 | 15.8 ms | 0.7 ms | 4.3 ms | 1.7 ms | 3.7 ms |
761
+ | Plain elements (`replace()`) | 10,000 | 42 ms | 1.4 ms | 8.6 ms | 3.3 ms | 4.4 ms |
762
+ | Direct DOM (baseline) | 1,000 | 0.8 ms | 0.2 ms | 1 ms | 0.3 ms | 0.8 ms |
763
+ | Direct DOM (baseline) | 5,000 | 4 ms | 0.8 ms | 4.3 ms | 1.6 ms | 1.1 ms |
764
+ | Direct DOM (baseline) | 10,000 | 8.7 ms | 1.7 ms | 10.9 ms | 3.9 ms | 1.6 ms |
765
+
766
+ - Creating component items costs about 15 µs per item, because each item is a custom element with its own template, refs and lifecycle; plain elements cost about 3–4 µs and direct DOM about 1 µs.
767
+ - Once the items exist, updating, reordering and removing them stay within about two times of direct DOM: only items that are a new object are touched, and kept elements are moved, not rebuilt.
768
+ - A frame has about 16 ms. A list of a few thousand component items is comfortable; for tens of thousands, use plain elements with `replace()`, show a page or a window of the list at a time, or keep component items for the rows that need state and events.
769
+ - Event delegation adds no listeners as the list grows.
658
770
 
659
771
  ## Examples
660
772
 
@@ -695,7 +807,7 @@ There is no host element, so `data-on-*` attributes do nothing here (listen on `
695
807
 
696
808
  ## Lists
697
809
 
698
- There are two ways to fill a list. Use components when an item has anything of its own (state, events, a nested list), and `replace()` for small items made of plain elements.
810
+ There are two ways to fill a list, and one rule decides between them: **an item that has anything of its own (state, events, a nested list) is a component, filled with `items =`; an item that is a few texts and `data-if` is plain elements, filled with `replace()`.** Example 4 shows the first, Example 2 the second.
699
811
 
700
812
  ### Items as components: `items = ...`
701
813
 
@@ -758,9 +870,16 @@ Notes:
758
870
  - `data-each` cannot be nested. Split it into two templates: put the inner list in its own `.html` (a child component with its own `data-each`) and use that component's tag inside the outer item.
759
871
  - `<slot>` cannot be placed inside an item.
760
872
 
873
+ ### Moving items, connecting and subscriptions
874
+
875
+ - In a browser with `moveBefore`, a moved item stays connected: neither `disconnected()` nor `connected()` runs, and its focus, scroll position and subscriptions are untouched.
876
+ - In a browser without it, a moved item is taken out and put back: `disconnected()` runs and releases everything registered with `bind`, `on` and `onCleanup`, then `connected()` runs again. Its element, props and state stay the same.
877
+ - So subscribe in `connected()` (`this.bind(store, ...)`, `this.on(...)`), not in the constructor or a field initializer: then the subscription is made again after a move in every browser. A subscription made in the constructor is released by the first move and never comes back.
878
+ - Data that every item needs (a theme, the current user, a registry) belongs in a `createStore` that the item component binds in `connected()`, not copied into every item object.
879
+
761
880
  ### Order of list updates
762
881
 
763
- `replace()` and `upsert()` put a new item's element into the document first and then call `update`, so a child component placed in an item is connected before the parent has set its properties. Build per-item state in the child when it first receives data (for example in a property setter), and guard a render in `connected()` with the data it needs.
882
+ Assigning `items` sets the `data-prop` property of a new item before its element goes into the document, so the item component is connected with its item already set and its first render shows it. `replace()` and `upsert()` put a new item's element into the document first and then call `update`, so a child component placed in such an item is connected before the parent has set its properties; there, build per-item state when the child first receives data (for example in a property setter), and guard a render in `connected()` with the data it needs.
764
883
 
765
884
  ## Trusted Types, Translations and Icons
766
885
 
@@ -776,8 +895,23 @@ Notes:
776
895
 
777
896
  ## Checks
778
897
 
898
+ Each tool checks what it can see, and none repeats another:
899
+
900
+ | Tool | What it checks |
901
+ |---|---|
902
+ | A formatter such as oxfmt (`oxfmt --check`, or `vp fmt --check`) | Whether the template markup is well formed (a misnested closing tag such as `<p><div></p></div>` is a syntax error there, while the HTML parser the library uses would silently repair it into a different tree) and laid out consistently. It knows nothing of `{{ }}` or `data-*` names. Generate with whitespace `collapse` when you use one, or reformatting can change the spacing on the page. |
903
+ | `ts-dom gen` | The template rules: holes, `data-each`, `data-if`, `data-on-*`, forbidden markup. |
904
+ | `tsc` | That every name in the HTML exists in the component with the right type, and that lists, props and events fit. |
905
+ | `ts-dom lint` / the oxlint plugin | What the HTML declares but the class never uses, data flow between components, accessibility. |
906
+ | `ts-dom doctor` | The project set-up that the other tools depend on. |
907
+ | `ts-dom check` | Runs `gen`, `tsc` and `lint` in one pass and points the template contract errors at the template. |
908
+
779
909
  Four complementary checks. **Every message has a stable rule id and explains what is wrong, why, and how to fix it**; `npx ts-dom rules` lists all rules.
780
910
 
911
+ **When the set-up itself may be wrong: `npx ts-dom doctor [dirs...]`.** It checks what a project needs before any of the checks can work: a tsconfig with `strict`, the DOM lib, a module resolution that maps `./x.html.js` to the generated file and an `include` that covers the template folders; the runtime package being installed; templates imported as `./x.html.js` (not `./x.html`); generated files that are stale; the Vite plugin and the oxlint plugin being configured; and, when a formatter such as oxfmt or Prettier is set up, templates generated with whitespace `collapse`. Every issue has a stable rule id and a `Fix:` hint, and `--json` prints them as data.
912
+
913
+ **One command for all of them: `npx ts-dom check [dirs...]`.** It reports template diagnostics, regenerates the generated files (with `--no-write` it reports stale ones instead), runs the project's own `tsc` (TypeScript 5, 6 or 7; `--project=` picks the tsconfig), and runs the lint rules (`--a11y` adds the accessibility checks). TypeScript errors that come from the template contract point at the template, not at the `Component(...)` call: a `{{ title }}` with nothing named `title` is reported at its line and column in the `.html` as `missing-binding`, and a missing handler as `missing-handler`; item component problems get their own rule ids, and the errors that only follow from a broken class are left out. `--json` prints the issues as data, and the exit code is 1 when there is an error (with `--strict`, a warning too).
914
+
781
915
  1. **codegen diagnostics** (`ts-dom gen` and the Vite plugin, with line and column numbers; on errors they abort and write no files): duplicate `data-ref`, `data-each` without a single root element, `data-each` not on a `<template>`, nested `data-each`, an incomplete or invalid `data-key` / `data-prop`, a `data-key` list whose item is not one custom element, `data-if` on an item root element, `<slot>` inside an item, `<style>` not at the first level, invalid holes, holes inside attributes, invalid `data-on-*` (including names that collide with DOM or ts-dom members), `<html>`, `<head>` and `<body>` inside a template, and markup that runs code or Trusted Types blocks: `<script>`, `on*` attributes, `srcdoc`, and `javascript:` URLs.
782
916
  2. **tsc**: an error when TypeScript uses a name the HTML does not declare (`this.refs.nope`, `this.show("nope", true)`); all constraints between `state`, `props`, `derive`, `emits` and the HTML (see "Data Flow"); child component prop names and types; `define()` checks that the methods for `data-on-*` exist.
783
917
  3. **Runtime**: `Component()` and components, at definition, connection, and use, throw errors that carry a `[rule id]` and a fix for the same problems.
package/README.zh-TW.md CHANGED
@@ -6,7 +6,9 @@ Nativele 以 `@nativele/ts-dom` 發布這個函式庫(從 `@nativele/ts-dom`
6
6
 
7
7
  [English](README.md) | 繁體中文
8
8
 
9
- 用 TypeScript 管理 HTML 的小型函式庫(原型)。HTML 描述結構與「資料要放的位置」,TypeScript 負責行為與狀態,DOM 就是唯一的真實呈現。
9
+ 輕量的 UI 函式庫:HTML 描述結構與資料要放的位置,TypeScript 負責行為與狀態,由原生 DOM 渲染,沒有虛擬 DOM,也沒有範本表達式。它為型別安全與 AI agent 協作而設計:`tsc` 會檢查 HTML 用到的每個名稱,每則診斷都說明怎麼修。複雜的介面以原生自訂元素組合而成。
10
+
11
+ **Nativele 是為 AI coding agent 打造的。** agent 寫好範本與類別,執行 `ts-dom check`,就能從型別錯誤自行修正:HTML 用了類別沒有的名稱、事件處理方法不存在、清單項目的形狀不對。沒有要另外學的範本語言,也沒有要推敲的隱式響應式,agent 在 HTML 與類別裡讀到的就是實際發生的全部。`ts-dom gen` 與 `ts-dom lint` 的每則診斷都標明規則並附上可直接套用的 `Fix:` 提示,[`llms.txt`](llms.txt) 用一頁說明整個 API,[給 AI agent 的規則](#給-ai-agent-的規則) 則是可以直接貼進專案 agent 指引檔的清單。
10
12
 
11
13
  ```html
12
14
  <!-- user-card.html:HTML 只描述結構、資料要放的位置、事件要找哪個方法 -->
@@ -47,7 +49,7 @@ HTML 用了 `state` 沒有的名稱、`data-on-click` 指向不存在的方法
47
49
  把這些貼進專案的 agent 指引檔(`AGENTS.md`、`CLAUDE.md` 等):
48
50
 
49
51
  - 元件的 `.html` 與 `.ts` 一起寫;不要編輯 `x.html.ts` 與 `ts-dom-tags.ts`,它們是 `ts-dom gen` 產生的。
50
- - 改了任何 `.html` 之後,執行 `npx ts-dom gen && npx tsc`,然後把型別回報的每個錯誤(`state` 少了某個名稱、少了處理方法)修好,不要用型別斷言蓋掉。
52
+ - 改了任何 `.html` 或元件之後,執行 `npx ts-dom check`(加 `--json` 以資料形式讀取結果)。它會重新產生產生檔、執行 `tsc`、把範本契約的錯誤對應回範本的行列,並跑 lint 規則。照每則的 `Fix:` 提示修好所有回報的問題,不要用型別斷言蓋掉型別錯誤。
51
53
  - 範本一律以 `./x.html.js` 匯入。
52
54
  - HTML 裡不放表達式:計算值放進 `derive`,state 只用 `this.set()` 改。
53
55
  - `ts-dom gen` 與 `ts-dom lint` 的每則診斷結尾都有 `Fix:` 提示,照做。`npx ts-dom rules` 列出所有規則編號,`npx ts-dom lint --a11y` 另外檢查無障礙。
@@ -62,7 +64,7 @@ npx ts-dom gen # 為每個 x.html 產生 x.html.ts(含型別);每次
62
64
  npx tsc # 型別檢查並編譯;不需要 Vite 或其他 bundler
63
65
  ```
64
66
 
65
- 下面三個範例都是 `examples/readme/` 裡真實可執行的檔案(有型別檢查、lint 與測試)。
67
+ 下面四個範例都是 `examples/readme/` 裡真實可執行的檔案(有型別檢查、lint 與測試)。
66
68
 
67
69
  ### 範例 1:計數器(state、衍生值、事件、data-if)
68
70
 
@@ -147,7 +149,7 @@ export class TodoList extends Component(tpl, {
147
149
  }
148
150
 
149
151
  // Automatic binding handles {{ remaining }}; the list is filled here.
150
- override render(view: { items: Item[] }, changed: ReadonlySet<string>): void {
152
+ override render(view: { items: readonly Item[] }, changed: ReadonlySet<string>): void {
151
153
  if (!changed.has("items")) return;
152
154
  this.lists.items.replace(
153
155
  view.items,
@@ -229,6 +231,97 @@ define("order-line", OrderLine);
229
231
 
230
232
  資料流只有一個方向:父層的 state → 子元件的 props(`this.refs.qty.value = ...`);子元件的請求 → 事件 → 父層的處理方法。
231
233
 
234
+ ### 範例 4:元件組成的清單(`items =`、props 往下、事件往上)
235
+
236
+ 項目有自己的東西(狀態、事件、內層清單)時,就把它做成元件,整份清單用一次指派填入。項目元件以 prop 接收自己的 task,用事件把變更往上送:
237
+
238
+ ```html
239
+ <!-- task-row.html -->
240
+ <label><input type="checkbox" data-ref="done" data-on-change="toggle" /> {{ title }}</label>
241
+ <button type="button" data-on-click="drop">Remove</button>
242
+ ```
243
+
244
+ ```ts
245
+ // task-row.ts
246
+ import { Component, define, event } from "@nativele/ts-dom";
247
+ import tpl from "./task-row.html.js";
248
+
249
+ export interface Task {
250
+ id: number;
251
+ title: string;
252
+ done: boolean;
253
+ }
254
+
255
+ /** One item of the list: it receives its task as a prop and asks the parent for changes through events. */
256
+ export class TaskRow extends Component(tpl, {
257
+ props: { task: null as Task | null },
258
+ derive: { title: (s) => s.task?.title ?? "" },
259
+ emits: { "task-toggle": event(), "task-remove": event() },
260
+ }) {
261
+ override render(view: { task: Task | null }): void {
262
+ const done = view.task?.done ?? false;
263
+ if (this.refs.done.checked !== done) this.refs.done.checked = done;
264
+ }
265
+ toggle(): void {
266
+ this.emit("task-toggle");
267
+ }
268
+ drop(): void {
269
+ this.emit("task-remove");
270
+ }
271
+ }
272
+ define("task-row", TaskRow);
273
+ ```
274
+
275
+ 父元件在 `<template data-each>` 上寫出識別項目的欄位(`data-key`)與接收項目的 prop(`data-prop`),在 `render` 裡指派整份清單:
276
+
277
+ ```html
278
+ <!-- task-board.html -->
279
+ <form data-on-submit="add">
280
+ <input data-ref="title" aria-label="New task" />
281
+ <button type="submit">Add</button>
282
+ </form>
283
+ <button type="button" data-on-click="sortByTitle">Sort by title</button>
284
+ <ul>
285
+ <template data-each="tasks" data-key="id" data-prop="task">
286
+ <task-row role="listitem" data-on-task-toggle="toggleTask" data-on-task-remove="removeTask"></task-row>
287
+ </template>
288
+ </ul>
289
+ ```
290
+
291
+ ```ts
292
+ // task-board.ts
293
+ import { Component, define } from "@nativele/ts-dom";
294
+ import type { EventHit } from "@nativele/ts-dom";
295
+ import type { Task } from "./task-row.js";
296
+ import tpl from "./task-board.html.js";
297
+
298
+ export class TaskBoard extends Component(tpl, { state: { tasks: [] as Task[], nextId: 1 } }) {
299
+ add(e: Event): void {
300
+ e.preventDefault();
301
+ const title = this.refs.title.value.trim();
302
+ if (!title) return;
303
+ this.set((s) => ({ tasks: [...s.tasks, { id: s.nextId, title, done: false }], nextId: s.nextId + 1 }));
304
+ this.refs.title.value = "";
305
+ }
306
+ // A changed task is a new object: the list sets the prop only on items that are not the same object as before.
307
+ toggleTask(_e: Event, hit: EventHit): void {
308
+ this.set((s) => ({ tasks: s.tasks.map((t) => (t.id === hit.key ? { ...t, done: !t.done } : t)) }));
309
+ }
310
+ removeTask(_e: Event, hit: EventHit): void {
311
+ this.set((s) => ({ tasks: s.tasks.filter((t) => t.id !== hit.key) }));
312
+ }
313
+ sortByTitle(): void {
314
+ this.set((s) => ({ tasks: [...s.tasks].sort((a, b) => a.title.localeCompare(b.title)) }));
315
+ }
316
+ override render(view: { tasks: readonly Task[] }, changed: ReadonlySet<string>): void {
317
+ if (changed.has("tasks")) this.lists.tasks.items = view.tasks;
318
+ }
319
+ }
320
+ define("task-board", TaskBoard);
321
+ ```
322
+
323
+ 新增、刪除、排序與切換都透過 `this.set()`;清單會保留還在的 task 的元素(所以核取方塊的焦點不會丟)、為新的 task 建立元素、移除消失的,並只對換成新物件的項目設定 `task`。`tsc` 會檢查指派的項目是否符合 `<task-row>` 的 `task` prop 並帶有 `id`。
324
+
232
325
  ## 設計原則
233
326
 
234
327
  - HTML 是唯一的結構描述來源。
@@ -289,6 +382,8 @@ npx tsc # 型別檢查並編譯到 out/
289
382
 
290
383
  開發時可用 `npx ts-dom gen --watch` 持續監看 `.html`,搭配 `tsc --watch`。
291
384
 
385
+ `npx ts-dom gen --check` 不寫任何檔案:它列出缺少、過期(範本或 `define()` 改過)或孤立(範本已刪除)的產生檔,有任何一個就以結束碼 1 結束(`--json` 輸出機器可讀格式)。專案若有提交產生檔,可以在 CI 使用;也可以在看型別錯誤之前,先確認產生檔是新的。
386
+
292
387
  在程式中以 `.js` 結尾匯入範本(標準 NodeNext 寫法,TS 會對應到產生的 `x.html.ts`):
293
388
 
294
389
  ```ts
@@ -341,6 +436,10 @@ lint: {
341
436
  },
342
437
  ```
343
438
 
439
+ 每個規則代號也各自是一條規則,所以專案可以逐條啟用、各自設定嚴重程度,例如用 `"ts-dom/unused-ref": "warn"` 或 `"ts-dom/missing-handler": "error"`,而不是整組 `ts-dom/unused-markup`。`ts-dom rules` 會列出所有代號。
440
+
441
+ **在編輯器裡。** 在 VS Code 安裝 Oxc 擴充(它執行 `oxlint --lsp`),並在 oxlint 設定裡保留這個外掛:輸入時問題就會顯示在範本的 import 那一行,`missing-handler` 會提供快速修正,把方法加進元件類別。命令列可用 `oxlint --fix-suggestions` 套用同樣的修正。修正只能改動正在檢查的檔案,所以出在 HTML 裡的問題(沒用到的 `data-ref`、壞掉的標記)只會回報、不會在那裡修正;標記結構與排版請交給 oxfmt 這類格式化工具。
442
+
344
443
  不用 oxlint 時,同樣的檢查可用 CLI:
345
444
 
346
445
  ```sh
@@ -349,6 +448,8 @@ npx ts-dom lint --a11y # 另外檢查 ARIA / 無障礙(警告)
349
448
  npx ts-dom lint --strict # 警告也使結束碼為 1(錯誤一定會)
350
449
  npx ts-dom lint --json # 輸出 JSON:{ "issues": [{ file, line, col, severity, rule, message }] }
351
450
  npx ts-dom rules # 列出所有規則代號與說明
451
+ npx ts-dom rules --explain=unused-ref # 規則存在的理由、錯誤範例與修正
452
+ npx ts-dom rules --json # 以資料列出所有規則:{ id, group, description, why, bad, good }
352
453
  ```
353
454
 
354
455
  每則訊息的格式都一樣,說明**哪裡錯、為什麼、怎麼修**,並附上 HTML 裡的行與欄:
@@ -360,7 +461,7 @@ a.html:3:1 warning [unknown-prop] <x-kid lable> is not a declared prop of <x-kid
360
461
 
361
462
  結束碼:0 = 沒有問題,1 = 有錯誤(或使用 `--strict` 時有警告)。oxlint 外掛的訊息以 `./x.html.js:行:欄 [規則代號] 訊息` 開頭,回報在 `import` 那一行。
362
463
 
363
- 建議的檢查流程:`ts-dom gen && tsc && ts-dom lint`。
464
+ 建議的檢查流程:`ts-dom check`(一次跑完 `gen`、`tsc` 與 `lint`,見「檢查」)。
364
465
 
365
466
  ## 型別如何產生
366
467
 
@@ -513,6 +614,8 @@ class Counter extends Component(tpl, {
513
614
  - `render` 內可以呼叫 `set`,會在當次結束後再處理一輪;連續 50 輪還沒穩定會丟出「疑似無限迴圈」錯誤。
514
615
  - 不使用 `state` 時,原本的 `refs`、`text`、`show`、`bind`、`createStore` 照常可用,完全向下相容。
515
616
 
617
+ **一路唯讀到底**:`state`、`props`、傳給 `render` 的 `view` 以及每個 `derive` 函式的參數,型別都是 `DeepReadonly`:裡面的陣列與物件都不能就地修改。`this.state.items[0].done = true` 或 `this.state.items.push(x)` 都是 `tsc` 錯誤,因為就地修改的物件保有原本的參考,以參考比較的清單與子元件 props 不會更新。請改成傳入新物件:`this.set((s) => ({ items: s.items.map((i) => (i.id === id ? { ...i, done: true } : i)) }))`。替 `render` 的 `view` 或接收 state 的輔助函式標註型別時,請用唯讀型別(`readonly Item[]`,或從 `@nativele/ts-dom` 匯入的 `DeepReadonly<State>`)。
618
+
516
619
  ## 事件:data-on-*
517
620
 
518
621
  HTML 只寫事件名稱與方法名稱,邏輯在 TS:
@@ -648,15 +751,24 @@ if (changed.has("value") && this.refs.input.value !== v.value) this.refs.input.v
648
751
 
649
752
  ## 效能
650
753
 
651
- 量測環境:真實 Chrome(WSL),每項只有一個文字洞,只計腳本執行時間(不含版面與繪製),單次量測,請當成數量級參考。
652
-
653
- | 清單大小 | 首次建立 | 資料完全相同 | 只改 1 項 | 反轉順序 |
654
- |---|---|---|---|---|
655
- | 1,000 | 約 11 ms | 約 0.6 ms | 約 0.4 ms | 約 1.3 ms |
656
- | 5,000 | 約 23–34 ms | 約 2 ms | 約 0.9 ms | 約 6–8 ms |
657
- | 10,000 | 約 65 ms | 約 9 ms | 約 2 ms | 約 19 ms |
658
-
659
- 「資料完全相同」是 `render` 每次都重跑整個清單的成本:文字節點值沒變就不寫,所以很便宜。一個畫面的預算約 16 ms,上萬項再每次按鍵都重跑就會吃緊,請用 `changed` 略過、`skipUnchanged`,或把清單拆小。事件委派不會隨項目數增加監聽器。
754
+ `npm run bench` 會在真實 Chrome 跑大型清單的基準測試並印出這張表。每個項目顯示一段文字;時間是 5 次的中位數,只計腳本執行時間(不含版面與繪製)。這是在一台機器(WSL 上的 Chrome)量測的結果,請看比例,不要當成絕對數值。「直接操作 DOM」是用原生 DOM 呼叫手寫的 keyed 清單,也就是任何函式庫成本的下限。
755
+
756
+ | 做法 | 項目數 | 建立 | 更新 10% | 反轉順序 | 刪除一半 | 追加 1,000 項 |
757
+ |---|---|---|---|---|---|---|
758
+ | 元件(`items =`) | 1,000 | 15.4 ms | 0.5 ms | 1.8 ms | 1.1 ms | 14.6 ms |
759
+ | 元件(`items =`) | 5,000 | 80.5 ms | 1.6 ms | 10 ms | 5.4 ms | 17.6 ms |
760
+ | 元件(`items =`) | 10,000 | 160.6 ms | 3.2 ms | 21 ms | 11.3 ms | 16 ms |
761
+ | 純元素(`replace()`) | 1,000 | 2.8 ms | 0.1 ms | 0.7 ms | 0.2 ms | 2.9 ms |
762
+ | 純元素(`replace()`) | 5,000 | 15.8 ms | 0.7 ms | 4.3 ms | 1.7 ms | 3.7 ms |
763
+ | 純元素(`replace()`) | 10,000 | 42 ms | 1.4 ms | 8.6 ms | 3.3 ms | 4.4 ms |
764
+ | 直接操作 DOM(基準線) | 1,000 | 0.8 ms | 0.2 ms | 1 ms | 0.3 ms | 0.8 ms |
765
+ | 直接操作 DOM(基準線) | 5,000 | 4 ms | 0.8 ms | 4.3 ms | 1.6 ms | 1.1 ms |
766
+ | 直接操作 DOM(基準線) | 10,000 | 8.7 ms | 1.7 ms | 10.9 ms | 3.9 ms | 1.6 ms |
767
+
768
+ - 元件項目的建立成本約每項 15 µs,因為每個項目都是有自己範本、refs 與生命週期的自訂元素;純元素約 3–4 µs,直接操作 DOM 約 1 µs。
769
+ - 項目建立之後,更新、重排與刪除都在直接操作 DOM 的兩倍左右以內:只會處理換成新物件的項目,保留的元素是搬動而不是重建。
770
+ - 一個畫面約 16 ms。幾千個元件項目的清單沒有問題;上萬項時請改用 `replace()` 的純元素、一次只顯示一頁或一段範圍,或只讓需要狀態與事件的列使用元件項目。
771
+ - 事件委派不會隨項目增加而多掛監聽器。
660
772
 
661
773
  ## 範例
662
774
 
@@ -697,7 +809,7 @@ function menuItem(label: string): HTMLLIElement {
697
809
 
698
810
  ## 清單
699
811
 
700
- 填清單有兩種方式。項目有自己的東西(狀態、事件、內層清單)就用元件;只是幾個純元素組成的小項目就用 `replace()`。
812
+ 填清單有兩種方式,用一條規則決定:**項目有自己的東西(狀態、事件、內層清單)就是元件,用 `items =` 填入;項目只是幾個文字與 `data-if` 就是純元素,用 `replace()` 填入。** 範例 4 示範前者,範例 2 示範後者。
701
813
 
702
814
  ### 項目是元件:`items = ...`
703
815
 
@@ -760,9 +872,16 @@ this.lists.tags.replace(
760
872
  - `data-each` 不能巢狀,要分成兩套模板:把內層清單放進自己的 `.html`(有自己 `data-each` 的子元件),在外層項目裡使用該元件的標籤。
761
873
  - `<slot>` 不能放在項目內。
762
874
 
875
+ ### 項目的搬動、連接與訂閱
876
+
877
+ - 瀏覽器支援 `moveBefore` 時,被搬動的項目保持連接:`disconnected()` 與 `connected()` 都不會執行,焦點、捲動位置與訂閱都不受影響。
878
+ - 不支援時,被搬動的項目會先移出再放回:`disconnected()` 執行並釋放所有用 `bind`、`on`、`onCleanup` 登記的東西,接著 `connected()` 再執行一次。元素本身、props 與 state 都不變。
879
+ - 所以訂閱要寫在 `connected()`(`this.bind(store, ...)`、`this.on(...)`),不要寫在建構子或欄位初始值:這樣在任何瀏覽器搬動之後都會重新訂閱。寫在建構子的訂閱,第一次搬動就被釋放,之後不會再回來。
880
+ - 每個項目都需要的資料(主題、目前使用者、登記表)放進 `createStore`,由項目元件在 `connected()` 裡 bind,不要複製進每個項目物件。
881
+
763
882
  ### 清單更新的順序
764
883
 
765
- `replace()` 與 `upsert()` 會先把新項目的元素放進文件,再呼叫 `update`,所以放在項目內的子元件會在父元件設定屬性之前就已經連接。每列自己的狀態請等子元件第一次拿到資料時再建立(例如在屬性的 setter 裡),`connected()` 裡的渲染要先確認需要的資料已經存在。
884
+ 指派 `items` 時,新項目的 `data-prop` 屬性會在元素放進文件之前就設定好,所以項目元件連接時已經有自己的項目,第一次渲染就會顯示出來。`replace()` 與 `upsert()` 則是先把新項目的元素放進文件,再呼叫 `update`,所以放在這種項目內的子元件會在父元件設定屬性之前就已經連接;這時每列自己的狀態請等子元件第一次拿到資料時再建立(例如在屬性的 setter 裡),`connected()` 裡的渲染要先確認需要的資料已經存在。
766
885
 
767
886
  ## Trusted Types、翻譯與圖示
768
887
 
@@ -778,8 +897,23 @@ this.lists.tags.replace(
778
897
 
779
898
  ## 檢查
780
899
 
900
+ 每個工具只檢查自己看得到的部分,彼此不重複:
901
+
902
+ | 工具 | 檢查什麼 |
903
+ |---|---|
904
+ | oxfmt 這類格式化工具(`oxfmt --check` 或 `vp fmt --check`) | 範本標記是否結構正確(例如 `<p><div></p></div>` 這種錯置的結束標籤,在那裡是語法錯誤;而本函式庫使用的 HTML 解析器會悄悄把它修正成另一棵樹),以及排版是否一致。它不認識 `{{ }}` 或 `data-*` 的名稱。使用時請以 `collapse` 空白模式產生,否則重新排版可能改變畫面上的空白。 |
905
+ | `ts-dom gen` | 範本規則:洞、`data-each`、`data-if`、`data-on-*`、禁止的標記。 |
906
+ | `tsc` | HTML 裡的每個名稱都存在於元件中且型別正確,清單、props 與事件都對得上。 |
907
+ | `ts-dom lint` / oxlint 外掛 | HTML 宣告了但類別從未使用的東西、元件之間的資料流、無障礙。 |
908
+ | `ts-dom doctor` | 其他工具所依賴的專案設定。 |
909
+ | `ts-dom check` | 一次跑完 `gen`、`tsc` 與 `lint`,並把範本契約的錯誤指向範本。 |
910
+
781
911
  四道檢查,互補。**每一則訊息都有穩定的規則代號,並說明哪裡錯、為什麼、怎麼修**;`npx ts-dom rules` 列出全部規則。
782
912
 
913
+ **懷疑是設定本身有問題時:`npx ts-dom doctor [dirs...]`。** 它檢查專案在任何檢查能運作之前需要的東西:tsconfig 要開 `strict`、含 DOM lib、模組解析能把 `./x.html.js` 對到產生檔、`include` 涵蓋範本資料夾;runtime 套件已安裝;範本以 `./x.html.js` 匯入(不是 `./x.html`);產生檔是否過期;Vite 外掛與 oxlint 外掛是否設定;以及設定了 oxfmt 或 Prettier 這類格式化工具時,範本是否以 `collapse` 空白模式產生。每則都有穩定的規則代號與 `Fix:` 提示,`--json` 以資料形式輸出。
914
+
915
+ **一個指令跑完全部:`npx ts-dom check [dirs...]`。** 它會回報範本診斷、重新產生產生檔(加 `--no-write` 則改為回報過期的產生檔)、執行專案自己的 `tsc`(TypeScript 5、6、7 皆可;`--project=` 指定 tsconfig),並跑 lint 規則(`--a11y` 加上無障礙檢查)。來自範本契約的 TypeScript 錯誤會指向範本,而不是 `Component(...)` 那一行:`{{ title }}` 找不到任何名為 `title` 的東西時,會以 `missing-binding` 回報在 `.html` 的行列;缺少的處理方法則是 `missing-handler`;項目元件的問題各有自己的規則代號;只是因為類別已經壞掉而連帶產生的錯誤會被略過。`--json` 以資料形式輸出,有錯誤時結束碼為 1(加 `--strict` 時警告也算)。
916
+
783
917
  1. **codegen 診斷**(`ts-dom gen` 與 Vite 外掛,帶行列號,有錯誤時中斷、不寫檔):重複的 `data-ref`、`data-each` 沒有單一根元素、`data-each` 不在 `<template>` 上、`data-each` 巢狀、`data-key` 與 `data-prop` 不完整或不合法、有 `data-key` 的清單其項目不是單一自訂元素、`data-if` 在項目根元素、`<slot>` 在項目內、`<style>` 不在第一層、不合法的洞、屬性內的洞、不合法的 `data-on-*`(含與 DOM 或 ts-dom 成員同名),範本內的 `<html>`、`<head>`、`<body>`,以及會執行程式碼或被 Trusted Types 擋下的標記:`<script>`、`on*` 屬性、`srcdoc`、`javascript:` 網址。
784
918
  2. **tsc**:TypeScript 使用了 HTML 沒有宣告的名稱就報錯(`this.refs.nope`、`this.show("nope", true)`);`state`、`props`、`derive`、`emits` 與 HTML 之間的所有約束(見「資料流」);子元件 prop 的名稱與型別;`define()` 檢查 `data-on-*` 的方法存在。
785
919
  3. **執行期**:`Component()` 與元件在定義、連接與使用時,對同樣的問題丟出帶 `[規則代號]` 與修正方式的錯誤。