@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/dist/index.d.mts CHANGED
@@ -176,7 +176,15 @@ export declare function event<T = void>(opts?: {
176
176
  type DetailOf<D> = D extends EventDecl<infer T> ? T : never;
177
177
  type EmitArgs<D> = [DetailOf<D>] extends [void] ? [detail?: undefined] : [detail: DetailOf<D>];
178
178
  /** The data passed to render(): state, props and derived values merged. */
179
- type View<D, R> = Readonly<D> & Readonly<R>;
179
+ /**
180
+ * Read-only all the way down: arrays, objects, Maps and Sets inside it cannot be changed through it.
181
+ * State, props and derived values are typed this way, so changing an object in place instead of passing a new one is a tsc error.
182
+ */
183
+ type DeepReadonly<T> = T extends ((...args: never[]) => unknown) ? T : T extends Node | Date | RegExp | Promise<unknown> ? T : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<K, DeepReadonly<V>> : T extends ReadonlySet<infer V> ? ReadonlySet<DeepReadonly<V>> : T extends readonly unknown[] ? { readonly [K in keyof T]: DeepReadonly<T[K]>; } : T extends object ? IsPlainObject<T> extends true ? { readonly [K in keyof T]: DeepReadonly<T[K]>; } : T : T;
184
+ type IsPlainObject<T> = { [K in keyof T]: T[K]; } extends T ? true : false;
185
+ /** Props as the element exposes them: the parent assigns them, but cannot change the objects inside through them. */
186
+ type PropAccessors<P> = { -readonly [K in keyof P]: DeepReadonly<P[K]>; };
187
+ type View<D, R> = DeepReadonly<D> & DeepReadonly<R>;
180
188
  interface DataOptions<St extends object, P extends object, R, E extends Record<string, EventDecl<unknown>>> extends ComponentOptions {
181
189
  /** Data owned by the component and changed only through set(). Fields named like a {{ hole }} or data-if in the HTML bind automatically. */
182
190
  state?: St;
@@ -190,7 +198,7 @@ interface DataOptions<St extends object, P extends object, R, E extends Record<s
190
198
  * Derived values: computed from state and props, usable directly by {{ holes }}, data-if and render().
191
199
  * Recomputed after every data change (there is no dependency tracking).
192
200
  */
193
- derive?: { [K in keyof R]: (s: Readonly<St & P>) => R[K]; };
201
+ derive?: { [K in keyof R]: (s: DeepReadonly<St & P>) => R[K]; };
194
202
  /** The events this component emits. */
195
203
  emits?: E;
196
204
  }
@@ -246,14 +254,14 @@ interface ComponentInstance<S extends Schema> extends HTMLElement {
246
254
  /** Members added when the `state` / `props` / `derive` / `emits` options are used. */
247
255
  interface StatefulInstance<S extends Schema, St extends object, P extends object, R, E extends Record<string, EventDecl<unknown>>> extends ComponentInstance<S> {
248
256
  /** The component's own data (read-only). Use set() to change it. */
249
- readonly state: Readonly<St>;
257
+ readonly state: DeepReadonly<St>;
250
258
  /** Data provided by the parent (read-only). Do not assign it inside the component; use emit() to notify the parent. */
251
- readonly props: Readonly<P>;
259
+ readonly props: DeepReadonly<P>;
252
260
  /**
253
261
  * The only way to change state: shallow-merges the patch, and does nothing when no field actually changed.
254
262
  * When something changed it synchronously updates the same-named {{ holes }} and data-if, then calls render(). It cannot change props.
255
263
  */
256
- set(patch: Partial<St> | ((s: Readonly<St>) => Partial<St>)): void;
264
+ set(patch: Partial<DeepReadonly<St>> | ((s: DeepReadonly<St>) => Partial<DeepReadonly<St>>)): void;
257
265
  /** Coalesce several set() calls and prop changes: the view is updated and render() is called once, after fn returns. */
258
266
  batch(fn: () => void): void;
259
267
  /**
@@ -284,7 +292,7 @@ export declare function Component<S extends Schema>(tpl: Template<S>, opts?: Com
284
292
  * `reservedPropNames` (a prop shares a name with HTMLElement or a ts-dom member), `nativeEventNames` (emits shares a name with a native event).
285
293
  * (The check lives in the return type, not in the parameter: in the parameter it prevents TypeScript from inferring the type of derive.)
286
294
  */
287
- export declare function Component<S extends Schema, St extends object = Record<never, never>, P extends object = Record<never, never>, R = Record<never, never>, E extends Record<string, EventDecl<unknown>> = Record<never, never>>(tpl: Template<S>, opts: DataOptions<St, P, R, E>): IsClean<Problems<S, St, P, R, E>> extends true ? new () => StatefulInstance<S, St, P, R, E> & P : Problems<S, St, P, R, E>;
295
+ export declare function Component<S extends Schema, St extends object = Record<never, never>, P extends object = Record<never, never>, R = Record<never, never>, E extends Record<string, EventDecl<unknown>> = Record<never, never>>(tpl: Template<S>, opts: DataOptions<St, P, R, E>): IsClean<Problems<S, St, P, R, E>> extends true ? new () => StatefulInstance<S, St, P, R, E> & PropAccessors<P> : Problems<S, St, P, R, E>;
288
296
  type CheckHandlers<I> = I extends {
289
297
  readonly "~events"?: infer E;
290
298
  } ? [Exclude<NonNullable<E>, keyof I>] extends [never] ? unknown : {
@@ -295,4 +303,4 @@ type CheckHandlers<I> = I extends {
295
303
  * component as a public method, otherwise tsc reports an error that lists the missing names (`missingHandlers`).
296
304
  */
297
305
  export declare function define<C extends abstract new (...args: never[]) => HTMLElement>(name: string, ctor: C & CheckHandlers<InstanceType<C>>): void;
298
- export type { ComponentInstance, ComponentOptions, DataOptions, El, EmitEvent, EventDecl, EventHit, ItemContext, ListItem, ListSchema, ReadableStore, ReplaceOptions, Schema, StatefulInstance, Store, Template, TemplateInstance, TextOf, TextValue, Update, View };
306
+ export type { ComponentInstance, ComponentOptions, DataOptions, DeepReadonly, El, EmitEvent, EventDecl, EventHit, ItemContext, ListItem, ListSchema, PropAccessors, ReadableStore, ReplaceOptions, Schema, StatefulInstance, Store, Template, TemplateInstance, TextOf, TextValue, Update, View };
package/dist/index.mjs CHANGED
@@ -228,7 +228,7 @@ var List = class {
228
228
  if (!this.binding) throw new Error("[list-not-bound] this list has no data-key and data-prop, so its items cannot be assigned. Fix: add them to the <template data-each>, e.g. data-key=\"id\" data-prop=\"item\", or call replace().");
229
229
  if (!Array.isArray(items)) throw new Error(`[invalid-items] items must be an array of objects, but got ${items === null ? "null" : typeof items}. Fix: assign an array, for example items = view.todos ?? [].`);
230
230
  if (items.length) this.#checkProp();
231
- this.replace(items, this.#keyOf, this.#assign, { skipUnchanged: true });
231
+ this.#reconcile(items, this.#keyOf, this.#assign, true, true);
232
232
  }
233
233
  #checkProp() {
234
234
  const prop = this.binding?.prop;
@@ -263,7 +263,7 @@ var List = class {
263
263
  return this.#items.get(key);
264
264
  }
265
265
  #create(key) {
266
- const el = this.proto.cloneNode(true);
266
+ const el = this.binding ? document.importNode(this.proto, true) : this.proto.cloneNode(true);
267
267
  const holder = document.createDocumentFragment();
268
268
  holder.append(el);
269
269
  const conds = mountConds(holder);
@@ -324,6 +324,10 @@ var List = class {
324
324
  }
325
325
  /** Keyed reconciliation: remove extra items, create missing ones, reorder, and call update() for every item. */
326
326
  replace(items, key, update, opts = {}) {
327
+ this.#reconcile(items, key, update, opts.skipUnchanged === true, false);
328
+ }
329
+ /** `updateFirst`: a new item is updated before it is inserted, so an item component is connected with its prop already set. */
330
+ #reconcile(items, key, update, skipUnchanged, updateFirst) {
327
331
  const next = /* @__PURE__ */ new Set();
328
332
  for (const it of items) {
329
333
  const k = key(it);
@@ -340,13 +344,14 @@ var List = class {
340
344
  if (!entry) {
341
345
  entry = this.#create(k);
342
346
  this.#items.set(k, entry);
347
+ if (updateFirst) this.#run(entry, k, it, update);
343
348
  }
344
349
  if (entry.el.nextSibling !== after || entry.el.parentNode !== parent) {
345
350
  if (entry.el.parentNode === parent && parent?.moveBefore) parent.moveBefore(entry.el, after);
346
351
  else parent?.insertBefore(entry.el, after);
347
352
  }
348
353
  after = entry.el;
349
- this.#run(entry, k, it, update, opts.skipUnchanged);
354
+ this.#run(entry, k, it, update, skipUnchanged);
350
355
  }
351
356
  }
352
357
  };
@@ -1,4 +1,4 @@
1
- import { a as analyze, d as listHtml, f as scanDefines } from "./a11y-Bn9pDvWV.mjs";
1
+ import { a as analyze, f as listHtml, h as scanDefines } from "./a11y-wWL9rie7.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { basename, dirname, relative } from "node:path";
4
4
  // src/lib/lint.ts
@@ -43,7 +43,8 @@ function componentClasses(code, htmlSpec) {
43
43
  const bodyEnd = matchClose(code, bodyStart, "{", "}");
44
44
  if (bodyEnd > 0) out.push({
45
45
  call: code.slice(call, callEnd + 1),
46
- body: code.slice(bodyStart, bodyEnd + 1)
46
+ body: code.slice(bodyStart, bodyEnd + 1),
47
+ end: bodyEnd
47
48
  });
48
49
  }
49
50
  return out;
@@ -4,6 +4,13 @@ interface ImportNode {
4
4
  value: string;
5
5
  };
6
6
  }
7
+ interface Fixer {
8
+ insertTextBeforeRange(range: [number, number], text: string): unknown;
9
+ }
10
+ interface Suggestion {
11
+ desc: string;
12
+ fix(fixer: Fixer): unknown;
13
+ }
7
14
  interface Context {
8
15
  options?: [{
9
16
  ignore?: string[];
@@ -15,6 +22,7 @@ interface Context {
15
22
  report(d: {
16
23
  message: string;
17
24
  node: unknown;
25
+ suggest?: Suggestion[];
18
26
  }): void;
19
27
  }
20
28
  declare const _default: {
@@ -22,7 +30,7 @@ declare const _default: {
22
30
  name: string;
23
31
  };
24
32
  rules: {
25
- /** The HTML declares markup that this file never uses. Always on when enabled; reported as errors. */
33
+ /** The HTML declares markup that this file never uses. Reported as errors. */
26
34
  "unused-markup": {
27
35
  meta: {
28
36
  type: "suggestion";
@@ -41,6 +49,7 @@ declare const _default: {
41
49
  };
42
50
  additionalProperties: boolean;
43
51
  }[];
52
+ hasSuggestions: boolean;
44
53
  };
45
54
  create(context: Context): {
46
55
  ImportDeclaration(node: ImportNode): void;
@@ -65,6 +74,7 @@ declare const _default: {
65
74
  };
66
75
  additionalProperties: boolean;
67
76
  }[];
77
+ hasSuggestions: boolean;
68
78
  };
69
79
  create(context: Context): {
70
80
  ImportDeclaration(node: ImportNode): void;
@@ -89,6 +99,7 @@ declare const _default: {
89
99
  };
90
100
  additionalProperties: boolean;
91
101
  }[];
102
+ hasSuggestions: boolean;
92
103
  };
93
104
  create(context: Context): {
94
105
  ImportDeclaration(node: ImportNode): void;
@@ -1,5 +1,5 @@
1
- import { n as checkA11y } from "./a11y-Bn9pDvWV.mjs";
2
- import { a as dataFlow, i as contractsFor, l as templateCode, r as componentClasses, u as unusedMarkup } from "./lint-CqWk4U5P.mjs";
1
+ import { n as checkA11y, t as A11Y_RULES } from "./a11y-wWL9rie7.mjs";
2
+ import { a as dataFlow, i as contractsFor, l as templateCode, n as UNUSED_MARKUP_RULES, r as componentClasses, t as DATA_FLOW_RULES, u as unusedMarkup } from "./lint-CP_i4VPA.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
  // src/lib/oxlint-plugin.ts
@@ -14,46 +14,86 @@ const ignoreSchema = [{
14
14
  /** Child component contracts, scanned once per process from the working directory. */
15
15
  let contracts = null;
16
16
  const getContracts = () => contracts ??= contractsFor([process.cwd()]);
17
+ const cache = /* @__PURE__ */ new Map();
18
+ function groupDiagnostics(group, html, code, spec, filename) {
19
+ const key = `${group}\0${filename}\0${spec}`;
20
+ const hit = cache.get(key);
21
+ if (hit && hit.code === code && hit.html === html) return hit.diags;
22
+ const diags = group === "unused-markup" ? unusedMarkup(html, templateCode(code, spec)) : group === "a11y" ? checkA11y(html) : dataFlow(html, componentClasses(code, spec), [], getContracts());
23
+ cache.set(key, {
24
+ code,
25
+ html,
26
+ diags
27
+ });
28
+ return diags;
29
+ }
30
+ function readTemplate(filename, spec) {
31
+ try {
32
+ return readFileSync(resolve(dirname(filename), spec.slice(0, -3)), "utf8");
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ /** A missing handler can be added to the class that uses the template: offered as a suggestion, since the method body is the developer's to write. */
38
+ function suggestionsFor(d, code, spec) {
39
+ if (d.rule !== "missing-handler") return void 0;
40
+ const name = /="([^"]+)"/.exec(d.message)?.[1];
41
+ const target = componentClasses(code, spec)[0];
42
+ if (!name || !target) return void 0;
43
+ return [{
44
+ desc: `Add a method ${name}() to the component class`,
45
+ fix: (fixer) => fixer.insertTextBeforeRange([target.end, target.end], `\n ${name}(_e: Event): void {}\n`)
46
+ }];
47
+ }
17
48
  /**
18
- * Build an oxlint rule that runs `check` on every `import x from "./name.html.js"` and reports the findings
49
+ * Build an oxlint rule that checks every `import x from "./name.html.js"` and reports the findings
19
50
  * on the import line (an oxlint plugin can only report inside the file being linted).
20
51
  * The message starts with the HTML location, the rule id and what to do: `./x.html.js:3:5 [unused-ref] ...`.
52
+ * `only` limits the rule to one rule id of the group; without it the rule reports the whole group, minus `ignore`.
21
53
  */
22
- function templateRule(description, check) {
54
+ function templateRule(group, description, only) {
23
55
  return {
24
56
  meta: {
25
57
  type: "suggestion",
26
58
  docs: { description },
27
- schema: ignoreSchema
59
+ schema: only ? [] : ignoreSchema,
60
+ hasSuggestions: true
28
61
  },
29
62
  create(context) {
30
63
  return { ImportDeclaration(node) {
31
64
  const spec = node.source.value;
32
65
  if (!spec.endsWith(".html.js")) return;
33
- let html;
34
- try {
35
- html = readFileSync(resolve(dirname(context.filename), spec.slice(0, -3)), "utf8");
36
- } catch {
37
- return;
66
+ const html = readTemplate(context.filename, spec);
67
+ if (html === null) return;
68
+ const code = context.sourceCode.text;
69
+ const ignore = context.options?.[0]?.ignore ?? [];
70
+ for (const d of groupDiagnostics(group, html, code, spec, context.filename)) {
71
+ if (only ? d.rule !== only : ignore.includes(d.rule)) continue;
72
+ const suggest = suggestionsFor(d, code, spec);
73
+ context.report({
74
+ message: `${spec}:${d.line}:${d.col} [${d.rule}] ${d.message}`,
75
+ node,
76
+ ...suggest ? { suggest } : {}
77
+ });
38
78
  }
39
- for (const d of check(html, context, spec)) context.report({
40
- message: `${spec}:${d.line}:${d.col} [${d.rule}] ${d.message}`,
41
- node
42
- });
43
79
  } };
44
80
  }
45
81
  };
46
82
  }
47
- const ignored = (c) => c.options?.[0]?.ignore ?? [];
83
+ const perRule = (group, rules) => Object.fromEntries(Object.entries(rules).map(([id, description]) => [id, templateRule(group, description, id)]));
84
+ const { "no-importer": _noImporter, ...unusedMarkupRules } = UNUSED_MARKUP_RULES;
48
85
  var oxlint_plugin_default = {
49
86
  meta: { name: "ts-dom" },
50
87
  rules: {
51
- /** The HTML declares markup that this file never uses. Always on when enabled; reported as errors. */
52
- "unused-markup": templateRule("Markup declared in the HTML (data-ref, data-if, data-each, {{ }}, data-on-*) must be used by the component class", (html, c, spec) => unusedMarkup(html, templateCode(c.sourceCode.text, spec)).filter((d) => !ignored(c).includes(d.rule))),
88
+ /** The HTML declares markup that this file never uses. Reported as errors. */
89
+ "unused-markup": templateRule("unused-markup", "Markup declared in the HTML (data-ref, data-if, data-each, {{ }}, data-on-*) must be used by the component class"),
53
90
  /** ARIA and basic accessibility problems in the template. Off by default. */
54
- a11y: templateRule("Check the template for ARIA and basic accessibility problems", (html, c) => checkA11y(html, ignored(c))),
91
+ a11y: templateRule("a11y", "Check the template for ARIA and basic accessibility problems"),
55
92
  /** Data flow suggestions and child component contract checks. Off by default. */
56
- "data-flow": templateRule("Keep props flowing down and events flowing up one level at a time, and check child component attributes and events", (html, c, spec) => dataFlow(html, componentClasses(c.sourceCode.text, spec), ignored(c), getContracts()))
93
+ "data-flow": templateRule("data-flow", "Keep props flowing down and events flowing up one level at a time, and check child component attributes and events"),
94
+ ...perRule("unused-markup", unusedMarkupRules),
95
+ ...perRule("data-flow", DATA_FLOW_RULES),
96
+ ...perRule("a11y", Object.fromEntries(Object.entries(A11Y_RULES).map(([k, v]) => [k, v.split(". Fix:")[0]])))
57
97
  }
58
98
  };
59
99
  export { oxlint_plugin_default as default };
@@ -1,4 +1,4 @@
1
- import { c as generateFile, d as listHtml, f as scanDefines, i as CodegenError, l as isFullPage, n as checkA11y, o as formatDiagnostics, s as generateAll, u as isSelected } from "./a11y-Bn9pDvWV.mjs";
1
+ import { c as generateAll, d as isSelected, f as listHtml, h as scanDefines, i as CodegenError, l as generateFile, n as checkA11y, s as formatDiagnostics, u as isFullPage } from "./a11y-wWL9rie7.mjs";
2
2
  import { existsSync, readFileSync, unlinkSync } from "node:fs";
3
3
  import { resolve, sep } from "node:path";
4
4
  // src/lib/vite-plugin.ts
package/llms.txt CHANGED
@@ -1,8 +1,8 @@
1
1
  # Nativele
2
2
 
3
- > A TypeScript-driven, HTML-first UI library. Bind HTML templates to TypeScript custom elements. HTML describes structure and where data goes; TypeScript owns state and behavior; the DOM is the only view. Fully typed by tsc, no virtual DOM, no bundler required.
3
+ > A TypeScript-driven, HTML-first UI library built for type safety and AI agents. Bind HTML templates to TypeScript custom elements. HTML describes structure and where data goes; TypeScript owns state and behavior; the DOM is the only view. Fully typed by tsc, no virtual DOM, no bundler required.
4
4
 
5
- Workflow: write `x.html` + `x.ts`, run `npx ts-dom gen` (writes `x.html.ts`), then `npx tsc`. Import a template as `import tpl from "./x.html.js"` (note the `.js`). Lint with `npx ts-dom lint` (add `--a11y`). `npx ts-dom rules` lists every rule id. Every diagnostic is `file:line:col severity [rule-id] message` and ends with a `Fix:` hint: read the hint and apply it.
5
+ Workflow: write `x.html` + `x.ts`, run `npx ts-dom check` (regenerates `x.html.ts`, runs tsc and lint, points errors at the template). Import a template as `import tpl from "./x.html.js"` (note the `.js`). Lint with `npx ts-dom lint` (add `--a11y`). `npx ts-dom rules` lists every rule id. Every diagnostic is `file:line:col severity [rule-id] message` and ends with a `Fix:` hint: read the hint and apply it.
6
6
 
7
7
  ## Template syntax (HTML only names things; no expressions)
8
8
  - `{{ name }}`: text hole, a single identifier only. NOT `{{ a.b }}` / `{{ a + 1 }}`. Not allowed in attributes.
@@ -38,6 +38,7 @@ define("my-counter", MyCounter);
38
38
  Rules the compiler enforces: every `{{ hole }}` / `data-if` name must exist in state, props or derive (error `missingStateKeys`); state/props/derive names must be unique (`conflictingKeys`); a prop must not be named like an HTMLElement/ts-dom member such as `title`, `hidden`, `render` (`reservedPropNames`); emitted event names must not be native events (`nativeEventNames`); every `data-on-*` method must exist as a public method (`missingHandlers`, reported by `define()`).
39
39
 
40
40
  ## Data flow (one direction)
41
+ - `state`, `props`, `render`'s `view` and `derive` arguments are `DeepReadonly`: changing an object or array inside them is a tsc error. Replace with new objects (`s.items.map((i) => (i.id === id ? { ...i, done: true } : i))`). Annotate with read-only types (`readonly Item[]`, `DeepReadonly<State>`).
41
42
  - State: changed only by its owner with `this.set(patch | s => patch)`; no change => no render. `this.batch(fn)` merges several updates. Rendering is synchronous.
42
43
  - Props go DOWN: the parent sets them with an attribute or a property (`this.refs.child.value = v.x`, typed). A component never assigns its own props; to ask for a change it `emit`s an event.
43
44
  - Events go UP: `emit` does not bubble; the parent handles it with `data-on-<event>` on the child tag, casting with `EmitEvent<Child, "event-name">`.
@@ -68,12 +69,19 @@ define("my-counter", MyCounter);
68
69
  `const view = tpl.instantiate()` returns `{ root: DocumentFragment, rootElement, refs, lists, text, show }` typed like a component (`rootElement` exists in the type only when the template has exactly one top-level element, `<style>` excluded), for pieces created in code. No host: `data-on-*` is inert (use `refs.x.addEventListener`) and `<slot>` is not projected.
69
70
 
70
71
  ## Lists
72
+ - Rule: an item with anything of its own (state, events, a nested list) is a component filled with `items =`; an item of a few texts and `data-if` is plain elements filled with `replace()`.
73
+ - Subscribe in `connected()`, never in the constructor: without `moveBefore` a moved item is disconnected (its `bind`/`on`/`onCleanup` released) and connected again. Data every item needs goes in a `createStore` the item binds.
74
+ - Full example (add, remove, sort, toggle, child emit): `examples/readme/task-board.ts` and `task-row.ts` in the repository.
71
75
  - Items that are components: `<template data-each="todos" data-key="id" data-prop="todo"><todo-row></todo-row></template>`, then `this.lists.todos.items = view.todos` (inside `render`, guarded by `changed.has("todos")`). Keeps the element of each key, creates and removes the rest, sets the `todo` prop of each changed item; `hit.key` in handlers. The item component declares `todo` in its `props`; `tsc` checks the items against that prop type and the `data-key` field; an item component defined in a scanned folder must be exported and listed in `ts-dom-tags.ts` (run `ts-dom gen`), otherwise the assignment is a type error that names the problem.
72
76
  - Items that are plain elements: `<template data-each="x">` with `this.lists.x.replace(items, keyFn, (item, { text, show }) => ...)`.
73
77
  - `data-each` is never nested: a list inside an item is the item component's own `data-each`.
74
78
 
75
79
  ## Rules for agents
76
- - Never edit `x.html.ts` or `ts-dom-tags.ts` (generated). After any `.html` change run `npx ts-dom gen && npx tsc` and fix every type error instead of casting it away. Until `gen` has run, `tsc` only reports that `./x.html.js` is missing.
80
+ - `npx ts-dom rules --explain=<id>` (or `rules --json`) explains any rule id from a diagnostic: why it exists, a wrong example and the fix.
81
+ - Markup well-formedness is a formatter's job: `oxfmt --check` reports misnested tags that the library's parser would silently repair. Generate with `--whitespace=collapse` when a formatter is used.
82
+ - If checks behave oddly (no types, a template not found), run `npx ts-dom doctor --json` first: it reports tsconfig, import path, runtime, generated file and plugin problems with a `Fix:` each.
83
+ - `npx ts-dom gen --check` (writes nothing, exit 1) tells whether the generated files are stale before you trust `tsc` errors.
84
+ - Never edit `x.html.ts` or `ts-dom-tags.ts` (generated). After any `.html` or component change run `npx ts-dom check --json`: it regenerates, runs tsc, maps template contract errors to the template line/col (`missing-binding`, `missing-handler`, item rules), and runs lint. Fix every issue by its `Fix:` hint; never cast a type error away.
77
85
  - No expressions in HTML; computed values go in `derive`. Change state only with `this.set()`. Lists, attributes and focus go in `render(view, changed)`; check `changed.has("items")` before rebuilding a list.
78
86
  - Props go down (parent sets them), events go up (`emit`, handled with `data-on-<event>` on the child tag). `data-each` cannot be nested: make the inner list its own component.
79
87
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nativele/ts-dom",
3
- "version": "0.4.0",
4
- "description": "A TypeScript-driven, HTML-first UI library. Bind HTML to TypeScript: HTML describes the structure, TypeScript owns behavior and state, and the DOM is the single source of truth. Fully typed, no virtual DOM, no bundler required.",
3
+ "version": "0.4.6",
4
+ "description": "A TypeScript-driven, HTML-first UI library built for type safety and AI agents: HTML describes the structure, TypeScript owns behavior and state, the native DOM renders it. No virtual DOM, no template expressions, no bundler required.",
5
5
  "keywords": [
6
6
  "html",
7
7
  "typescript",
@@ -68,7 +68,8 @@
68
68
  "test:browser": "vp test run browser/",
69
69
  "verify:pack": "vp pack --publint --attw",
70
70
  "smoke": "bash scripts/consumer-smoke.sh",
71
- "smoke:no-bundler": "bash scripts/no-bundler-smoke.sh"
71
+ "smoke:no-bundler": "bash scripts/no-bundler-smoke.sh",
72
+ "bench": "node scripts/bench.ts"
72
73
  },
73
74
  "dependencies": {
74
75
  "parse5": "^8.1.0"