@nativele/ts-dom 0.5.1 → 0.5.3

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,57 @@ 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.5.3
14
+
15
+ How to lay out an app with several pages: a new architecture guide, an Official Skill that applies it, and a medium example app checked in CI. Building the example found a codegen bug with several scanned folders, fixed here.
16
+
17
+ ### Guide and Skill
18
+
19
+ - New guide `docs/architecture.md`:
20
+ - feature folders with their components, stores and tests, and a shared folder whose components' interface is their props and events;
21
+ - pages as custom elements mounted by an external router: route params as props, leaving a page, lazy page modules;
22
+ - where state lives at each level, and the split between unit, real-browser and end-to-end tests;
23
+ - one scanned folder for the generated files, checks and tsconfig.
24
+ - New `nativele-architecture` (Official): the procedure for starting a multi-page Nativele app or restructuring one into this layout. The recommended router uses the Navigation API and `URLPattern` with no dependency; universal-router is the alternative. It covers the abort check after a lazy `import()` and the test split.
25
+ - Its example, a project tracker with lazy pages, shared components, feature and app-wide stores, unit, browser and end-to-end tests, runs in CI.
26
+
27
+ ### Fixes
28
+
29
+ - When several folders are scanned (`ts-dom gen a b`, `ts-dom check a b`, `ts-dom doctor a b`, the Vite plugin's `dirs`), each folder was generated on its own, so a template in one folder that used a component defined in another got no import of it and the build silently left the component out, while the types still passed. The folders now share their `define()` calls: the template imports the child by its relative path, `gen --check` and `doctor` agree with what `gen` writes, and the Vite plugin keeps the imports across folders when a template is saved or a `define()` is added or removed. A tag defined with different classes in two scanned folders is now reported as `duplicate-define` at the second `define()` call.
30
+
31
+ ## 0.5.2
32
+
33
+ Skills for the libraries a Nativele app most often sits next to: state stores, testing, data fetching, form validation and Tailwind CSS. Each was accepted through agent trials, and the trials found two bugs in the library, fixed here.
34
+
35
+ ### Skills
36
+
37
+ - New `nativele-state-management` (Official): where state lives (component `state`, `createStore` or an external store), and a selector adapter that connects Zustand, Redux Toolkit or nanostores to `bind`, so a component re-renders only when its slice changes. Its example project runs in CI.
38
+ - New `nativele-testing` (Official): component tests with Vitest and happy-dom or jsdom, real-browser tests with Vitest browser mode, and end-to-end tests with Playwright. It covers what the unit-test DOM cannot do (`moveBefore`, focus during a reorder), forms, slots, isolation between test files, and a clean checkout. Its example project runs in CI.
39
+ - New `nativele-tanstack-query` (Experimental): `@tanstack/query-core` observers through `bind`. It covers loading, error and refreshing states; keys from props; shared requests; mutations; and stopping requests when a component is removed.
40
+ - New `nativele-zod` (Experimental): form validation with Zod or Valibot. It covers per-field messages, nested and array fields, `aria-invalid` and descriptions (also with several instances on a page), cross-field checks, and the emitted data typed from the schema.
41
+ - New `nativele-tailwind` (Experimental): Tailwind CSS v4 in templates. It covers source detection (and excluding the installed Skills), `shadow: true` with an adopted stylesheet, `<style scoped>` defaults in a layer so utilities win, class-based dark mode in a shadow root, and templates outside the project.
42
+ - `nativele-integration` is now Official, verified with Preact 11 and an example project in CI. Its worked example is Preact:
43
+ - the JSX declaration that types Nativele tags;
44
+ - `onitem-select` event props;
45
+ - a ref that sets the property for a component defined after render;
46
+ - keyed moves, which keep the element connected where the browser has `moveBefore`.
47
+
48
+ ### Fixes
49
+
50
+ - In a list item, a `data-ref` on an element inside a `data-if` block was missing from the item's `refs` (the generated type declared it, so the code compiled and then threw). The refs are now collected before the item hides its `data-if` blocks.
51
+ - `unused-ref` (and the matching checks for holes and lists) reported every ref as unused when the class held `this.refs` in a variable (`const r = this.refs; r.name.focus()`). Such an alias now counts as using every name, like `this.refs[k]`; destructuring (`const { name } = this.refs`) is still checked name by name.
52
+
53
+ ### Documentation
54
+
55
+ - `bind`: it calls back at once and on every change, and only a `bind` in `connected()` comes back after re-insertion. A source's `subscribe` must return its unsubscribe function and must not call the listener when subscribing.
56
+ - Styles:
57
+ - a template's `<style>` is not in a cascade layer, so it beats layered framework styles unless its rules are put in a layer;
58
+ - a `shadow: true` component uses an outside stylesheet through `adoptedStyleSheets`.
59
+ - Lists: without `moveBefore` (older browsers, happy-dom, jsdom), a moved item reconnects and loses focus.
60
+ - Slots: children added later are placed after a microtask.
61
+ - `define()`: defining a name a second time keeps the first class.
62
+ - Form recipe: `aria-invalid` must be `"true"`, and blur validation needs `data-on-focusout`.
63
+
13
64
  ## 0.5.1
14
65
 
15
66
  Nativele components now have a Skill for each major framework, every Skill is accepted only after a fresh agent has done real work with it, and the checks that those trials found wrong in real framework projects are fixed.
package/README.md CHANGED
@@ -63,10 +63,16 @@ npx ts-dom skills add nativele-core # copy it into .claude/skills/ (when the
63
63
  | `nativele-core` | building components: the `ts-dom check` loop, templates, state, lists, data flow |
64
64
  | `nativele-react` | Nativele components in React 19: props, events, children and slots, lifecycle, TSX types |
65
65
  | `nativele-vue` | Nativele components in Vue 3: props, events, `v-model`, slots, `<KeepAlive>`, template types |
66
- | `nativele-svelte` | Nativele components in Svelte 5: props, events, slots, template types (Experimental) |
66
+ | `nativele-svelte` | Nativele components in Svelte 5: props, events, slots, template types |
67
67
  | `nativele-angular` | Nativele components in Angular: bindings, change detection, the build, content projection (Experimental) |
68
+ | `nativele-architecture` | app layout: feature folders, shared components and stores, pages mounted by a router, lazy pages, the test split |
68
69
  | `nativele-vite` | Nativele in a Vite app: the plugin and `root`, multi-page builds, hot updates |
69
70
  | `nativele-integration` | Nativele components in a framework without its own Skill (Preact, Solid, ...): properties, events, slots, lifecycle, template types (Experimental) |
71
+ | `nativele-state-management` | where state lives; `createStore`, and Zustand, Redux Toolkit or nanostores through `bind` |
72
+ | `nativele-testing` | component tests with Vitest, real-browser tests |
73
+ | `nativele-tanstack-query` | data fetching with `@tanstack/query-core` (Experimental) |
74
+ | `nativele-zod` | form validation with Zod or Valibot (Experimental) |
75
+ | `nativele-tailwind` | Tailwind CSS in templates and Shadow DOM (Experimental) |
70
76
 
71
77
  Tiers: Official Skills are verified by the maintainers against an example project in CI; Community Skills are contributed and show their author and last verified versions; Experimental Skills are not fully verified yet. `ts-dom skills` marks a Skill as stale when its verification is more than six months old or does not cover the installed version.
72
78
 
@@ -223,6 +229,7 @@ The full guide is in English:
223
229
  - [Components and data flow: state, props, derive, events, composition, stores](https://github.com/Pixmi/nativele/blob/main/docs/components.md)
224
230
  - [Recipes: loading and error states, store data, filtered lists, dialogs, form validation](https://github.com/Pixmi/nativele/blob/main/docs/recipes.md)
225
231
  - [Lists: lists of components with `items =`, plain lists with `replace()`, moving items](https://github.com/Pixmi/nativele/blob/main/docs/lists.md)
232
+ - [Architecture: feature folders, shared components and stores, pages mounted by a router, where state lives, testing strategy](https://github.com/Pixmi/nativele/blob/main/docs/architecture.md)
226
233
  - [Setup, checks and lint: installation, Vite, oxlint, `ts-dom check`, `ts-dom doctor`, accessibility](https://github.com/Pixmi/nativele/blob/main/docs/tooling.md)
227
234
  - [Performance: the large list benchmark](https://github.com/Pixmi/nativele/blob/main/docs/performance.md)
228
235
  - [Design: principles, deliberate decisions, limitations](https://github.com/Pixmi/nativele/blob/main/docs/design.md)
package/README.zh-TW.md CHANGED
@@ -63,10 +63,16 @@ npx ts-dom skills add nativele-core # 複製到 .claude/skills/(專案有 .
63
63
  | `nativele-core` | 開發元件:`ts-dom check` 循環、範本、state、清單、資料流 |
64
64
  | `nativele-react` | 在 React 19 中使用 Nativele 元件:props、事件、子元素與 slot、生命週期、TSX 型別 |
65
65
  | `nativele-vue` | 在 Vue 3 中使用 Nativele 元件:props、事件、`v-model`、slot、`<KeepAlive>`、範本型別 |
66
- | `nativele-svelte` | 在 Svelte 5 中使用 Nativele 元件:props、事件、slot、範本型別(Experimental) |
66
+ | `nativele-svelte` | 在 Svelte 5 中使用 Nativele 元件:props、事件、slot、範本型別 |
67
67
  | `nativele-angular` | 在 Angular 中使用 Nativele 元件:綁定、變更偵測、建置流程、內容投影(Experimental) |
68
+ | `nativele-architecture` | 應用的結構:功能資料夾、共用元件與 store、由 router 掛載的頁面、延遲載入、測試分層 |
68
69
  | `nativele-vite` | 在 Vite 應用中使用 Nativele:外掛與 `root`、多頁建置、熱更新 |
69
70
  | `nativele-integration` | 在沒有專屬 Skill 的框架(Preact、Solid 等)中使用 Nativele 元件:property、事件、slot、生命週期、範本型別(Experimental) |
71
+ | `nativele-state-management` | 狀態放在哪裡;`createStore`,以及透過 `bind` 接上 Zustand、Redux Toolkit、nanostores |
72
+ | `nativele-testing` | 用 Vitest 做元件測試、真實瀏覽器測試 |
73
+ | `nativele-tanstack-query` | 用 `@tanstack/query-core` 取資料(Experimental) |
74
+ | `nativele-zod` | 用 Zod 或 Valibot 驗證表單(Experimental) |
75
+ | `nativele-tailwind` | 在範本與 Shadow DOM 中使用 Tailwind CSS(Experimental) |
70
76
 
71
77
  等級:Official 由維護者以 CI 中的範例專案驗證;Community 由社群貢獻,標示作者與最後驗證版本;Experimental 尚未充分驗證。驗證超過六個月,或不涵蓋已安裝的版本時,`ts-dom skills` 會標示為可能過期。
72
78
 
@@ -223,6 +229,7 @@ define("task-board", TaskBoard);
223
229
  - [Components and data flow: state, props, derive, events, composition, stores](https://github.com/Pixmi/nativele/blob/main/docs/components.md)
224
230
  - [Recipes: loading and error states, store data, filtered lists, dialogs, form validation](https://github.com/Pixmi/nativele/blob/main/docs/recipes.md)
225
231
  - [Lists: lists of components with `items =`, plain lists with `replace()`, moving items](https://github.com/Pixmi/nativele/blob/main/docs/lists.md)
232
+ - [Architecture: feature folders, shared components and stores, pages mounted by a router, where state lives, testing strategy](https://github.com/Pixmi/nativele/blob/main/docs/architecture.md)
226
233
  - [Setup, checks and lint: installation, Vite, oxlint, `ts-dom check`, `ts-dom doctor`, accessibility](https://github.com/Pixmi/nativele/blob/main/docs/tooling.md)
227
234
  - [Performance: the large list benchmark](https://github.com/Pixmi/nativele/blob/main/docs/performance.md)
228
235
  - [Design: principles, deliberate decisions, limitations](https://github.com/Pixmi/nativele/blob/main/docs/design.md)
@@ -1,6 +1,6 @@
1
1
  import { i as RESERVED_NAMES, n as IDENT, r as holeName, t as HOLE_ANY } from "./holes-DTBtTZF_.mjs";
2
2
  import { existsSync, readFileSync, readdirSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
3
- import { dirname, join, relative, sep } from "node:path";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { parseFragment } from "parse5";
5
5
  // src/lib/codegen.ts
6
6
  /** Every diagnostic that `ts-dom gen` can report while analyzing a template. */
@@ -29,7 +29,7 @@ const CODEGEN_RULES = {
29
29
  "forbidden-tag": "<script> cannot be in a template (it would run code)",
30
30
  "forbidden-attribute": "on* event attributes and srcdoc cannot be in a template (they run code or are blocked by Trusted Types)",
31
31
  "unsafe-url": "href / src / action / formaction cannot start with javascript:",
32
- "duplicate-define": "a custom element tag can be defined only once per scanned directory"
32
+ "duplicate-define": "a custom element tag can be defined only once across the scanned directories"
33
33
  };
34
34
  const RESERVED_ELEMENT_NAMES = /* @__PURE__ */ new Set([
35
35
  "annotation-xml",
@@ -406,13 +406,15 @@ function nonCodeRanges(code) {
406
406
  return out;
407
407
  }
408
408
  /**
409
- * Scan a directory for every `define("my-tag", MyClass)` call.
409
+ * Scan one or more directories for every `define("my-tag", MyClass)` call; a file under two of them is read once.
410
410
  * Calls that only appear inside a string or a comment (for example sample code in a test or in documentation) are ignored.
411
411
  */
412
- function scanDefines(dir) {
412
+ function scanDefines(dirs) {
413
413
  const defs = [];
414
414
  const re = /\bdefine\(\s*(["'])([a-z][a-z0-9]*(?:-[a-z0-9]+)+)\1\s*,\s*([A-Za-z_$][\w$]*)\s*\)/g;
415
- for (const file of listTs(dir)) {
415
+ const files = /* @__PURE__ */ new Map();
416
+ for (const dir of typeof dirs === "string" ? [dirs] : dirs) for (const f of listTs(dir)) if (!files.has(resolve(f))) files.set(resolve(f), f);
417
+ for (const file of files.values()) {
416
418
  const code = readFileSync(file, "utf8");
417
419
  const skip = nonCodeRanges(code);
418
420
  const inCode = (pos) => !skip.some(([a, b]) => pos >= a && pos < b);
@@ -422,12 +424,15 @@ function scanDefines(dir) {
422
424
  const [, , tag, className] = m;
423
425
  const named = has(new RegExp(`export\\s+class\\s+${className}\\b`, "g")) || has(new RegExp(`export\\s*\\{[^}]*\\b${className}\\b[^}]*\\}`, "g"));
424
426
  const isDefault = has(new RegExp(`export\\s+default\\s+class\\s+${className}\\b`, "g"));
427
+ const before = code.slice(0, m.index);
425
428
  defs.push({
426
429
  tag,
427
430
  className,
428
431
  file,
429
432
  exported: named || isDefault,
430
- isDefault
433
+ isDefault,
434
+ line: before.split("\n").length,
435
+ col: m.index - before.lastIndexOf("\n")
431
436
  });
432
437
  }
433
438
  }
@@ -437,6 +442,30 @@ const relImport = (fromDir, file) => {
437
442
  const r = relative(fromDir, file).split(sep).join("/").replace(/\.ts$/, ".js");
438
443
  return r.startsWith(".") ? r : `./${r}`;
439
444
  };
445
+ /**
446
+ * A tag that two of the directories define with different classes, reported at its second define() call.
447
+ * A tag defined twice within one directory is reported by `generateTags` instead.
448
+ */
449
+ function crossDirDuplicates(dirs) {
450
+ const byTag = /* @__PURE__ */ new Map();
451
+ for (const d of scanDefines(dirs)) byTag.set(d.tag, [...byTag.get(d.tag) ?? [], d]);
452
+ const inside = (dir, file) => resolve(file).startsWith(resolve(dir) + sep);
453
+ const out = [];
454
+ for (const [tag, defs] of byTag) {
455
+ const [first, second] = defs;
456
+ if (defs.length < 2 || dirs.some((dir) => defs.every((d) => inside(dir, d.file)))) continue;
457
+ const at = (d) => `${relative(process.cwd(), d.file)}:${d.line ?? 1}`;
458
+ out.push({
459
+ file: second.file,
460
+ line: second.line ?? 1,
461
+ col: second.col ?? 1,
462
+ severity: "error",
463
+ rule: "duplicate-define",
464
+ message: `The tag <${tag}> is defined more than once in the scanned folders (${at(first)} and ${at(second)}), so the second define() fails at runtime. Fix: each custom element name can be defined only once; rename one of them.`
465
+ });
466
+ }
467
+ return out;
468
+ }
440
469
  /** Scan `define("my-tag", MyClass)` calls and return the components a type can be generated for (the class must be exported). */
441
470
  function scanTags(dir) {
442
471
  return scanDefines(dir).filter((d) => d.exported).map((d) => ({
@@ -489,10 +518,9 @@ function renderTags(dir) {
489
518
  * Everything `generateAll` would write for a directory, in memory: generated path to content, or null for a file that should not exist.
490
519
  * Templates with error diagnostics are left out and their messages returned in `errors`.
491
520
  */
492
- function planGenerated(dir, options = {}) {
521
+ function planGenerated(dir, options = {}, defs = scanDefines(dir)) {
493
522
  const files = /* @__PURE__ */ new Map();
494
523
  const errors = [];
495
- const defs = scanDefines(dir);
496
524
  for (const f of listHtml(dir)) {
497
525
  if (!isSelected(dir, f, options)) continue;
498
526
  try {
@@ -517,12 +545,12 @@ function planGenerated(dir, options = {}) {
517
545
  };
518
546
  }
519
547
  /** Generated files under `dir` that differ from what `ts-dom gen` would write now. */
520
- function staleGenerated(dir, options = {}) {
521
- return checkGenerated(dir, options).stale;
548
+ function staleGenerated(dir, options = {}, defs) {
549
+ return checkGenerated(dir, options, defs).stale;
522
550
  }
523
551
  /** `staleGenerated`, plus the errors that would make `ts-dom gen` fail (template diagnostics, a tag defined twice). */
524
- function checkGenerated(dir, options = {}) {
525
- const plan = planGenerated(dir, options);
552
+ function checkGenerated(dir, options = {}, defs) {
553
+ const plan = planGenerated(dir, options, defs);
526
554
  const out = [];
527
555
  for (const [file, next] of plan.files) {
528
556
  const prev = existsSync(file) ? readFileSync(file, "utf8") : null;
@@ -555,11 +583,13 @@ function listGenerated(dir) {
555
583
  }
556
584
  return out;
557
585
  }
558
- /** Generate every `x.html.ts` (and the tag map) under a directory; collects all errors and throws them together. */
559
- function generateAll(dir, options = {}) {
586
+ /**
587
+ * Generate every `x.html.ts` (and the tag map) under a directory; collects all errors and throws them together.
588
+ * `defs` are the define() calls templates import child components from (default: those under `dir`).
589
+ */
590
+ function generateAll(dir, options = {}, defs = scanDefines(dir)) {
560
591
  const written = [];
561
592
  const errors = [];
562
- const defs = scanDefines(dir);
563
593
  for (const f of listHtml(dir)) {
564
594
  if (!isSelected(dir, f, options)) continue;
565
595
  try {
@@ -580,17 +610,31 @@ function generateAll(dir, options = {}) {
580
610
  if (errors.length) throw new CodegenError(errors.join("\n"));
581
611
  return written;
582
612
  }
613
+ /** `generateAll` for every directory, with the define() calls of all of them: a template imports a child component defined in another directory. */
614
+ function generateDirs(dirs, options = {}) {
615
+ const defs = scanDefines(dirs);
616
+ const written = [];
617
+ const errors = [];
618
+ for (const dir of dirs) try {
619
+ written.push(...generateAll(dir, options, defs));
620
+ } catch (e) {
621
+ if (e instanceof CodegenError) errors.push(e.message);
622
+ else throw e;
623
+ }
624
+ for (const d of crossDirDuplicates(dirs)) errors.push(formatDiagnostics(d.file, [d]));
625
+ if (errors.length) throw new CodegenError(errors.join("\n"));
626
+ return written;
627
+ }
583
628
  /** Watch .html (and define() calls in .ts) under the directories and regenerate; returns a stop function. Errors go to onError and never stop the watcher. */
584
629
  function watchAll(dirs, opts = {}) {
585
630
  const timers = /* @__PURE__ */ new Map();
586
631
  const watchers = dirs.map((dir) => watch(dir, { recursive: true }, (_event, name) => {
587
632
  if (!name || name.includes("node_modules")) return;
588
633
  if (name.endsWith(".ts") && !name.endsWith(".html.ts") && !name.endsWith(".d.ts") && !name.endsWith(TAGS_FILE)) {
589
- const key = `${dir}:tags`;
590
- clearTimeout(timers.get(key));
591
- timers.set(key, setTimeout(() => {
634
+ clearTimeout(timers.get("tags"));
635
+ timers.set("tags", setTimeout(() => {
592
636
  try {
593
- for (const f of generateAll(dir, opts)) opts.onGenerate?.(f);
637
+ for (const f of generateDirs(dirs, opts)) opts.onGenerate?.(f);
594
638
  } catch (e) {
595
639
  if (e instanceof CodegenError) opts.onError?.(e.message);
596
640
  else throw e;
@@ -604,7 +648,7 @@ function watchAll(dirs, opts = {}) {
604
648
  timers.set(file, setTimeout(() => {
605
649
  try {
606
650
  if (!existsSync(file) || !isSelected(dir, file, opts) || isFullPage(readFileSync(file, "utf8"))) return;
607
- opts.onGenerate?.(generateFile(file, opts, dir));
651
+ opts.onGenerate?.(generateFile(file, opts, scanDefines(dirs)));
608
652
  } catch (e) {
609
653
  if (e instanceof CodegenError) opts.onError?.(e.message);
610
654
  else throw e;
@@ -814,4 +858,4 @@ function checkA11y(html, ignore = []) {
814
858
  for (const { id, el } of labelFor) if (!ids.has(id)) report("label-for", el, `<label for="${id}"> points to an id that does not exist in this template. Fix: add id="${id}" to the control, or wrap the control in the label.`);
815
859
  return out.sort((a, b) => a.line - b.line || a.col - b.col);
816
860
  }
817
- export { watchAll as _, analyze as a, generateAll as c, isSelected as d, listHtml as f, staleGenerated as g, scanDefines as h, CodegenError as i, generateFile as l, renderTags as m, checkA11y as n, checkGenerated as o, listTs as p, CODEGEN_RULES as r, formatDiagnostics as s, A11Y_RULES as t, isFullPage as u };
861
+ export { staleGenerated as _, analyze as a, formatDiagnostics as c, isFullPage as d, isSelected as f, scanDefines as g, renderTags as h, CodegenError as i, generateDirs as l, listTs as m, checkA11y as n, checkGenerated as o, listHtml as p, CODEGEN_RULES as r, crossDirDuplicates as s, A11Y_RULES as t, generateFile as u, watchAll as v };
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { _ as watchAll, a as analyze, c as generateAll, d as isSelected, f as listHtml, g as staleGenerated, i as CodegenError, m as renderTags, n as checkA11y, o as checkGenerated, p as listTs, r as CODEGEN_RULES, t as A11Y_RULES } from "./a11y-CV3nBgBn.mjs";
3
- import { c as lintUsage, n as UNUSED_MARKUP_RULES, o as formatIssue, s as lintDataFlow, t as DATA_FLOW_RULES } from "./lint-DfUAh7t3.mjs";
2
+ import { _ as staleGenerated, a as analyze, c as formatDiagnostics, f as isSelected, g as scanDefines, h as renderTags, i as CodegenError, l as generateDirs, m as listTs, n as checkA11y, o as checkGenerated, p as listHtml, r as CODEGEN_RULES, s as crossDirDuplicates, t as A11Y_RULES, v as watchAll } from "./a11y-Bw-8ZIAK.mjs";
3
+ import { c as lintUsage, n as UNUSED_MARKUP_RULES, o as formatIssue, s as lintDataFlow, t as DATA_FLOW_RULES } from "./lint-qEPgmmMa.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { cpSync, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
6
  import { dirname, join, relative, resolve } from "node:path";
@@ -272,16 +272,22 @@ function runCheck(options) {
272
272
  message: e.message.replace(/^[^[]*\[duplicate-define\]\s*/, "")
273
273
  });
274
274
  }
275
- if (options.write === false) for (const dir of dirs) for (const s of staleGenerated(dir, generate)) issues.push({
276
- file: rel(s.file),
277
- line: 1,
278
- col: 1,
279
- rule: "stale-generated",
280
- severity: "error",
281
- message: `The generated file is ${s.reason}, so the type errors below may not match the templates. Fix: run ts-dom gen (or ts-dom check without --no-write).`
275
+ for (const d of crossDirDuplicates(dirs)) issues.push({
276
+ ...d,
277
+ file: rel(d.file)
282
278
  });
283
- else for (const dir of dirs) try {
284
- generateAll(dir, generate);
279
+ if (options.write === false) {
280
+ const defs = scanDefines(dirs);
281
+ for (const dir of dirs) for (const s of staleGenerated(dir, generate, defs)) issues.push({
282
+ file: rel(s.file),
283
+ line: 1,
284
+ col: 1,
285
+ rule: "stale-generated",
286
+ severity: "error",
287
+ message: `The generated file is ${s.reason}, so the type errors below may not match the templates. Fix: run ts-dom gen (or ts-dom check without --no-write).`
288
+ });
289
+ } else try {
290
+ generateDirs(dirs, generate);
285
291
  } catch (e) {
286
292
  if (!(e instanceof CodegenError)) throw e;
287
293
  }
@@ -522,6 +528,7 @@ function runDoctor(options) {
522
528
  resolvable = false;
523
529
  }
524
530
  if (!resolvable) add("runtime-not-installed", "error", `The generated files import "${runtime}", which cannot be resolved from this project. Fix: npm i ${runtime}.`);
531
+ const defs = scanDefines(dirs);
525
532
  for (const d of dirs) {
526
533
  for (const f of listTs(d)) readFileSync(f, "utf8").split("\n").forEach((text, i) => {
527
534
  const m = /from\s+["'](\.{1,2}\/[^"']+\.html)["']/.exec(text);
@@ -530,7 +537,7 @@ function runDoctor(options) {
530
537
  line: i + 1
531
538
  });
532
539
  });
533
- for (const s of staleGenerated(d, generate)) add("stale-generated", "warning", `The generated file is ${s.reason}. Fix: run ts-dom gen (or ts-dom check, which regenerates).`, { file: rel(s.file) });
540
+ for (const s of staleGenerated(d, generate, defs)) add("stale-generated", "warning", `The generated file is ${s.reason}. Fix: run ts-dom gen (or ts-dom check, which regenerates).`, { file: rel(s.file) });
534
541
  }
535
542
  const viteConfig = [
536
543
  "vite.config.ts",
@@ -1355,9 +1362,10 @@ const dirs = args.filter((a) => !a.startsWith("--"));
1355
1362
  if (!dirs.length) dirs.push("src");
1356
1363
  try {
1357
1364
  if (cmd === "gen" && flags.has("--check")) {
1358
- const results = dirs.map((d) => checkGenerated(d, generate));
1365
+ const defs = scanDefines(dirs);
1366
+ const results = dirs.map((d) => checkGenerated(d, generate, defs));
1359
1367
  const stale = results.flatMap((r) => r.stale);
1360
- const errors = results.flatMap((r) => r.errors);
1368
+ const errors = [...results.flatMap((r) => r.errors), ...crossDirDuplicates(dirs).map((d) => formatDiagnostics(d.file, [d]))];
1361
1369
  if (flags.has("--json")) console.log(JSON.stringify({
1362
1370
  stale: stale.map((s) => ({
1363
1371
  ...s,
@@ -1373,8 +1381,8 @@ try {
1373
1381
  if (stale.length || errors.length) process.exitCode = 1;
1374
1382
  } else if (cmd === "gen") {
1375
1383
  let failed = false;
1376
- for (const d of dirs) try {
1377
- generateAll(d, generate);
1384
+ try {
1385
+ generateDirs(dirs, generate);
1378
1386
  } catch (e) {
1379
1387
  if (!(e instanceof CodegenError) || !flags.has("--watch")) throw e;
1380
1388
  console.error(e.message);
package/dist/index.mjs CHANGED
@@ -268,14 +268,14 @@ var List = class {
268
268
  holder.append(el);
269
269
  const conds = mountConds(holder);
270
270
  const text = mountHoles(holder);
271
- conds.hideAll();
272
- const show = conds.show;
273
- el.remove();
274
271
  const refs = {};
275
272
  for (const n of [el, ...el.querySelectorAll("[data-ref]")]) {
276
273
  const name = n.getAttribute("data-ref");
277
274
  if (name) refs[name] = n;
278
275
  }
276
+ conds.hideAll();
277
+ const show = conds.show;
278
+ el.remove();
279
279
  this.onCreate?.(el);
280
280
  const entry = {
281
281
  el,
@@ -1,4 +1,4 @@
1
- import { a as analyze, f as listHtml, h as scanDefines } from "./a11y-CV3nBgBn.mjs";
1
+ import { a as analyze, g as scanDefines, p as listHtml } from "./a11y-Bw-8ZIAK.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { basename, dirname, relative } from "node:path";
4
4
  // src/lib/lint.ts
@@ -87,7 +87,7 @@ function unusedMarkup(html, code) {
87
87
  message,
88
88
  ...at(a, key)
89
89
  });
90
- const dynamic = (obj) => used(`\\b${obj}\\[\\s*[^"'\\s\\]]`);
90
+ const dynamic = (obj) => used(`\\b${obj}\\[\\s*[^"'\\s\\]]`) || used(`[\\w$]\\s*=\\s*this\\.${obj}\\s*[;,)\\n]|[(,]\\s*this\\.${obj}\\s*[,)]`);
91
91
  const dynRefs = dynamic("refs");
92
92
  const dynText = dynamic("text");
93
93
  const dynLists = dynamic("lists");
@@ -1,5 +1,5 @@
1
- import { n as checkA11y, t as A11Y_RULES } from "./a11y-CV3nBgBn.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-DfUAh7t3.mjs";
1
+ import { n as checkA11y, t as A11Y_RULES } from "./a11y-Bw-8ZIAK.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-qEPgmmMa.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 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-CV3nBgBn.mjs";
1
+ import { c as formatDiagnostics, d as isFullPage, f as isSelected, g as scanDefines, i as CodegenError, l as generateDirs, n as checkA11y, p as listHtml, u as generateFile } from "./a11y-Bw-8ZIAK.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
@@ -19,16 +19,10 @@ function tsDom(opts = {}) {
19
19
  const a11y = opts.a11y === true ? {} : opts.a11y || null;
20
20
  let dirs = (opts.dirs ?? ["src"]).map((d) => resolve(d));
21
21
  const shown = /* @__PURE__ */ new Map();
22
- const defsByDir = /* @__PURE__ */ new Map();
23
- const defsOf = (dir) => {
24
- let defs = defsByDir.get(dir);
25
- if (!defs) defsByDir.set(dir, defs = existsSync(dir) ? scanDefines(dir) : []);
26
- return defs;
27
- };
28
- const isDefineFile = (file) => {
29
- const dir = dirOf(file);
30
- return dir !== void 0 && defsOf(dir).some((d) => resolve(d.file) === file);
31
- };
22
+ let defs;
23
+ const existing = () => dirs.filter((d) => existsSync(d));
24
+ const allDefs = () => defs ??= scanDefines(existing());
25
+ const isDefineFile = (file) => allDefs().some((d) => resolve(d.file) === file);
32
26
  const dirOf = (file) => dirs.find((d) => file.startsWith(d + sep));
33
27
  const warnA11y = (warn) => {
34
28
  if (!a11y) return;
@@ -48,7 +42,7 @@ function tsDom(opts = {}) {
48
42
  const out = `${file}.ts`;
49
43
  const before = read(out);
50
44
  try {
51
- generateFile(file, generate, defsOf(dir));
45
+ generateFile(file, generate, allDefs());
52
46
  } catch (e) {
53
47
  if (e instanceof CodegenError) logger.error(e.message);
54
48
  else throw e;
@@ -57,16 +51,16 @@ function tsDom(opts = {}) {
57
51
  if (a11y) for (const diag of checkA11y(html, a11y.ignore)) logger.warn(formatDiagnostics(file, [diag]));
58
52
  return read(out) !== before;
59
53
  };
60
- /** Regenerate a directory after a define() call may have changed: the tag map and the child imports of every template. */
61
- const regenerateDir = (dir, logger) => {
62
- const before = new Map(listHtml(dir).map((f) => [`${f}.ts`, read(`${f}.ts`)]));
54
+ /** Regenerate after a define() call may have changed: the tag maps and the child imports of every template. */
55
+ const regenerateAll = (logger) => {
56
+ const before = new Map(existing().flatMap(listHtml).map((f) => [`${f}.ts`, read(`${f}.ts`)]));
63
57
  try {
64
- generateAll(dir, generate);
58
+ generateDirs(existing(), generate);
65
59
  } catch (e) {
66
60
  if (e instanceof CodegenError) logger.error(e.message);
67
61
  else throw e;
68
62
  }
69
- defsByDir.set(dir, scanDefines(dir));
63
+ defs = scanDefines(existing());
70
64
  for (const [out, content] of before) {
71
65
  const now = read(out);
72
66
  if (now !== null && now !== content) shown.set(out, now);
@@ -90,10 +84,11 @@ function tsDom(opts = {}) {
90
84
  enforce: "pre",
91
85
  configResolved(config) {
92
86
  dirs = (opts.dirs ?? ["src"]).map((d) => resolve(config.root, d));
87
+ defs = void 0;
93
88
  },
94
89
  buildStart() {
95
90
  try {
96
- for (const d of dirs) if (existsSync(d)) generateAll(d, generate);
91
+ generateDirs(existing(), generate);
97
92
  } catch (e) {
98
93
  if (e instanceof CodegenError) this.error(e.message);
99
94
  throw e;
@@ -105,15 +100,13 @@ function tsDom(opts = {}) {
105
100
  server.watcher.add(dirs);
106
101
  server.watcher.on("add", (f) => {
107
102
  const file = resolve(f);
108
- const dir = dirOf(file);
109
103
  if (file.endsWith(".html")) regenerate(file, logger);
110
- else if (dir && isSourceTs(file) && read(file)?.includes("define(")) regenerateDir(dir, logger);
104
+ else if (dirOf(file) && isSourceTs(file) && read(file)?.includes("define(")) regenerateAll(logger);
111
105
  });
112
106
  server.watcher.on("unlink", (f) => {
113
107
  const file = resolve(f);
114
- const dir = dirOf(file);
115
108
  if (file.endsWith(".html")) removeGenerated(file);
116
- else if (dir && isDefineFile(file)) regenerateDir(dir, logger);
109
+ else if (dirOf(file) && isDefineFile(file)) regenerateAll(logger);
117
110
  });
118
111
  },
119
112
  handleHotUpdate(ctx) {
@@ -130,9 +123,8 @@ function tsDom(opts = {}) {
130
123
  if (regenerate(file, logger)) reload(ctx.server, `${file}.ts`);
131
124
  return [];
132
125
  }
133
- const dir = dirOf(file);
134
- if (!dir || !isSourceTs(file)) return;
135
- if (read(file)?.includes("define(") || isDefineFile(file)) regenerateDir(dir, logger);
126
+ if (!dirOf(file) || !isSourceTs(file)) return;
127
+ if (read(file)?.includes("define(") || isDefineFile(file)) regenerateAll(logger);
136
128
  }
137
129
  };
138
130
  }
package/llms.txt CHANGED
@@ -79,7 +79,7 @@ define("my-counter", MyCounter);
79
79
  - `data-each` is never nested: a list inside an item is the item component's own `data-each`.
80
80
 
81
81
  ## Rules for agents
82
- - Skills: `npx ts-dom skills` lists the task procedures shipped with the package (`nativele-core` for building components, `nativele-react`, `nativele-vue`, `nativele-svelte` and `nativele-angular` for those frameworks, `nativele-vite` for Vite, `nativele-integration` for other frameworks, ...), each with verified versions; `npx ts-dom skills <name>` prints one, `npx ts-dom skills add <name>` copies it into `.claude/skills/` or `.agents/skills/`, and `npx ts-dom doctor` suggests the ones that match the project's dependencies.
82
+ - Skills: `npx ts-dom skills` lists the task procedures shipped with the package (`nativele-core` for building components, `nativele-react`, `nativele-vue`, `nativele-svelte` and `nativele-angular` for those frameworks, `nativele-vite` for Vite, `nativele-integration` for other frameworks, `nativele-state-management`, `nativele-testing`, `nativele-tanstack-query`, `nativele-zod` and `nativele-tailwind` for those tools, `nativele-architecture` for laying out an app with several pages, ...), each with verified versions; `npx ts-dom skills <name>` prints one, `npx ts-dom skills add <name>` copies it into `.claude/skills/` or `.agents/skills/`, and `npx ts-dom doctor` suggests the ones that match the project's dependencies.
83
83
  - `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.
84
84
  - 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.
85
85
  - 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.
@@ -104,5 +104,5 @@ In a browser without a bundler map the bare name once: `<script type="importmap"
104
104
 
105
105
  ## Docs
106
106
  - [README](README.md): introduction, the short version, rules for agents, quick start. [README.zh-TW.md](README.zh-TW.md): the same in Traditional Chinese.
107
- - Guides (English): [templates](docs/templates.md), [components and data flow](docs/components.md), [lists](docs/lists.md), [setup, checks and lint](docs/tooling.md), [performance](docs/performance.md), [design](docs/design.md), [developing Nativele](docs/contributing.md).
107
+ - Guides (English): [templates](docs/templates.md), [components and data flow](docs/components.md), [lists](docs/lists.md), [architecture](docs/architecture.md), [setup, checks and lint](docs/tooling.md), [performance](docs/performance.md), [design](docs/design.md), [developing Nativele](docs/contributing.md).
108
108
  - `npx ts-dom rules`: every rule id with a description.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativele/ts-dom",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Designed for AI agents. Driven by TypeScript. Defined in vanilla HTML. A lightweight library for building web components: typed templates, native custom elements, no virtual DOM, no template expressions, no bundler required.",
5
5
  "keywords": [
6
6
  "html",
package/skills/README.md CHANGED
@@ -125,11 +125,15 @@ Otherwise the maintainers send the gaps back to the contributor as review commen
125
125
  |---|---|---|---|
126
126
  | `nativele-core` | Official | available | building with Nativele itself: the `ts-dom check` loop, components, lists, data flow |
127
127
  | `nativele-react` | Official | available | Nativele components in React 19: props, events, children and slots, lifecycle, TSX types |
128
- | `nativele-integration` | Experimental | available | Nativele components in a framework without its own Skill: defining before render, properties and attributes, events, slots, lifecycle, types |
128
+ | `nativele-integration` | Official | available | Nativele components in a framework without its own Skill: defining before render, properties and attributes, events, slots, lifecycle, types; verified with Preact |
129
129
  | `nativele-vite` | Official | available | Nativele in a Vite app: the plugin and `root`, multi-page builds, the build script, hot updates, Vite+ |
130
130
  | `nativele-vue` | Official | available | Nativele components in Vue 3: `isCustomElement`, props, events, `v-model`, slots, `<KeepAlive>`, template types |
131
131
  | `nativele-svelte` | Experimental | available | Nativele components in Svelte 5: props, event attributes, slots, `bind:this`, template types |
132
132
  | `nativele-angular` | Experimental | available | Nativele components in Angular: `CUSTOM_ELEMENTS_SCHEMA`, bindings, zoneless change detection, `ts-dom gen` in the build, content projection |
133
- | `nativele-state-management` | Official | planned | `createStore`, and Zustand or Redux through `bind` |
134
- | `nativele-testing` | Official | planned | Vitest and Playwright |
135
- | Tailwind CSS, TanStack Query, Zod | Experimental or Community | open for contributions | |
133
+ | `nativele-state-management` | Official | available | where state lives; `createStore`, and Zustand, Redux Toolkit or nanostores through `bind` with a selector adapter |
134
+ | `nativele-testing` | Official | available | component tests with Vitest and happy-dom or jsdom, real-browser tests with Vitest browser mode or Playwright |
135
+ | `nativele-tanstack-query` | Experimental | available | `@tanstack/query-core` observers through `bind`: loading and error states, keys from props, mutations |
136
+ | `nativele-zod` | Experimental | available | form validation with Zod or Valibot: per-field messages, nested and array fields, `aria-invalid` |
137
+ | `nativele-tailwind` | Experimental | available | Tailwind CSS v4 in templates: source detection, `shadow: true` with adopted style sheets, `<style scoped>` layers |
138
+ | `nativele-architecture` | Official | available | app layout: feature folders, shared components and stores, pages as custom elements mounted by a router (Navigation API or universal-router), lazy pages, one scanned folder, unit/browser/end-to-end split |
139
+ | Other libraries | Experimental or Community | open for contributions | |