@nativele/ts-dom 0.2.1 → 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/CHANGELOG.md CHANGED
@@ -10,6 +10,12 @@ 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.3.0
14
+
15
+ - `ts-dom gen` rejects `<html>`, `<head>` and `<body>` inside a template (rule `document-tag`) instead of silently dropping them; a file that starts with `<!doctype html>` or `<html>` is still a page entry whatever its name.
16
+ - `tpl.instantiate().rootElement` is the template's only top-level element, typed by its tag; the type has it only for a template with exactly one top-level element (generated `x.html.ts` files carry a `rootElement` field, so regenerate them with `ts-dom gen`).
17
+ - Non-bubbling events such as media events (`canplay`, `loadeddata`, ...), `close`, `cancel` and `invalid` now reach `data-on-*` handlers; they were listened for in the bubble phase and never fired.
18
+
13
19
  ## 0.2.1
14
20
 
15
21
  - Releases are published through npm Trusted Publishing: the release workflow stages the version without an npm token, and a maintainer approves it on npm. No library code changes.
@@ -37,7 +43,6 @@ First release.
37
43
  - `whitespace: "collapse"` (CLI `--whitespace=collapse`, Vite plugin option) collapses whitespace runs and drops line-break-only text between tags; the default keeps text as written.
38
44
  - `replace()` throws `[duplicate-key]` for two items with the same key and leaves the list untouched. `EventHit<Item>` types `hit.item` when the handler annotates it. `bind` accepts any source with `get` and `subscribe` (`ReadableStore`).
39
45
  - `ts-dom gen --include=<glob> --exclude=<glob>` (and the Vite plugin options) generate only the selected templates, for migrating a project in steps.
40
-
41
46
  ### Composition
42
47
 
43
48
  - Child components used in a template are imported automatically by the generated `x.html.ts`; HTML is the only source of dependencies.
package/README.md CHANGED
@@ -352,7 +352,7 @@ export default template<{
352
352
  }>([["h2", {}, ["Hello, {{ name }}!"]], /* ... */]);
353
353
  ```
354
354
 
355
- Whole-page documents (starting with `<!doctype>` or `<html>`, such as `index.html`) are page entry points, not templates, and are not processed.
355
+ Whole-page documents are page entry points, not templates, and are not processed. A file is a page when its content starts with `<!doctype html>` or `<html>` (comments before it are fine); the file name does not matter, so `index.html`, `main.html` and `login.html` all work. A template is a fragment of a component, so `<html>`, `<head>` and `<body>` are rejected in it (rule `document-tag`): the browser would drop them from the fragment and whatever you put on them would have no effect. To keep a file out of generation in another way, use `--exclude`.
356
356
 
357
357
  ## HTML Syntax
358
358
 
@@ -656,14 +656,16 @@ const off = store.subscribe((s) => console.log(s)); // returns an unsubscribe fu
656
656
  ```ts
657
657
  import itemTpl from "./menu-item.html.js";
658
658
 
659
- function menuItem(label: string): HTMLElement {
659
+ function menuItem(label: string): HTMLLIElement {
660
660
  const view = itemTpl.instantiate();
661
661
  view.text.label = label;
662
662
  view.refs.button.addEventListener("click", () => select(label));
663
- return view.root.firstElementChild as HTMLElement;
663
+ return view.rootElement; // typed by the template's top-level tag
664
664
  }
665
665
  ```
666
666
 
667
+ `rootElement` is the template's only top-level element (a top-level `<style>` does not count); it exists in the type only when the template has exactly one, so a template that later gains a second root element makes `tsc` report the use.
668
+
667
669
  There is no host element, so `data-on-*` attributes do nothing here (listen on `refs`) and `<slot>` is not projected; use a component when you need those.
668
670
 
669
671
  ## Lists
@@ -704,6 +706,10 @@ Notes:
704
706
  - `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.
705
707
  - `<slot>` cannot be placed inside an item.
706
708
 
709
+ ### Order of list updates
710
+
711
+ `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.
712
+
707
713
  ## Trusted Types, Translations and Icons
708
714
 
709
715
  - Trusted Types: pages that enforce `require-trusted-types-for 'script'` work with no policy, because `ts-dom gen` writes the markup as data and the runtime builds it with DOM calls. Your own code must avoid `innerHTML` too.
@@ -720,7 +726,7 @@ Notes:
720
726
 
721
727
  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.
722
728
 
723
- 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`, `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), and markup that runs code or Trusted Types blocks: `<script>`, `on*` attributes, `srcdoc`, and `javascript:` URLs.
729
+ 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`, `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.
724
730
  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.
725
731
  3. **Runtime**: `Component()` and components, at definition, connection, and use, throw errors that carry a `[rule id]` and a fix for the same problems.
726
732
  4. **lint** (`ts-dom/unused-markup`, `ts-dom/data-flow`, `ts-dom/a11y`): markup declared in the HTML but never used (errors, with the HTML location), data flow suggestions and child component contracts (warnings), ARIA (warnings).
package/README.zh-TW.md CHANGED
@@ -354,7 +354,7 @@ export default template<{
354
354
  }>([["h2", {}, ["Hello, {{ name }}!"]], /* ... */]);
355
355
  ```
356
356
 
357
- 整頁文件(以 `<!doctype>` 或 `<html>` 開頭,例如 `index.html`)是頁面入口,不是範本,不會被處理。
357
+ 整頁文件是頁面入口,不是範本,不會被處理。判斷依據是檔案內容:以 `<!doctype html>` 或 `<html>` 開頭(前面可以有註解)就是頁面,與檔名無關,所以 `index.html`、`main.html`、`login.html` 都可以。範本是元件的片段,因此其中的 `<html>`、`<head>`、`<body>` 會被拒絕(規則 `document-tag`):瀏覽器會把它們從片段中丟掉,放在上面的任何東西都不會有效果。想用別的方式讓檔案不被處理,可以用 `--exclude`。
358
358
 
359
359
  ## HTML 語法
360
360
 
@@ -658,14 +658,16 @@ const off = store.subscribe((s) => console.log(s)); // 回傳取消函式
658
658
  ```ts
659
659
  import itemTpl from "./menu-item.html.js";
660
660
 
661
- function menuItem(label: string): HTMLElement {
661
+ function menuItem(label: string): HTMLLIElement {
662
662
  const view = itemTpl.instantiate();
663
663
  view.text.label = label;
664
664
  view.refs.button.addEventListener("click", () => select(label));
665
- return view.root.firstElementChild as HTMLElement;
665
+ return view.rootElement; // 型別由範本第一層的標籤決定
666
666
  }
667
667
  ```
668
668
 
669
+ `rootElement` 是範本第一層唯一的元素(第一層的 `<style>` 不算);只有範本剛好一個根元素時型別上才有它,所以範本日後多出第二個根元素時,`tsc` 會在使用處報錯。
670
+
669
671
  這種用法沒有宿主元素,所以 `data-on-*` 不會生效(請在 `refs` 上自行監聽),`<slot>` 也不會投影;需要這些時請用元件。
670
672
 
671
673
  ## 清單
@@ -706,6 +708,10 @@ this.lists.tags.replace(
706
708
  - `data-each` 不能巢狀,要分成兩套模板:把內層清單放進自己的 `.html`(有自己 `data-each` 的子元件),在外層項目裡使用該元件的標籤。
707
709
  - `<slot>` 不能放在項目內。
708
710
 
711
+ ### 清單更新的順序
712
+
713
+ `replace()` 與 `upsert()` 會先把新項目的元素放進文件,再呼叫 `update`,所以放在項目內的子元件會在父元件設定屬性之前就已經連接。每列自己的狀態請等子元件第一次拿到資料時再建立(例如在屬性的 setter 裡),`connected()` 裡的渲染要先確認需要的資料已經存在。
714
+
709
715
  ## Trusted Types、翻譯與圖示
710
716
 
711
717
  - Trusted Types:啟用 `require-trusted-types-for 'script'` 的頁面不需要設定任何 policy 就能用,因為 `ts-dom gen` 把標記寫成資料,執行期只用 DOM 呼叫建立。你自己的程式碼也要避免 `innerHTML`。
@@ -722,7 +728,7 @@ this.lists.tags.replace(
722
728
 
723
729
  四道檢查,互補。**每一則訊息都有穩定的規則代號,並說明哪裡錯、為什麼、怎麼修**;`npx ts-dom rules` 列出全部規則。
724
730
 
725
- 1. **codegen 診斷**(`ts-dom gen` 與 Vite 外掛,帶行列號,有錯誤時中斷、不寫檔):重複的 `data-ref`、`data-each` 沒有單一根元素、`data-each` 不在 `<template>` 上、`data-each` 巢狀、`data-if` 在項目根元素、`<slot>` 在項目內、`<style>` 不在第一層、不合法的洞、屬性內的洞、不合法的 `data-on-*`(含與 DOM 或 ts-dom 成員同名),以及會執行程式碼或被 Trusted Types 擋下的標記:`<script>`、`on*` 屬性、`srcdoc`、`javascript:` 網址。
731
+ 1. **codegen 診斷**(`ts-dom gen` 與 Vite 外掛,帶行列號,有錯誤時中斷、不寫檔):重複的 `data-ref`、`data-each` 沒有單一根元素、`data-each` 不在 `<template>` 上、`data-each` 巢狀、`data-if` 在項目根元素、`<slot>` 在項目內、`<style>` 不在第一層、不合法的洞、屬性內的洞、不合法的 `data-on-*`(含與 DOM 或 ts-dom 成員同名),範本內的 `<html>`、`<head>`、`<body>`,以及會執行程式碼或被 Trusted Types 擋下的標記:`<script>`、`on*` 屬性、`srcdoc`、`javascript:` 網址。
726
732
  2. **tsc**:TypeScript 使用了 HTML 沒有宣告的名稱就報錯(`this.refs.nope`、`this.show("nope", true)`);`state`、`props`、`derive`、`emits` 與 HTML 之間的所有約束(見「資料流」);子元件 prop 的名稱與型別;`define()` 檢查 `data-on-*` 的方法存在。
727
733
  3. **執行期**:`Component()` 與元件在定義、連接與使用時,對同樣的問題丟出帶 `[規則代號]` 與修正方式的錯誤。
728
734
  4. **lint**(`ts-dom/unused-markup`、`ts-dom/data-flow`、`ts-dom/a11y`):HTML 宣告了卻沒被使用的標記(錯誤,附 HTML 位置)、資料流建議與子元件契約(警告)、ARIA(警告)。
@@ -22,6 +22,7 @@ const CODEGEN_RULES = {
22
22
  "nested-each": "data-each templates cannot be nested",
23
23
  "duplicate-each": "data-each names must be unique",
24
24
  "each-root-count": "a data-each template needs exactly one root element",
25
+ "document-tag": "<html>, <head> and <body> cannot be in a template (the browser drops them from fragments)",
25
26
  "forbidden-tag": "<script> cannot be in a template (it would run code)",
26
27
  "forbidden-attribute": "on* event attributes and srcdoc cannot be in a template (they run code or are blocked by Trusted Types)",
27
28
  "unsafe-url": "href / src / action / formaction cannot start with javascript:",
@@ -66,6 +67,17 @@ function analyze(html) {
66
67
  ...locOf(n)
67
68
  });
68
69
  };
70
+ const blanked = html.replace(/<!--[\s\S]*?-->/g, (c) => c.replace(/[^\n]/g, " "));
71
+ for (const m of blanked.matchAll(/<(html|head|body)(?=[\s/>])/gi)) {
72
+ const before = blanked.slice(0, m.index);
73
+ a.diagnostics.push({
74
+ rule: "document-tag",
75
+ severity: "error",
76
+ message: `<${m[1]?.toLowerCase()}> cannot be in a template: a template is a fragment of a component, and the browser drops <html>, <head> and <body> from fragments, so nothing you put on them has any effect. Fix: remove the tag and keep its content. If this file is a whole page, start it with <!doctype html> and it is treated as a page entry and skipped (or leave it out with --exclude).`,
77
+ line: before.split("\n").length,
78
+ col: m.index - before.lastIndexOf("\n")
79
+ });
80
+ }
69
81
  const mark = (key, n) => {
70
82
  if (!a.locs.has(key)) a.locs.set(key, locOf(n));
71
83
  };
@@ -176,6 +188,15 @@ const union = (xs) => {
176
188
  return arr.length ? arr.map(q).join(" | ") : "never";
177
189
  };
178
190
  const obj = (m) => `{ ${[...m].map(([n, t]) => `${q(n)}: ${t}`).join("; ")} }`;
191
+ /** The type of the template's only top-level element (a `<style>` does not count); `never` when there is not exactly one, or other text sits beside it. */
192
+ function rootElementType(html) {
193
+ const tops = parseFragment(html).childNodes.filter((n) => n.nodeName !== "#comment" && !(n.nodeName === "#text" && !n.value.trim()));
194
+ const elements = tops.filter((n) => isEl$1(n) && !(n.tagName === "style" && n.namespaceURI === HTML_NS) && !(n.tagName === "template" && n.attrs.some((a) => a.name === "data-each")));
195
+ const others = tops.filter((n) => !(isEl$1(n) && (n.tagName === "style" || n.tagName === "template")));
196
+ if (elements.length !== 1 || others.length !== 1) return "never";
197
+ const el = elements[0];
198
+ return el.namespaceURI === HTML_NS ? `El<${JSON.stringify(el.tagName)}>` : "SVGElement";
199
+ }
179
200
  function globToRegExp(glob) {
180
201
  let re = "";
181
202
  for (let i = 0; i < glob.length; i++) {
@@ -240,6 +261,7 @@ function generateModule(html, options = {}, imports = []) {
240
261
  lists: { ${lists} };
241
262
  ifs: ${union(a.scope.ifs)};
242
263
  slots: ${union(a.slots)};
264
+ rootElement: ${rootElementType(html)};
243
265
  text: ${union(a.scope.text)};
244
266
  events: ${union(a.events)};
245
267
  }`;
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { d as listHtml, i as CodegenError, n as checkA11y, p as watchAll, r as CODEGEN_RULES, s as generateAll, t as A11Y_RULES } from "./a11y-DZvViTVA.mjs";
3
- import { c as lintUsage, n as UNUSED_MARKUP_RULES, o as formatIssue, s as lintDataFlow, t as DATA_FLOW_RULES } from "./lint-B7SIRo0c.mjs";
2
+ import { d as listHtml, i as CodegenError, n as checkA11y, p as watchAll, r as CODEGEN_RULES, s as generateAll, t as A11Y_RULES } from "./a11y-CeZcA10z.mjs";
3
+ import { c as lintUsage, n as UNUSED_MARKUP_RULES, o as formatIssue, s as lintDataFlow, t as DATA_FLOW_RULES } from "./lint-DS-IOjkf.mjs";
4
4
  import { readFileSync, readdirSync, statSync } from "node:fs";
5
5
  import { join, relative } from "node:path";
6
6
  // src/cli.ts
package/dist/index.d.mts CHANGED
@@ -68,6 +68,8 @@ interface Schema {
68
68
  events: string;
69
69
  ifs: string;
70
70
  slots: string;
71
+ /** The template's only top-level element (a `<style>` does not count), or `never` when there is not exactly one. */
72
+ rootElement: Element;
71
73
  }
72
74
  /**
73
75
  * A template's parsed markup, written by codegen: a string is a text node, `["#comment", text]` a comment,
@@ -75,7 +77,10 @@ interface Schema {
75
77
  */
76
78
  type TemplateNode = string | readonly [tag: string, attrs: Readonly<Record<string, string>>, children: readonly TemplateNode[], ns?: string] | readonly ["#comment", string];
77
79
  /** A template built without a component: the nodes, and the typed handles to the places the template names. */
78
- interface TemplateInstance<S extends Schema> {
80
+ type TemplateInstance<S extends Schema> = TemplateInstanceBase<S> & ([S["rootElement"]] extends [never] ? unknown : {
81
+ readonly rootElement: S["rootElement"];
82
+ });
83
+ interface TemplateInstanceBase<S extends Schema> {
79
84
  /** The built nodes; insert it (or its children) where it should show. */
80
85
  readonly root: DocumentFragment;
81
86
  /** The elements marked with `data-ref`. */
package/dist/index.mjs CHANGED
@@ -357,7 +357,33 @@ const NON_BUBBLING = /* @__PURE__ */ new Set([
357
357
  "load",
358
358
  "error",
359
359
  "scroll",
360
- "toggle"
360
+ "scrollend",
361
+ "toggle",
362
+ "beforetoggle",
363
+ "cancel",
364
+ "close",
365
+ "invalid",
366
+ "abort",
367
+ "canplay",
368
+ "canplaythrough",
369
+ "durationchange",
370
+ "emptied",
371
+ "ended",
372
+ "loadeddata",
373
+ "loadedmetadata",
374
+ "loadstart",
375
+ "pause",
376
+ "play",
377
+ "playing",
378
+ "progress",
379
+ "ratechange",
380
+ "seeked",
381
+ "seeking",
382
+ "stalled",
383
+ "suspend",
384
+ "timeupdate",
385
+ "volumechange",
386
+ "waiting"
361
387
  ]);
362
388
  function Component(tpl, opts = {}) {
363
389
  const shadow = opts.shadow === true;
@@ -698,10 +724,13 @@ function template(nodes) {
698
724
  build: () => buildNodes(nodes),
699
725
  instantiate() {
700
726
  const root = buildNodes(nodes);
727
+ const tops = [...root.children].filter((el) => el.localName !== "style" && !(el instanceof HTMLTemplateElement && el.hasAttribute("data-each")));
728
+ const rootElement = tops.length === 1 ? tops[0] : void 0;
701
729
  const { refs, lists, text, conds } = mountTemplate(root);
702
730
  conds.hideAll();
703
731
  return {
704
732
  root,
733
+ rootElement,
705
734
  refs,
706
735
  lists,
707
736
  text,
@@ -1,4 +1,4 @@
1
- import { a as analyze, d as listHtml, f as scanDefines } from "./a11y-DZvViTVA.mjs";
1
+ import { a as analyze, d as listHtml, f as scanDefines } from "./a11y-CeZcA10z.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { basename, dirname, relative } from "node:path";
4
4
  // src/lib/lint.ts
@@ -1,5 +1,5 @@
1
- import { n as checkA11y } from "./a11y-DZvViTVA.mjs";
2
- import { a as dataFlow, i as contractsFor, l as templateCode, r as componentClasses, u as unusedMarkup } from "./lint-B7SIRo0c.mjs";
1
+ import { n as checkA11y } from "./a11y-CeZcA10z.mjs";
2
+ import { a as dataFlow, i as contractsFor, l as templateCode, r as componentClasses, u as unusedMarkup } from "./lint-DS-IOjkf.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
  // src/lib/oxlint-plugin.ts
@@ -1,4 +1,4 @@
1
- import { c as generateFile, d as listHtml, i as CodegenError, l as isFullPage, n as checkA11y, o as formatDiagnostics, s as generateAll, u as isSelected } from "./a11y-DZvViTVA.mjs";
1
+ import { c as generateFile, d as listHtml, i as CodegenError, l as isFullPage, n as checkA11y, o as formatDiagnostics, s as generateAll, u as isSelected } from "./a11y-CeZcA10z.mjs";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { resolve, sep } from "node:path";
4
4
  // src/lib/vite-plugin.ts
package/llms.txt CHANGED
@@ -65,7 +65,7 @@ define("my-counter", MyCounter);
65
65
  ```
66
66
 
67
67
  ## Template without a component
68
- `const view = tpl.instantiate()` returns `{ root: DocumentFragment, refs, lists, text, show }` typed like a component, for pieces created in code. No host: `data-on-*` is inert (use `refs.x.addEventListener`) and `<slot>` is not projected.
68
+ `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
69
 
70
70
  ## Setup
71
71
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativele/ts-dom",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
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.",
5
5
  "keywords": [
6
6
  "html",