@nativele/ts-dom 0.5.3 → 0.5.4
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 +37 -0
- package/README.md +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/{a11y-Bw-8ZIAK.mjs → a11y-CiEJj_e6.mjs} +16 -14
- package/dist/cli.mjs +163 -25
- package/dist/{lint-qEPgmmMa.mjs → lint-Did4w-NL.mjs} +2 -2
- package/dist/oxlint-plugin.mjs +2 -2
- package/dist/vite-plugin.mjs +5 -2
- package/llms.txt +3 -1
- package/package.json +1 -1
- package/skills/README.md +2 -2
- package/skills/nativele-angular/SKILL.md +1 -3
- package/skills/nativele-architecture/SKILL.md +1 -1
- package/skills/nativele-integration/SKILL.md +4 -6
- package/skills/nativele-react/SKILL.md +8 -12
- package/skills/nativele-svelte/SKILL.md +8 -12
- package/skills/nativele-testing/SKILL.md +1 -1
- package/skills/nativele-vite/SKILL.md +1 -1
- package/skills/nativele-vue/SKILL.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,43 @@ 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.4
|
|
14
|
+
|
|
15
|
+
The guides, the Skills and the tools agree with each other: wrong statements in the guides are fixed, facts the Skills repeated now live in the guides, and the tools report the traps the Skill trials hit instead of staying silent.
|
|
16
|
+
|
|
17
|
+
### New checks
|
|
18
|
+
|
|
19
|
+
- `ts-dom doctor` reports `typecheck-before-gen` (warning): a package.json script, with its `pre` script and the scripts it runs through `npm run` (or pnpm, yarn, bun), that runs `tsc`, `vue-tsc`, `svelte-check` or `ng build`/`serve`/`test` before any `ts-dom gen` or `ts-dom check` while `.gitignore` leaves out `*.html.ts`; on a clean checkout it fails with TS2307. A `tsc -p` whose config covers none of the scanned folders is skipped.
|
|
20
|
+
- `ts-dom doctor` reports `framework-template` (error): an `.html` in a scanned folder that a TypeScript file names as an Angular `templateUrl`. The `invalid-hole` and `no-importer` hints now also say that the folder may not be meant to be scanned.
|
|
21
|
+
- `formatter-whitespace` also reports generated files written with `preserve` while a package.json script or the Vite config asks for `--whitespace=collapse`, with the command to regenerate them.
|
|
22
|
+
- `vite-plugin-missing` also reports a `vitest.config.*` that replaces the Vite config for Vitest without the `tsDom()` plugin and without importing `./vite.config`.
|
|
23
|
+
|
|
24
|
+
### Fixes
|
|
25
|
+
|
|
26
|
+
- The Vite plugin warns (`dir-not-found`) when a `tsDom({ dirs })` entry does not exist under Vite's `root`, instead of skipping it silently.
|
|
27
|
+
- A generated `ts-dom-tags.ts` in a subfolder that is not itself scanned (left over after components moved) is an orphan: `gen` deletes it, and `gen --check`, `check --no-write` and `doctor` report it as `stale-generated`.
|
|
28
|
+
- A `tsc` error located in a generated `x.html.ts` or `ts-dom-tags.ts` now says to run `ts-dom gen` or delete the leftover file, not to change the TypeScript there.
|
|
29
|
+
- `ts-dom check` leaves out `tsc` errors inside the body of a class it already reported for a contract error (for example an implicit `any` on a `bind` callback's parameter next to a `{{ hole }}` that names nothing).
|
|
30
|
+
- `ts-dom --help` lists `--include`, `--exclude` and `--whitespace` under `check` and `doctor`, which accept them.
|
|
31
|
+
|
|
32
|
+
### Documentation
|
|
33
|
+
|
|
34
|
+
- `docs/components.md` gains:
|
|
35
|
+
- Light DOM slots and children managed by a framework;
|
|
36
|
+
- when a Light or Shadow component builds its content;
|
|
37
|
+
- the `props` and `"~emits"` members that framework declarations use;
|
|
38
|
+
- `EmitEvent` with an undeclared name;
|
|
39
|
+
- `ReadableStore`.
|
|
40
|
+
- Five framework Skills link to the slot section instead of repeating it.
|
|
41
|
+
- Corrected:
|
|
42
|
+
- the import map in `docs/design.md`;
|
|
43
|
+
- the generated files being "already in `.gitignore`" (a project adds them itself);
|
|
44
|
+
- duplicate and misplaced reordering notes;
|
|
45
|
+
- `llms.txt` lists every guide.
|
|
46
|
+
- The Skills agree on one build-script rule: a project whose type check is plain `tsc` runs `ts-dom check` before the bundler, and a framework type checker gets `ts-dom gen` in front. The README Skill tables show the right tiers.
|
|
47
|
+
- `test/skills.test.ts` checks the README Skill tables against each Skill's tier, and every link and anchor in the READMEs, `llms.txt`, the guides and the Skills.
|
|
48
|
+
- `docs/tooling.md`: the new checks; the bare specifier to map is `@nativele/ts-dom`; the Vite plugin generates when the build, the dev server or Vitest starts, never type-checks, prints rejected templates only in the terminal, and sends a full reload to every client; a run without `--whitespace` keeps each file's recorded mode; `check` runs `tsc` once per referenced project of a solution-style tsconfig.
|
|
49
|
+
|
|
13
50
|
## 0.5.3
|
|
14
51
|
|
|
15
52
|
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.
|
package/README.md
CHANGED
|
@@ -63,11 +63,11 @@ 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 |
|
|
66
|
+
| `nativele-svelte` | Nativele components in Svelte 5: props, events, slots, template types (Experimental) |
|
|
67
67
|
| `nativele-angular` | Nativele components in Angular: bindings, change detection, the build, content projection (Experimental) |
|
|
68
68
|
| `nativele-architecture` | app layout: feature folders, shared components and stores, pages mounted by a router, lazy pages, the test split |
|
|
69
69
|
| `nativele-vite` | Nativele in a Vite app: the plugin and `root`, multi-page builds, hot updates |
|
|
70
|
-
| `nativele-integration` | Nativele components in a framework without its own Skill (Preact, Solid, ...): properties, events, slots, lifecycle, template types
|
|
70
|
+
| `nativele-integration` | Nativele components in a framework without its own Skill (Preact, Solid, ...): properties, events, slots, lifecycle, template types |
|
|
71
71
|
| `nativele-state-management` | where state lives; `createStore`, and Zustand, Redux Toolkit or nanostores through `bind` |
|
|
72
72
|
| `nativele-testing` | component tests with Vitest, real-browser tests |
|
|
73
73
|
| `nativele-tanstack-query` | data fetching with `@tanstack/query-core` (Experimental) |
|
package/README.zh-TW.md
CHANGED
|
@@ -63,11 +63,11 @@ 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
|
|
66
|
+
| `nativele-svelte` | 在 Svelte 5 中使用 Nativele 元件:props、事件、slot、範本型別(Experimental) |
|
|
67
67
|
| `nativele-angular` | 在 Angular 中使用 Nativele 元件:綁定、變更偵測、建置流程、內容投影(Experimental) |
|
|
68
68
|
| `nativele-architecture` | 應用的結構:功能資料夾、共用元件與 store、由 router 掛載的頁面、延遲載入、測試分層 |
|
|
69
69
|
| `nativele-vite` | 在 Vite 應用中使用 Nativele:外掛與 `root`、多頁建置、熱更新 |
|
|
70
|
-
| `nativele-integration` | 在沒有專屬 Skill 的框架(Preact、Solid 等)中使用 Nativele 元件:property、事件、slot
|
|
70
|
+
| `nativele-integration` | 在沒有專屬 Skill 的框架(Preact、Solid 等)中使用 Nativele 元件:property、事件、slot、生命週期、範本型別 |
|
|
71
71
|
| `nativele-state-management` | 狀態放在哪裡;`createStore`,以及透過 `bind` 接上 Zustand、Redux Toolkit、nanostores |
|
|
72
72
|
| `nativele-testing` | 用 Vitest 做元件測試、真實瀏覽器測試 |
|
|
73
73
|
| `nativele-tanstack-query` | 用 `@tanstack/query-core` 取資料(Experimental) |
|
|
@@ -104,7 +104,7 @@ function analyze(html) {
|
|
|
104
104
|
mark(listName ? `text:${listName}/${name}` : `text:${name}`, at);
|
|
105
105
|
} else {
|
|
106
106
|
const inner = (m[1] ?? "").trim();
|
|
107
|
-
report("invalid-hole", "error", `{{ ${inner} }} is not a valid hole: a hole must be a single identifier, paths and expressions are not supported. Fix: compute the value in TypeScript (state, derive or this.text) and use a named hole such as {{ name }}.`, at);
|
|
107
|
+
report("invalid-hole", "error", `{{ ${inner} }} is not a valid hole: a hole must be a single identifier, paths and expressions are not supported. Fix: compute the value in TypeScript (state, derive or this.text) and use a named hole such as {{ name }}. If the file is another framework's template, this folder is not meant to be scanned: scan only the folder with the Nativele templates, or leave the file out with --exclude.`, at);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -517,8 +517,9 @@ function renderTags(dir) {
|
|
|
517
517
|
/**
|
|
518
518
|
* Everything `generateAll` would write for a directory, in memory: generated path to content, or null for a file that should not exist.
|
|
519
519
|
* Templates with error diagnostics are left out and their messages returned in `errors`.
|
|
520
|
+
* `scanned` are all the directories of the run: a ts-dom-tags.ts in a subfolder is an orphan unless that subfolder is one of them.
|
|
520
521
|
*/
|
|
521
|
-
function planGenerated(dir, options = {}, defs = scanDefines(dir)) {
|
|
522
|
+
function planGenerated(dir, options = {}, defs = scanDefines(dir), scanned = [dir]) {
|
|
522
523
|
const files = /* @__PURE__ */ new Map();
|
|
523
524
|
const errors = [];
|
|
524
525
|
for (const f of listHtml(dir)) {
|
|
@@ -538,26 +539,26 @@ function planGenerated(dir, options = {}, defs = scanDefines(dir)) {
|
|
|
538
539
|
if (e instanceof CodegenError) errors.push(e.message);
|
|
539
540
|
else throw e;
|
|
540
541
|
}
|
|
541
|
-
for (const f of listGenerated(dir)) if (!files.has(f) && !existsSync(f.slice(0, -3))) files.set(f, null);
|
|
542
|
+
for (const f of listGenerated(dir, scanned)) if (!files.has(f) && !existsSync(f.slice(0, -3))) files.set(f, null);
|
|
542
543
|
return {
|
|
543
544
|
files,
|
|
544
545
|
errors
|
|
545
546
|
};
|
|
546
547
|
}
|
|
547
548
|
/** Generated files under `dir` that differ from what `ts-dom gen` would write now. */
|
|
548
|
-
function staleGenerated(dir, options = {}, defs) {
|
|
549
|
-
return checkGenerated(dir, options, defs).stale;
|
|
549
|
+
function staleGenerated(dir, options = {}, defs, scanned) {
|
|
550
|
+
return checkGenerated(dir, options, defs, scanned).stale;
|
|
550
551
|
}
|
|
551
552
|
/** `staleGenerated`, plus the errors that would make `ts-dom gen` fail (template diagnostics, a tag defined twice). */
|
|
552
|
-
function checkGenerated(dir, options = {}, defs) {
|
|
553
|
-
const plan = planGenerated(dir, options, defs);
|
|
553
|
+
function checkGenerated(dir, options = {}, defs, scanned) {
|
|
554
|
+
const plan = planGenerated(dir, options, defs, scanned);
|
|
554
555
|
const out = [];
|
|
555
556
|
for (const [file, next] of plan.files) {
|
|
556
557
|
const prev = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
557
558
|
if (next === null) {
|
|
558
559
|
if (prev !== null && prev.startsWith(TAGS_HEADER)) out.push({
|
|
559
560
|
file,
|
|
560
|
-
reason: file
|
|
561
|
+
reason: file === join(dir, TAGS_FILE) ? "outdated" : "orphan"
|
|
561
562
|
});
|
|
562
563
|
} else if (prev === null) out.push({
|
|
563
564
|
file,
|
|
@@ -573,13 +574,14 @@ function checkGenerated(dir, options = {}, defs) {
|
|
|
573
574
|
errors: plan.errors
|
|
574
575
|
};
|
|
575
576
|
}
|
|
576
|
-
function listGenerated(dir) {
|
|
577
|
+
function listGenerated(dir, scanned, top = true) {
|
|
577
578
|
const out = [];
|
|
579
|
+
const ownTags = top || scanned.some((d) => resolve(d) === resolve(dir));
|
|
578
580
|
for (const name of readdirSync(dir)) {
|
|
579
581
|
if (name === "node_modules") continue;
|
|
580
582
|
const full = join(dir, name);
|
|
581
|
-
if (statSync(full).isDirectory()) out.push(...listGenerated(full));
|
|
582
|
-
else if (name.endsWith(".html.ts") && readFileSync(full, "utf8").startsWith(TAGS_HEADER)) out.push(full);
|
|
583
|
+
if (statSync(full).isDirectory()) out.push(...listGenerated(full, scanned, false));
|
|
584
|
+
else if ((name.endsWith(".html.ts") || name === TAGS_FILE && !ownTags) && readFileSync(full, "utf8").startsWith(TAGS_HEADER)) out.push(full);
|
|
583
585
|
}
|
|
584
586
|
return out;
|
|
585
587
|
}
|
|
@@ -587,7 +589,7 @@ function listGenerated(dir) {
|
|
|
587
589
|
* Generate every `x.html.ts` (and the tag map) under a directory; collects all errors and throws them together.
|
|
588
590
|
* `defs` are the define() calls templates import child components from (default: those under `dir`).
|
|
589
591
|
*/
|
|
590
|
-
function generateAll(dir, options = {}, defs = scanDefines(dir)) {
|
|
592
|
+
function generateAll(dir, options = {}, defs = scanDefines(dir), scanned = [dir]) {
|
|
591
593
|
const written = [];
|
|
592
594
|
const errors = [];
|
|
593
595
|
for (const f of listHtml(dir)) {
|
|
@@ -606,7 +608,7 @@ function generateAll(dir, options = {}, defs = scanDefines(dir)) {
|
|
|
606
608
|
if (e instanceof CodegenError) errors.push(e.message);
|
|
607
609
|
else throw e;
|
|
608
610
|
}
|
|
609
|
-
for (const f of listGenerated(dir)) if (!existsSync(f.slice(0, -3))) unlinkSync(f);
|
|
611
|
+
for (const f of listGenerated(dir, scanned)) if (!existsSync(f.slice(0, -3))) unlinkSync(f);
|
|
610
612
|
if (errors.length) throw new CodegenError(errors.join("\n"));
|
|
611
613
|
return written;
|
|
612
614
|
}
|
|
@@ -616,7 +618,7 @@ function generateDirs(dirs, options = {}) {
|
|
|
616
618
|
const written = [];
|
|
617
619
|
const errors = [];
|
|
618
620
|
for (const dir of dirs) try {
|
|
619
|
-
written.push(...generateAll(dir, options, defs));
|
|
621
|
+
written.push(...generateAll(dir, options, defs, dirs));
|
|
620
622
|
} catch (e) {
|
|
621
623
|
if (e instanceof CodegenError) errors.push(e.message);
|
|
622
624
|
else throw e;
|
package/dist/cli.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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-
|
|
3
|
-
import { c as lintUsage, n as UNUSED_MARKUP_RULES, o as formatIssue, s as lintDataFlow, t as DATA_FLOW_RULES } from "./lint-
|
|
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-CiEJj_e6.mjs";
|
|
3
|
+
import { c as lintUsage, n as UNUSED_MARKUP_RULES, o as formatIssue, s as lintDataFlow, t as DATA_FLOW_RULES } from "./lint-Did4w-NL.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { cpSync, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
// src/lib/tsconfig.ts
|
|
@@ -164,13 +164,22 @@ function mapTscErrors(errors, cwd) {
|
|
|
164
164
|
}
|
|
165
165
|
};
|
|
166
166
|
const brokenClasses = /* @__PURE__ */ new Set();
|
|
167
|
+
const brokenBodies = [];
|
|
167
168
|
for (const e of errors) {
|
|
168
169
|
if (!/missingStateKeys|conflictingKeys|reservedPropNames|nativeEventNames/.test(e.message)) continue;
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
170
|
+
const text = readFileSync(resolve(cwd, e.file), "utf8");
|
|
171
|
+
const lines = text.split("\n");
|
|
172
|
+
const m = /class\s+([A-Za-z_$][\w$]*)\s+extends\s+Component\s*\(/.exec(lines[e.line - 1] ?? "");
|
|
173
|
+
if (!m?.[1]) continue;
|
|
174
|
+
brokenClasses.add(m[1]);
|
|
175
|
+
const end = classBodyEnd(text, lines.slice(0, e.line - 1).join("\n").length + (e.line > 1 ? 1 : 0) + m.index + m[0].length - 1);
|
|
176
|
+
if (end !== -1) brokenBodies.push({
|
|
177
|
+
file: rel(resolve(cwd, e.file)),
|
|
178
|
+
from: e.line,
|
|
179
|
+
to: text.slice(0, end).split("\n").length
|
|
180
|
+
});
|
|
172
181
|
}
|
|
173
|
-
const followsBroken = (
|
|
182
|
+
const followsBroken = (i) => [...brokenClasses].some((c) => i.message.includes(`'${c}'`) || i.message.includes(`'typeof ${c}'`)) || brokenBodies.some((b) => b.file === i.file && i.line > b.from && i.line <= b.to);
|
|
174
183
|
for (const e of errors) {
|
|
175
184
|
const file = resolve(cwd, e.file);
|
|
176
185
|
const where = `${rel(file)}:${e.line}:${e.col}`;
|
|
@@ -208,9 +217,31 @@ function mapTscErrors(errors, cwd) {
|
|
|
208
217
|
named = true;
|
|
209
218
|
issue(rule, `${e.message.split("\n")[0]} Fix: ${fix}.`);
|
|
210
219
|
}
|
|
211
|
-
if (
|
|
220
|
+
if (named) continue;
|
|
221
|
+
if (file.endsWith(".html.ts") || file.endsWith(`${sep}ts-dom-tags.ts`)) issue("tsc", `${e.code}: ${e.message} This file is generated, so an edit to it is overwritten. Fix: run ts-dom gen for the folder that holds it; if it is left over from a template or folder that is no longer scanned, delete it.`);
|
|
222
|
+
else issue("tsc", `${e.code}: ${e.message} Fix: change the TypeScript at this location so the types agree; ts-dom rules --explain=tsc.`);
|
|
212
223
|
}
|
|
213
|
-
return out.filter((i) => i.rule !== "tsc" || !followsBroken(i
|
|
224
|
+
return out.filter((i) => i.rule !== "tsc" || !followsBroken(i));
|
|
225
|
+
}
|
|
226
|
+
/** The offset of the `}` that closes a class body, given the offset of the `(` of its `extends Component(`; -1 when it cannot be found. Strings and comments are skipped. */
|
|
227
|
+
function classBodyEnd(text, open) {
|
|
228
|
+
let depth = 0;
|
|
229
|
+
let inBody = false;
|
|
230
|
+
for (let i = open; i < text.length; i++) {
|
|
231
|
+
const c = text[i];
|
|
232
|
+
if (c === "\"" || c === "'" || c === "`") {
|
|
233
|
+
for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
|
|
234
|
+
} else if (c === "/" && text[i + 1] === "/") i = text.indexOf("\n", i) === -1 ? text.length : text.indexOf("\n", i);
|
|
235
|
+
else if (c === "/" && text[i + 1] === "*") i = text.indexOf("*/", i) === -1 ? text.length : text.indexOf("*/", i) + 1;
|
|
236
|
+
else if ("([{".includes(c)) {
|
|
237
|
+
if (c === "{" && depth === 0) inBody = true;
|
|
238
|
+
depth++;
|
|
239
|
+
} else if (")]}".includes(c)) {
|
|
240
|
+
depth--;
|
|
241
|
+
if (depth === 0 && inBody) return i;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return -1;
|
|
214
245
|
}
|
|
215
246
|
function findTsc(cwd) {
|
|
216
247
|
const req = createRequire(join(cwd, "package.json"));
|
|
@@ -278,7 +309,7 @@ function runCheck(options) {
|
|
|
278
309
|
});
|
|
279
310
|
if (options.write === false) {
|
|
280
311
|
const defs = scanDefines(dirs);
|
|
281
|
-
for (const dir of dirs) for (const s of staleGenerated(dir, generate, defs)) issues.push({
|
|
312
|
+
for (const dir of dirs) for (const s of staleGenerated(dir, generate, defs, dirs)) issues.push({
|
|
282
313
|
file: rel(s.file),
|
|
283
314
|
line: 1,
|
|
284
315
|
col: 1,
|
|
@@ -451,9 +482,11 @@ const DOCTOR_RULES = {
|
|
|
451
482
|
"runtime-not-installed": "the runtime package the generated files import cannot be resolved",
|
|
452
483
|
"template-import-without-js": "a template is imported as ./x.html instead of ./x.html.js",
|
|
453
484
|
"stale-generated": "a generated file is missing, outdated or orphaned",
|
|
454
|
-
"
|
|
485
|
+
"framework-template": "a scanned .html is an Angular component's templateUrl, not a Nativele template",
|
|
486
|
+
"typecheck-before-gen": "a package.json script type-checks before ts-dom gen while *.html.ts is git-ignored, so it fails on a clean checkout",
|
|
487
|
+
"vite-plugin-missing": "the Vite config, or a vitest.config that replaces it for Vitest, does not use the ts-dom plugin, so templates are not generated by the dev server or the tests",
|
|
455
488
|
"oxlint-plugin-missing": "the lint config does not load the ts-dom oxlint plugin, so the template rules do not run in the editor",
|
|
456
|
-
"formatter-whitespace": "a formatter is set up while templates keep their whitespace, so reformatting can change the rendered spacing",
|
|
489
|
+
"formatter-whitespace": "a formatter is set up while templates keep their whitespace, so reformatting can change the rendered spacing; or the scripts or Vite config ask for collapse while the generated files were written with preserve",
|
|
457
490
|
"skill-available": "the project uses a tool that a Nativele Skill covers, and the project has no copy of that Skill",
|
|
458
491
|
"skill-outdated": "the project's copy of a Nativele Skill differs from the one shipped with the installed version"
|
|
459
492
|
};
|
|
@@ -467,6 +500,24 @@ function covers(pattern, dir) {
|
|
|
467
500
|
if (pattern.slice(star).startsWith("**")) return d === prefix || d.startsWith(`${prefix}/`);
|
|
468
501
|
return d === prefix;
|
|
469
502
|
}
|
|
503
|
+
const TYPECHECK = /(?:^|\s)(?:npx\s+)?(tsc|vue-tsc|svelte-check|ng\s+(?:build|serve|test))(?=\s|$)/;
|
|
504
|
+
const TSC_PROJECT = /(?:^|\s)(?:npx\s+)?(?:vue-)?tsc(?:\s.*?)?\s(?:-p|--project)[\s=]+(\S+)/;
|
|
505
|
+
const GENERATES = /(?:^|\s)ts-dom\s+(?:gen|check)(?=\s|$)/;
|
|
506
|
+
const RUN_SCRIPT = /^(?:npm\s+run(?:-script)?|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun\s+run)\s+([\w:.-]+)/;
|
|
507
|
+
/** The commands a script runs in order, with its pre and post scripts and the scripts it runs through npm run (or pnpm, yarn, bun) spelled out. */
|
|
508
|
+
function scriptSteps(scripts, name, depth = 0) {
|
|
509
|
+
const command = scripts[name];
|
|
510
|
+
if (depth > 5 || command === void 0) return [];
|
|
511
|
+
const own = command.split(/&&|;/).flatMap((step) => {
|
|
512
|
+
const ref = RUN_SCRIPT.exec(step.trim())?.[1];
|
|
513
|
+
return ref && scripts[ref] !== void 0 ? scriptSteps(scripts, ref, depth + 1) : [step.trim()];
|
|
514
|
+
});
|
|
515
|
+
return [
|
|
516
|
+
...scriptSteps(scripts, `pre${name}`, depth + 1),
|
|
517
|
+
...own,
|
|
518
|
+
...scriptSteps(scripts, `post${name}`, depth + 1)
|
|
519
|
+
];
|
|
520
|
+
}
|
|
470
521
|
const templateFolders = (dir) => [...new Set(listHtml(dir).map((f) => dirname(f)))];
|
|
471
522
|
const hasTemplates = (dir) => listHtml(dir).length > 0;
|
|
472
523
|
/** Check the project set-up an agent or a developer could get wrong; every issue names its rule and ends with a Fix hint. */
|
|
@@ -537,7 +588,14 @@ function runDoctor(options) {
|
|
|
537
588
|
line: i + 1
|
|
538
589
|
});
|
|
539
590
|
});
|
|
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) });
|
|
591
|
+
for (const s of staleGenerated(d, generate, defs, dirs)) 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) });
|
|
592
|
+
}
|
|
593
|
+
const scannedHtml = new Set(dirs.flatMap(listHtml));
|
|
594
|
+
if (scannedHtml.size) for (const f of listTs(cwd)) for (const m of readFileSync(f, "utf8").matchAll(/templateUrl\s*:\s*["'`]([^"'`]+\.html)["'`]/g)) {
|
|
595
|
+
const html = resolve(dirname(f), m[1]);
|
|
596
|
+
if (!scannedHtml.has(html)) continue;
|
|
597
|
+
scannedHtml.delete(html);
|
|
598
|
+
add("framework-template", "error", `${rel(html)} is the Angular template of ${rel(f)} (templateUrl), not a Nativele template, so ts-dom gen reports its Angular syntax as errors. Fix: scan only the folder with the Nativele templates (for example ts-dom gen src/nv, and the same folder for check, doctor and tsDom({ dirs })).`, { file: rel(html) });
|
|
541
599
|
}
|
|
542
600
|
const viteConfig = [
|
|
543
601
|
"vite.config.ts",
|
|
@@ -547,6 +605,16 @@ function runDoctor(options) {
|
|
|
547
605
|
].map((n) => join(cwd, n)).find((f) => existsSync(f));
|
|
548
606
|
const vite = viteConfig ? readText(viteConfig) : null;
|
|
549
607
|
if (viteConfig && vite !== null && !/@nativele\/ts-dom\/vite|vite-plugin(\.ts)?["']/.test(vite)) add("vite-plugin-missing", "warning", "The Vite config does not use the ts-dom plugin, so templates are not regenerated while the dev server runs. Fix: import { tsDom } from \"@nativele/ts-dom/vite\" and add tsDom() to plugins.", { file: rel(viteConfig) });
|
|
608
|
+
const vitestConfig = [
|
|
609
|
+
"ts",
|
|
610
|
+
"mts",
|
|
611
|
+
"cts",
|
|
612
|
+
"js",
|
|
613
|
+
"mjs",
|
|
614
|
+
"cjs"
|
|
615
|
+
].map((ext) => join(cwd, `vitest.config.${ext}`)).find((f) => existsSync(f));
|
|
616
|
+
const vitest = vitestConfig ? readText(vitestConfig) : null;
|
|
617
|
+
if (vitestConfig && vitest !== null && !/@nativele\/ts-dom\/vite|vite-plugin(\.ts)?["']/.test(vitest) && !/["']\.\/vite\.config(\.[cm]?[jt]s)?["']/.test(vitest)) add("vite-plugin-missing", "warning", `${rel(vitestConfig)} replaces the Vite config for Vitest and does not use the ts-dom plugin, so the templates are not generated when the tests start and a clean checkout fails to resolve ./x.html.js. Fix: add tsDom() to its plugins, or merge the Vite config into it: mergeConfig(viteConfig, defineConfig({ test: { ... } })) with viteConfig imported from "./vite.config".`, { file: rel(vitestConfig) });
|
|
550
618
|
const oxlintrc = [".oxlintrc.json", "oxlintrc.json"].map((n) => join(cwd, n)).find((f) => existsSync(f));
|
|
551
619
|
const lintConfigs = [oxlintrc ? readText(oxlintrc) : null, vite && /\blint\s*:/.test(vite) ? vite : null].filter((t) => t !== null);
|
|
552
620
|
if (lintConfigs.length && !lintConfigs.some((t) => /@nativele\/ts-dom\/oxlint|oxlint-plugin(\.ts)?["']/.test(t))) add("oxlint-plugin-missing", "warning", "The lint config does not load the ts-dom oxlint plugin, so unused markup, data flow and accessibility rules do not run in the editor. Fix: add { name: \"ts-dom\", specifier: \"@nativele/ts-dom/oxlint\" } to jsPlugins and enable \"ts-dom/unused-markup\".", { file: rel(oxlintrc ?? viteConfig) });
|
|
@@ -557,6 +625,31 @@ function runDoctor(options) {
|
|
|
557
625
|
...pkg.devDependencies
|
|
558
626
|
};
|
|
559
627
|
const scripts = Object.values(pkg.scripts ?? {});
|
|
628
|
+
if ((readText(join(cwd, ".gitignore")) ?? "").split("\n").some((l) => !l.trim().startsWith("!") && /\*\.html\.ts\s*$/.test(l)) && dirs.some(hasTemplates)) {
|
|
629
|
+
const all = pkg.scripts ?? {};
|
|
630
|
+
const checksTemplates = (step) => {
|
|
631
|
+
const project = TSC_PROJECT.exec(step)?.[1];
|
|
632
|
+
if (!project && !/(?:^|\s)(?:npx\s+)?(?:vue-)?tsc(?=\s|$)/.test(step)) return true;
|
|
633
|
+
const config = resolve(cwd, project ?? "tsconfig.json");
|
|
634
|
+
const configs = projectConfigs(cwd, existsSync(config) && statSync(config).isDirectory() ? join(config, "tsconfig.json") : config);
|
|
635
|
+
return !configs.length || configs.some((c) => {
|
|
636
|
+
const include = readTsConfig(c).include;
|
|
637
|
+
return !include || targets.some((t) => include.some((p) => covers(p, t)));
|
|
638
|
+
});
|
|
639
|
+
};
|
|
640
|
+
const dirArgs = options.dirs.join(" ");
|
|
641
|
+
for (const name of Object.keys(all)) {
|
|
642
|
+
if (/^(pre|post)./.test(name) && all[name.replace(/^(pre|post)/, "")] !== void 0) continue;
|
|
643
|
+
const step = scriptSteps(all, name).find((s) => GENERATES.test(s) || TYPECHECK.test(s) && checksTemplates(s));
|
|
644
|
+
if (step === void 0 || GENERATES.test(step)) continue;
|
|
645
|
+
const tool = TYPECHECK.exec(step)[1]?.replace(/\s+/g, " ");
|
|
646
|
+
const line = pkgText.split("\n").findIndex((l) => l.includes(`${JSON.stringify(name)}:`)) + 1;
|
|
647
|
+
add("typecheck-before-gen", "warning", `The script "${name}" runs ${tool} before any ts-dom gen, and .gitignore leaves out *.html.ts, so on a clean checkout ${tool} fails with TS2307 (cannot find module './x.html.js'). Fix: put "ts-dom gen ${dirArgs} && " in front of it, or use "ts-dom check ${dirArgs}" as the type check when the project uses plain tsc.`, {
|
|
648
|
+
file: "package.json",
|
|
649
|
+
...line ? { line } : {}
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
560
653
|
const ignoredHtml = /(^|\n)\s*(\*\*\/)?\*\.html\s*($|\n)/.test(readText(join(cwd, ".prettierignore")) ?? "");
|
|
561
654
|
const formatter = [".oxfmtrc.json", ".oxfmtrc.jsonc"].some((n) => existsSync(join(cwd, n))) || "oxfmt" in deps || scripts.some((c) => /(^|[\s;&|])(oxfmt|vp fmt)(\s|$)/.test(c)) || vite !== null && /\bfmt\s*:/.test(vite) || !ignoredHtml && ([
|
|
562
655
|
".prettierrc",
|
|
@@ -565,7 +658,22 @@ function runDoctor(options) {
|
|
|
565
658
|
"prettier.config.js",
|
|
566
659
|
"prettier.config.mjs"
|
|
567
660
|
].some((n) => existsSync(join(cwd, n))) || "prettier" in deps || scripts.some((c) => /(^|[\s;&|])prettier(\s|$)/.test(c)));
|
|
568
|
-
const
|
|
661
|
+
const collapseScript = Object.entries(pkg.scripts ?? {}).find(([, c]) => c.includes("--whitespace=collapse"))?.[0];
|
|
662
|
+
const collapseVite = vite !== null && /whitespace\s*:\s*["']collapse["']/.test(vite);
|
|
663
|
+
const generatedModes = dirs.flatMap(listHtml).flatMap((f) => {
|
|
664
|
+
const head = (readText(`${f}.ts`) ?? "").split("\n", 3);
|
|
665
|
+
return head[0]?.startsWith("// Generated by ts-dom.") ? [{
|
|
666
|
+
file: `${f}.ts`,
|
|
667
|
+
collapse: head.includes("// whitespace: collapse")
|
|
668
|
+
}] : [];
|
|
669
|
+
});
|
|
670
|
+
const preserved = generatedModes.filter((g) => !g.collapse);
|
|
671
|
+
if ((collapseScript || collapseVite) && preserved.length) {
|
|
672
|
+
const asker = collapseScript ? `The script "${collapseScript}"` : "The Vite config";
|
|
673
|
+
const rerun = collapseScript ? `npm run ${collapseScript}` : `ts-dom gen ${options.dirs.join(" ")} --whitespace=collapse`;
|
|
674
|
+
add("formatter-whitespace", "warning", `${asker} asks for whitespace "collapse", but ${preserved.length === 1 ? "this generated file was" : `${preserved.length} generated files (this one first) were`} written with "preserve" (by a gen or check run without the flag, for example), and a run without --whitespace keeps the mode a file was last written with, so the page gets the whitespace as written instead of collapsed. Fix: regenerate with the flag once: ${rerun}.`, { file: rel(preserved[0].file) });
|
|
675
|
+
}
|
|
676
|
+
const collapsed = collapseScript !== void 0 || collapseVite || generatedModes.some((g) => g.collapse);
|
|
569
677
|
if (formatter && !collapsed && dirs.some(hasTemplates)) add("formatter-whitespace", "warning", "A formatter (oxfmt or Prettier) is set up, and templates keep their whitespace as written, so reformatting a template can change the spacing on the page. Fix: generate with whitespace \"collapse\" (ts-dom gen --whitespace=collapse, or tsDom({ whitespace: \"collapse\" })).");
|
|
570
678
|
for (const skill of listSkills(options.skillsDir ?? skillsRoot())) {
|
|
571
679
|
const copies = installedCopies(cwd, skill.name);
|
|
@@ -1030,7 +1138,7 @@ this.refs.list.items = ["a", "b"];`
|
|
|
1030
1138
|
good: `<button data-on-click="open">open</button>`
|
|
1031
1139
|
},
|
|
1032
1140
|
"stale-generated": {
|
|
1033
|
-
why: "Types come from the generated *.html.ts files. If one is missing, outdated or orphaned, tsc checks against old templates and its errors may not match the HTML. ts-dom doctor reports the same condition as a warning.",
|
|
1141
|
+
why: "Types come from the generated *.html.ts files. If one is missing, outdated or orphaned (its template was deleted, or a ts-dom-tags.ts was left in a subfolder that is no longer scanned and declares its tags a second time), tsc checks against old templates and its errors may not match the HTML. ts-dom doctor reports the same condition as a warning.",
|
|
1034
1142
|
bad: `# card.html was edited, card.html.ts was not regenerated
|
|
1035
1143
|
npx ts-dom check --no-write`,
|
|
1036
1144
|
good: `npx ts-dom gen
|
|
@@ -1100,9 +1208,11 @@ const n: number = 1;`
|
|
|
1100
1208
|
good: `npx ts-dom check --project=tsconfig.json`
|
|
1101
1209
|
},
|
|
1102
1210
|
"dir-not-found": {
|
|
1103
|
-
why: "A folder given to ts-dom check or doctor that does not exist would otherwise be skipped silently, and a clean result would only mean that nothing was looked at.",
|
|
1104
|
-
bad: `npx ts-dom check ap # the templates live in app
|
|
1105
|
-
|
|
1211
|
+
why: "A folder given to ts-dom check or doctor, or to tsDom({ dirs }) in the Vite plugin, that does not exist would otherwise be skipped silently, and a clean result would only mean that nothing was looked at. The Vite plugin resolves dirs against Vite's root, not the working directory.",
|
|
1212
|
+
bad: `npx ts-dom check ap # the templates live in app/
|
|
1213
|
+
// vite.config.ts with root: "web": tsDom({ dirs: ["web/src"] }) looks for web/web/src`,
|
|
1214
|
+
good: `npx ts-dom check app
|
|
1215
|
+
// vite.config.ts with root: "web": tsDom({ dirs: ["src"] })`
|
|
1106
1216
|
},
|
|
1107
1217
|
"tsc-not-found": {
|
|
1108
1218
|
why: "ts-dom check runs the TypeScript compiler of the project. Without it installed, nothing can be type-checked.",
|
|
@@ -1154,13 +1264,35 @@ npx ts-dom check`
|
|
|
1154
1264
|
bad: `import tpl from "./card.html";`,
|
|
1155
1265
|
good: `import tpl from "./card.html.js";`
|
|
1156
1266
|
},
|
|
1267
|
+
"framework-template": {
|
|
1268
|
+
why: "Every .html in a scanned folder is read as a Nativele template. An Angular component's templateUrl file uses Angular syntax ({{ user.name }}, *ngIf, (click)), so gen reports errors whose hints ask to rewrite a file that belongs to another framework.",
|
|
1269
|
+
bad: `// src/app/app.ts
|
|
1270
|
+
@Component({ selector: "app-root", templateUrl: "./app.html" })
|
|
1271
|
+
# package.json
|
|
1272
|
+
"build": "ts-dom gen src && ng build"`,
|
|
1273
|
+
good: `# keep the Nativele components in their own folder and scan only it
|
|
1274
|
+
"build": "ts-dom gen src/nv && ng build"`
|
|
1275
|
+
},
|
|
1276
|
+
"typecheck-before-gen": {
|
|
1277
|
+
why: "The generated *.html.ts files are not committed. A script that runs tsc (or vue-tsc, svelte-check, ng build) before ts-dom gen works on a machine that has generated them once, and fails on a clean checkout or in CI with TS2307 for every ./x.html.js import.",
|
|
1278
|
+
bad: `// package.json, with *.html.ts in .gitignore
|
|
1279
|
+
"build": "tsc && vite build"`,
|
|
1280
|
+
good: `"build": "ts-dom gen src && tsc && vite build"
|
|
1281
|
+
// or, with plain tsc, let check generate and type-check in one step:
|
|
1282
|
+
"typecheck": "ts-dom check src"`
|
|
1283
|
+
},
|
|
1157
1284
|
"vite-plugin-missing": {
|
|
1158
|
-
why: "The Vite plugin regenerates the .html.ts files while the dev server runs. Without it, edits to a template are not reflected until gen is run by hand.",
|
|
1285
|
+
why: "The Vite plugin regenerates the .html.ts files while the dev server runs. Without it, edits to a template are not reflected until gen is run by hand. A vitest.config file replaces vite.config for Vitest, so unless it has the plugin too or merges vite.config, the tests start without generated files and fail on a clean checkout.",
|
|
1159
1286
|
bad: `// vite.config.ts
|
|
1160
|
-
export default defineConfig({ plugins: [] })
|
|
1287
|
+
export default defineConfig({ plugins: [] });
|
|
1288
|
+
// vitest.config.ts next to a vite.config.ts that has tsDom()
|
|
1289
|
+
export default defineConfig({ test: { environment: "happy-dom" } });`,
|
|
1161
1290
|
good: `// vite.config.ts
|
|
1162
1291
|
import { tsDom } from "@nativele/ts-dom/vite";
|
|
1163
|
-
export default defineConfig({ plugins: [tsDom()] })
|
|
1292
|
+
export default defineConfig({ plugins: [tsDom()] });
|
|
1293
|
+
// vitest.config.ts
|
|
1294
|
+
import viteConfig from "./vite.config";
|
|
1295
|
+
export default mergeConfig(viteConfig, defineConfig({ test: { environment: "happy-dom" } }));`
|
|
1164
1296
|
},
|
|
1165
1297
|
"oxlint-plugin-missing": {
|
|
1166
1298
|
why: "The oxlint plugin runs the template rules (unused markup, data flow, accessibility) in the editor. Without it they only run from the CLI.",
|
|
@@ -1173,11 +1305,14 @@ export default defineConfig({ plugins: [tsDom()] });`
|
|
|
1173
1305
|
}`
|
|
1174
1306
|
},
|
|
1175
1307
|
"formatter-whitespace": {
|
|
1176
|
-
why: "Text whitespace in a template is kept as written. A formatter that re-indents the HTML can then change the spaces rendered on the page.",
|
|
1308
|
+
why: "Text whitespace in a template is kept as written. A formatter that re-indents the HTML can then change the spaces rendered on the page. The same warning is given when the scripts or the Vite config ask for collapse but the generated files were written with preserve: a run without --whitespace keeps the mode a file was last written with, so one run without the flag leaves the project on preserve.",
|
|
1177
1309
|
bad: `# formatter is configured, templates keep their whitespace
|
|
1178
|
-
npx ts-dom gen
|
|
1310
|
+
npx ts-dom gen
|
|
1311
|
+
# or: package.json has "gen": "ts-dom gen src --whitespace=collapse", but the files came from a plain run
|
|
1312
|
+
npx ts-dom check src`,
|
|
1179
1313
|
good: `npx ts-dom gen --whitespace=collapse
|
|
1180
|
-
// or in vite.config.ts: tsDom({ whitespace: "collapse" })
|
|
1314
|
+
// or in vite.config.ts: tsDom({ whitespace: "collapse" })
|
|
1315
|
+
# and after a plain run: npm run gen (the script with the flag) once`
|
|
1181
1316
|
},
|
|
1182
1317
|
"skill-available": {
|
|
1183
1318
|
why: "A Nativele Skill gives an agent the verified steps, limitations and checks for a tool the project already uses. This is a suggestion and never changes the exit code.",
|
|
@@ -1212,11 +1347,14 @@ Commands
|
|
|
1212
1347
|
--no-write write nothing: report stale generated files instead (for a CI job that must not
|
|
1213
1348
|
change the working tree; gen --check only checks the generated files)
|
|
1214
1349
|
--project=<path> tsconfig for tsc (default: tsc's own lookup)
|
|
1350
|
+
--include=<glob>, --exclude=<glob>, --whitespace=collapse|preserve as for gen
|
|
1215
1351
|
--a11y also check ARIA / accessibility (warnings)
|
|
1216
1352
|
--strict exit with code 1 on warnings too (errors always do)
|
|
1217
1353
|
--json print the issues as JSON instead of text
|
|
1218
1354
|
doctor [dirs...] Check the project set-up: tsconfig, template imports, generated files, the runtime package,
|
|
1219
1355
|
and the Vite, oxlint and formatter configuration (default dir: src)
|
|
1356
|
+
--include=<glob>, --exclude=<glob>, --whitespace=collapse|preserve as for gen (they decide which
|
|
1357
|
+
generated files are stale)
|
|
1220
1358
|
--json print the issues as JSON instead of text
|
|
1221
1359
|
skills List the Nativele Skills shipped with this version: task procedures for coding agents
|
|
1222
1360
|
(set-up, a complete example, known limitations, verification), with the versions verified
|
|
@@ -1363,7 +1501,7 @@ if (!dirs.length) dirs.push("src");
|
|
|
1363
1501
|
try {
|
|
1364
1502
|
if (cmd === "gen" && flags.has("--check")) {
|
|
1365
1503
|
const defs = scanDefines(dirs);
|
|
1366
|
-
const results = dirs.map((d) => checkGenerated(d, generate, defs));
|
|
1504
|
+
const results = dirs.map((d) => checkGenerated(d, generate, defs, dirs));
|
|
1367
1505
|
const stale = results.flatMap((r) => r.stale);
|
|
1368
1506
|
const errors = [...results.flatMap((r) => r.errors), ...crossDirDuplicates(dirs).map((d) => formatDiagnostics(d.file, [d]))];
|
|
1369
1507
|
if (flags.has("--json")) console.log(JSON.stringify({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as analyze, g as scanDefines, p as listHtml } from "./a11y-
|
|
1
|
+
import { a as analyze, g as scanDefines, p as listHtml } from "./a11y-CiEJj_e6.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, relative } from "node:path";
|
|
4
4
|
// src/lib/lint.ts
|
|
@@ -126,7 +126,7 @@ function lintUsage(dirs, tsFiles) {
|
|
|
126
126
|
severity: "error",
|
|
127
127
|
line: 1,
|
|
128
128
|
col: 1,
|
|
129
|
-
message: `No TypeScript file imports this template. Fix: add \`import tpl from "./${name}.js"\` and \`class X extends Component(tpl)\` next to it, or delete the file.`
|
|
129
|
+
message: `No TypeScript file imports this template. Fix: add \`import tpl from "./${name}.js"\` and \`class X extends Component(tpl)\` next to it, or delete the file; if it is not a Nativele template, this folder is not meant to be scanned: scan only the folder with the Nativele templates.`
|
|
130
130
|
});
|
|
131
131
|
continue;
|
|
132
132
|
}
|
package/dist/oxlint-plugin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as checkA11y, t as A11Y_RULES } from "./a11y-
|
|
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-
|
|
1
|
+
import { n as checkA11y, t as A11Y_RULES } from "./a11y-CiEJj_e6.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-Did4w-NL.mjs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
// src/lib/oxlint-plugin.ts
|
package/dist/vite-plugin.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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-
|
|
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-CiEJj_e6.mjs";
|
|
2
2
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
-
import { resolve, sep } from "node:path";
|
|
3
|
+
import { relative, resolve, sep } from "node:path";
|
|
4
4
|
// src/lib/vite-plugin.ts
|
|
5
5
|
const GENERATED_HEADER = "// Generated by ts-dom.";
|
|
6
6
|
const slashes = (file) => file.split(sep).join("/");
|
|
@@ -17,6 +17,7 @@ function tsDom(opts = {}) {
|
|
|
17
17
|
exclude: opts.exclude
|
|
18
18
|
};
|
|
19
19
|
const a11y = opts.a11y === true ? {} : opts.a11y || null;
|
|
20
|
+
let root = process.cwd();
|
|
20
21
|
let dirs = (opts.dirs ?? ["src"]).map((d) => resolve(d));
|
|
21
22
|
const shown = /* @__PURE__ */ new Map();
|
|
22
23
|
let defs;
|
|
@@ -83,10 +84,12 @@ function tsDom(opts = {}) {
|
|
|
83
84
|
name: "ts-dom",
|
|
84
85
|
enforce: "pre",
|
|
85
86
|
configResolved(config) {
|
|
87
|
+
root = config.root;
|
|
86
88
|
dirs = (opts.dirs ?? ["src"]).map((d) => resolve(config.root, d));
|
|
87
89
|
defs = void 0;
|
|
88
90
|
},
|
|
89
91
|
buildStart() {
|
|
92
|
+
for (const d of dirs) if (!existsSync(d)) this.warn(`${relative(process.cwd(), d) || "."}:1:1 warning [dir-not-found] The folder ${d} given in tsDom({ dirs }) does not exist, so no templates in it are generated. Fix: dirs are resolved against Vite's root (${root}), not the working directory; give the folder relative to that root, or an absolute path.`);
|
|
90
93
|
try {
|
|
91
94
|
generateDirs(existing(), generate);
|
|
92
95
|
} catch (e) {
|
package/llms.txt
CHANGED
|
@@ -101,8 +101,10 @@ In a browser without a bundler map the bare name once: `<script type="importmap"
|
|
|
101
101
|
- `data-if` content is hidden until the name is truthy (it stays in memory, so its `refs` still work); `{{ hole }}` stays empty until filled.
|
|
102
102
|
- With Vite, `package.json` needs `"type": "module"`.
|
|
103
103
|
- `slotchange()` is Light-DOM only; with `shadow: true` use the native `slotchange` event.
|
|
104
|
+
- A Light DOM component builds its content when connected (a `shadow: true` one in the constructor), so a new Light element has no children, `refs` or `text` until it is appended.
|
|
105
|
+
- Inside a framework (React, Vue, Svelte, ...), children the framework later inserts, removes or reorders in a Light DOM host with slots break or land in the wrong slot: use `shadow: true`, or one stable element per slot.
|
|
104
106
|
|
|
105
107
|
## Docs
|
|
106
108
|
- [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), [architecture](docs/architecture.md), [setup, checks and lint](docs/tooling.md), [performance](docs/performance.md), [design](docs/design.md), [developing Nativele](docs/contributing.md).
|
|
109
|
+
- Guides (English): [templates](docs/templates.md), [components and data flow](docs/components.md), [recipes](docs/recipes.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
110
|
- `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.
|
|
3
|
+
"version": "0.5.4",
|
|
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
|
@@ -22,7 +22,7 @@ skills/
|
|
|
22
22
|
│ └── example/ # Official only: the project the Skill is verified with
|
|
23
23
|
│ ├── package.json # exact versions of every tool the Skill names
|
|
24
24
|
│ ├── ... # the minimal app the SKILL.md example comes from
|
|
25
|
-
│ └── verify.mjs #
|
|
25
|
+
│ └── verify.mjs # browser checks run by `npm run verify` (or the project's own tests, see below)
|
|
26
26
|
└── ...
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -90,7 +90,7 @@ None of this adds a runtime dependency: listing and copying read the Markdown fi
|
|
|
90
90
|
|
|
91
91
|
## Verification of Official Skills
|
|
92
92
|
|
|
93
|
-
- `skills/<name>/example/package.json` pins every tool version the Skill names, depends on the library through `file:../../..` (the library in the same repository), and has `npm run verify`, which runs `ts-dom check`, `ts-dom doctor`, `tsc`, the build, and a
|
|
93
|
+
- `skills/<name>/example/package.json` pins every tool version the Skill names, depends on the library through `file:../../..` (the library in the same repository), and has `npm run verify`, which runs `ts-dom check`, `ts-dom doctor`, `tsc`, the build, and a real-browser test of what the Skill promises: usually a `verify.mjs` script; an example whose Skill sets up tests (`nativele-testing`, `nativele-architecture`) runs its own Vitest and Playwright Test suites instead.
|
|
94
94
|
- `nativele-core` has no example of its own: the library's CI, which checks every example in the repository, verifies it.
|
|
95
95
|
- The workflow `.github/workflows/skills.yml` runs `npm run verify` for each Official Skill when an `example/` changes, when the library's runtime changes, and once a month, so a third-party release that breaks a Skill is found. A change to `SKILL.md` runs only `.github/workflows/skills-format.yml`, which checks every `SKILL.md` against this specification. Neither is a required check for `main`.
|
|
96
96
|
- The monthly run opens nothing by itself; a failure is reported in the workflow run, and the Skill is fixed or moved to Experimental.
|
|
@@ -274,9 +274,7 @@ export class App {
|
|
|
274
274
|
1. **`CUSTOM_ELEMENTS_SCHEMA` turns off binding checks for unknown elements.** With it, Angular's compiler accepts any property on any hyphenated tag: `[item]="42"`, `[count]="'x'"` and `[nope]="1"` on `<info-card>` all build without an error. Angular has no mechanism to type-check bindings on custom elements, so keep the values typed in the Angular class (`signal<Item>(...)`) and check the Nativele side with `ts-dom check src/nv`.
|
|
275
275
|
2. **`$event` is typed `Event`.** Angular's template type checker types `$event` of a DOM event it does not know as `Event`, so `select($event.detail)` or a handler parameter typed `CustomEvent<...>` fails with `TS2339: Property 'detail' does not exist on type 'Event'` or `TS2345: Argument of type 'Event' is not assignable to parameter of type 'CustomEvent<...>'`. Take `Event` and cast it with `EmitEvent<Component, 'event-name'>` from `@nativele/ts-dom`; a wrong event name there becomes `never` and fails with `Property 'detail' does not exist on type 'never'`. Nothing checks that the cast names the element the listener is on.
|
|
276
276
|
3. **Bind properties, not attributes, for objects.** `[attr.item]="item()"` writes the attribute `item="[object Object]"` and the prop stays `null`. `[attr.count]` works for a number prop (the attribute text is converted), but `[count]` is simpler.
|
|
277
|
-
4. **Light DOM slots and Angular-managed children do not mix
|
|
278
|
-
- `shadow: true` on the component; `@if` and `@for` children can then change freely;
|
|
279
|
-
- one stable element per slot that Angular never removes, with `@if`/`@for` inside it (the `light-card` above). A stable but empty wrapper still fills its slot, so the slot's fallback content does not show; when the fallback matters, use `shadow: true`.
|
|
277
|
+
4. **Light DOM slots and Angular-managed children do not mix** (https://github.com/Pixmi/nativele/blob/main/docs/components.md#slots-and-children-managed-by-a-framework). When `@if` or `@for` sit directly inside a Light DOM host with slots, Angular still adds and removes nodes, but a node it re-inserts lands in the wrong place: a `<span slot="title">` brought back by `@if` ends up in the default slot while the title shows its fallback, and `assignedNodes()` stops matching the page. Angular reports no error. Use `shadow: true`, or one stable element per slot that Angular never removes, with `@if`/`@for` inside it (the `light-card` above).
|
|
280
278
|
5. **Zoneless rendering is scheduled.** In the CLI's default zoneless app, Angular updates the DOM after a click in a later task, so code that calls `button.click()` and reads the DOM right away sees the old values; with `zone.js` the DOM is already updated when `click()` returns. A test that reads the DOM right after dispatching an event must wait (Playwright: wait for the expected text; TestBed: `await fixture.whenStable()`). The Nativele component itself renders synchronously as soon as Angular sets the property.
|
|
281
279
|
6. **State changed outside Angular's listeners.** In a zoneless app, a plain field set from a Nativele store subscription (`session.subscribe(...)` in an Angular class) or a `setTimeout` does not re-render the Angular view; put the value in a signal. Nativele components that `bind` the store update themselves either way.
|
|
282
280
|
7. **`ng serve` started before the modules were generated stays broken.** It fails with `Could not resolve "./info-card.html.js"` and does not rebuild when `ts-dom gen` creates the files later. Use `npm start` (step 4 runs `gen` first), or restart `ng serve`, or save one of the component `.ts` files.
|
|
@@ -167,7 +167,7 @@ Verified on 2026-11-05 with Node 24 and Google Chrome 148, with the example proj
|
|
|
167
167
|
npm pkg set scripts.verify="npm run build && ts-dom doctor src && npm test && npm run test:browser && npm run test:e2e"
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
-
Add `test-results` and `playwright-report` to `.gitignore`, next to `*.html.ts` and `ts-dom-tags.ts`.
|
|
170
|
+
The build script is for a project whose type check is plain `tsc`: `ts-dom check` generates the files and type-checks. In a framework project, keep the framework's type check (`tsc -b`, `vue-tsc -b`, `svelte-check`) and put `ts-dom gen src &&` in front of it instead (the framework's Skill shows the script). Add `test-results` and `playwright-report` to `.gitignore`, next to `*.html.ts` and `ts-dom-tags.ts`.
|
|
171
171
|
|
|
172
172
|
## Example
|
|
173
173
|
|
|
@@ -16,7 +16,7 @@ metadata:
|
|
|
16
16
|
|
|
17
17
|
- The app is built with a UI framework and wants to use components written with Nativele (`@nativele/ts-dom`), and no Skill exists for that framework yet. React, Vue, Svelte and Angular have their own (`nativele-react`, `nativele-vue`, `nativele-svelte`, `nativele-angular`); `npx ts-dom skills` lists every Skill.
|
|
18
18
|
- It is a procedure for finding out how the framework treats custom elements and connecting it correctly. It does not cover setting the framework itself up: start from the app as it is, with its own type checker and build.
|
|
19
|
-
- A framework with its own file format type-checks it with its own tool (Vue: `vue-tsc`). `ts-dom check` runs `vue-tsc` instead of `tsc` when the project's `package.json` lists it; for other formats see Troubleshooting. A framework that uses TSX (Preact, Solid) is checked by plain `tsc`, and `ts-dom check` reports its errors too.
|
|
19
|
+
- A framework with its own file format type-checks it with its own tool (Vue: `vue-tsc`). `ts-dom check` runs `vue-tsc` instead of `tsc` when the project's `package.json` lists it and it resolves from the project folder; for other formats see Troubleshooting. A framework that uses TSX (Preact, Solid) is checked by plain `tsc`, and `ts-dom check` reports its errors too.
|
|
20
20
|
- Not for writing the Nativele components themselves: use `nativele-core` (`npx ts-dom skills add nativele-core`).
|
|
21
21
|
|
|
22
22
|
Nativele components are native custom elements. The framework renders their tags like any other tag; nothing wraps them. Every rule below follows from that.
|
|
@@ -56,8 +56,8 @@ Verified on 2026-11-05 in Google Chrome 148, starting from `create-vite` 9.3.4 w
|
|
|
56
56
|
|
|
57
57
|
Many frameworks decide between property and attribute by checking whether the element already has the property, which is only true after `define()` has run.
|
|
58
58
|
|
|
59
|
-
5. Give the framework's template type checker the Nativele tags. `ts-dom gen` writes `ts-dom-tags.ts`, which adds every exported component to `HTMLElementTagNameMap`;
|
|
60
|
-
6. The generated files are not committed, so on a clean checkout a build script that runs the type checker before the bundler (the `preact-ts` template's `"build": "tsc -b && vite build"`) fails with `TS2307: Cannot find module './x.html.js'`. Put `ts-dom gen src/nv &&` in front: `"build": "ts-dom gen src/nv && tsc -b && vite build"`. The dev server needs no change: the Vite plugin generates the files when it starts.
|
|
59
|
+
5. Give the framework's template type checker the Nativele tags. `ts-dom gen` writes `ts-dom-tags.ts`, which adds every exported component to `HTMLElementTagNameMap`; the element type members a declaration reads (`props` and `"~emits"`) are in https://github.com/Pixmi/nativele/blob/main/docs/components.md#element-types-for-framework-declarations. Build the framework's tag declaration from these, so new components need no edit; "In Preact (verified)" below has the one for Preact's JSX, `nativele-react` the one for React and `nativele-vue` the Vue one. Check how the framework names an event listener in its types: Preact and React type `item-select` as the prop `onitem-select`, Vue types `@item-select` as `onItemSelect`. If the framework has no typed templates, skip this step.
|
|
60
|
+
6. The generated files are not committed, so on a clean checkout a build script that runs the type checker before the bundler (the `preact-ts` template's `"build": "tsc -b && vite build"`) fails with `TS2307: Cannot find module './x.html.js'`. Put `ts-dom gen src/nv &&` in front: `"build": "ts-dom gen src/nv && tsc -b && vite build"`. Do the same in front of whatever type check the framework's template runs (`vue-tsc -b`, `svelte-check`). Only a project whose type check is plain `tsc` replaces it with `ts-dom check`, which generates and type-checks: `"build": "ts-dom check src/nv && vite build"`, as `nativele-vite` sets it up. The dev server needs no change: the Vite plugin generates the files when it starts.
|
|
61
61
|
|
|
62
62
|
## Example
|
|
63
63
|
|
|
@@ -138,9 +138,7 @@ A template ref is typed from the same map: `useRef<HTMLElementTagNameMap["nv-inf
|
|
|
138
138
|
|
|
139
139
|
1. **Define before render.** Import the component modules before the framework mounts (step 4). A framework that sees an undefined element may write an attribute instead of a property: in Preact, `item={item}` becomes the attribute `item="[object Object]"`, the component gets `null` after `define()`, and the attribute stays even after a later new object is set as a property. A property set before `define()` is kept: Nativele takes it over when the element is upgraded. So when a module really is loaded later, set object props through the framework's property syntax, or, where it has none (Preact), from a ref: `ref={(el) => { if (el) el.item = item; }}`, and leave `item={item}` out of that tag, or Preact also writes the attribute.
|
|
140
140
|
2. **Props.** A prop whose default is a string, number or boolean can also come from an attribute, with camelCase names in kebab-case (`maxLength` from `max-length`, a boolean `true` when the attribute is present). Arrays and objects can only be set as properties. Pass a new object when data changes: props are compared by reference.
|
|
141
|
-
3. **Light DOM slots and framework-managed children do not mix
|
|
142
|
-
- `shadow: true` on the component, so the browser assigns slots natively and the children can change freely;
|
|
143
|
-
- one stable element per slot that the framework never removes, with the changing content inside it (`<div slot="title">...</div>`, `<div>...</div>` for the default slot). A stable but empty wrapper still fills its slot, so the slot's fallback content does not show; when the fallback matters, use `shadow: true`.
|
|
141
|
+
3. **Light DOM slots and framework-managed children do not mix** (https://github.com/Pixmi/nativele/blob/main/docs/components.md#slots-and-children-managed-by-a-framework): a framework that inserts, removes or reorders the children of a Light DOM host with slots can fail or leave nodes in the wrong slot. In Preact, toggling or reordering such children throws `NotFoundError: Failed to execute 'moveBefore' on 'Element'` (or `'insertBefore' on 'Node'`) and the slot loses its children. Use `shadow: true`, or one stable element per slot that the framework never removes, with the changing content inside it.
|
|
144
142
|
4. **Events do not bubble.** `emit` dispatches a `CustomEvent` on the component that does not bubble, so the listener must be on that element, not on an ancestor. The event data is `event.detail`.
|
|
145
143
|
5. **Lifecycle.** `connected()` runs each time the element is inserted and `disconnected()` each time it is removed, and `bind` subscriptions end on removal and start again on insertion. A framework that moves an element (reordering a keyed list) with `insertBefore` runs both; one that moves it with `moveBefore` keeps it connected, so neither runs and its subscriptions continue. Preact 11 uses `moveBefore` where the browser has it (Chrome does) and `insertBefore` elsewhere. Either way the moved element keeps its props and the children in its Light DOM slots. So subscribe in `connected()`, and do not rely on a move running it.
|
|
146
144
|
6. **The framework's templates are checked by the framework's type checker**, with the declaration from step 5, not by the Nativele rules. How much it checks depends on the framework: with the Preact declaration, `tsc` reports a wrong prop type, a wrong object shape, a wrong use of `e.detail`, an unknown prop name, an event prop in the wrong case (`onItemSelect`) and an unknown tag, but not a hyphenated attribute name (`selected-item`), which TypeScript never checks in JSX; `vue-tsc` reports a wrong prop type or event detail, but not an unknown prop name.
|
|
@@ -162,18 +162,14 @@ define("shadow-card", ShadowCard);
|
|
|
162
162
|
|
|
163
163
|
1. **Define before render.** Import every Nativele module before React first renders its tag (step 6). Otherwise props become attributes and object props are lost.
|
|
164
164
|
2. **Event prop names.** Only `on` plus the exact event name works (`onitem-select`); `onItemSelect` and `onItem-select` never fire. Nativele events do not bubble by default, so put the handler on the element that emits. A ref with `addEventListener` in `useEffect` also works (remove it in the cleanup).
|
|
165
|
-
3. **Light DOM slots and React children do not mix
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
</light-card>
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
A stable but empty wrapper still fills its slot, so the slot's fallback content does not show; when the fallback matters, use `shadow: true`.
|
|
165
|
+
3. **Light DOM slots and React children do not mix** (https://github.com/Pixmi/nativele/blob/main/docs/components.md#slots-and-children-managed-by-a-framework). When React inserts, removes or reorders the children of a Light DOM host with slots, it fails with `insertBefore`/`removeChild` errors that unmount the root, or leaves nodes outside the slot. Changing text in place and appending at the end happen to work, but do not rely on that. Use `shadow: true`, or one stable React element per slot and let React change only what is inside it:
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
<light-card>
|
|
169
|
+
<div slot="title">{showTitle && <span>{title}</span>}</div>
|
|
170
|
+
<div>{rows.map((r) => <p key={r.id}>{r.text}</p>)}</div>
|
|
171
|
+
</light-card>
|
|
172
|
+
```
|
|
177
173
|
4. **New objects for changed props.** A Nativele component compares props by reference; pass a new object when the data changes (React state updates usually do).
|
|
178
174
|
5. **TSX types need the declaration** in step 7; without it, every Nativele tag is `TS2339: Property 'info-card' does not exist on type 'JSX.IntrinsicElements'`. Check it with a `.tsx` that has a wrong prop under `// @ts-expect-error`, because `skipLibCheck` can hide mistakes inside the declaration itself. Keep these cases in a `.tsx` file of their own that the app never imports (the example uses `src/types-check.tsx`).
|
|
179
175
|
6. Not verified: server-side rendering, form controls inside Nativele components submitting with a React form, and React versions other than 19.3.
|
|
@@ -196,18 +196,14 @@ Used from Svelte:
|
|
|
196
196
|
|
|
197
197
|
1. **Define before mount.** Import the component modules before `mount` (step 6). Svelte 5 decides per value: when the element is defined, it sets a property the element has; when it is not defined yet (for example a module loaded later with `import()`), it sets objects and arrays as properties and writes every other value as an attribute string. Nativele reads both when the element is upgraded, which was verified for string, number and boolean props with one-word names (`count={0}` becomes `count="0"`, `false` becomes `open="false"`, both arrive with the right type). Other cases were not verified, so define first.
|
|
198
198
|
2. **Event attribute names.** Only `on` plus the exact event name works (`onitem-select`). `onItemSelect` listens to an event named `ItemSelect` and never fires; with the declaration it is also a `svelte-check` error. The legacy directive `on:item-select` fires too, but only use the event attributes: in a runes component `on:` is deprecated (compiler warning `event_directive_deprecated`), a component cannot mix it with event attributes (compile error `mixed_event_handler_syntaxes`), and the declaration in step 7 does not type it. Nativele events do not bubble, so put the handler on the element that emits.
|
|
199
|
-
3. **Light DOM slots and Svelte-managed children do not mix
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
</light-card>
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
A stable but empty wrapper still fills its slot, so the slot's fallback content does not show; when the fallback matters, use `shadow: true`.
|
|
199
|
+
3. **Light DOM slots and Svelte-managed children do not mix** (https://github.com/Pixmi/nativele/blob/main/docs/components.md#slots-and-children-managed-by-a-framework). Nativele moves the comment nodes Svelte uses as anchors into the slot positions together with the children. Svelte does not throw, but it goes wrong silently: an `{#if}` that adds `<span slot="title">` back inserts it next to its anchor, which now sits in the default slot, so the title slot shows its fallback; and `assignedNodes()` stops matching what is on the page. Keyed `{#each}` reordering happened to look right, but do not rely on it. Use `shadow: true`, or one stable element per slot and let Svelte change only what is inside it:
|
|
200
|
+
|
|
201
|
+
```svelte
|
|
202
|
+
<light-card>
|
|
203
|
+
<div slot="title">{#if showTitle}<span>Title</span>{/if}</div>
|
|
204
|
+
<div>{#each rows as r (r)}<p>{r}</p>{/each}</div>
|
|
205
|
+
</light-card>
|
|
206
|
+
```
|
|
211
207
|
4. **New objects for changed props.** A Nativele component compares props by reference. Changing a `$state` object in place (`item.name = "kiwi"`) does not re-render it; assign a new object (`item = { ...item, name: "kiwi" }`).
|
|
212
208
|
5. **Write closing tags.** `<info-card ... />` works, but the Svelte compiler warns `element_invalid_self_closing_tag` (shown in the dev server's terminal); write `<info-card ...></info-card>`.
|
|
213
209
|
6. **Template types need the declaration** in step 7. Without it, Svelte accepts any attribute on a hyphenated tag, and an inline handler's parameter is `any` (`Parameter 'e' implicitly has an 'any' type` under `strict`). With it, `svelte-check` reports a wrong prop type, an unknown prop name, a wrong `e.detail` field and a camelCase handler name. `<!-- @ts-expect-error -->` in markup does not silence an error, so keep the expected-error cases in a `.ts` file (step 8). To type an element reference, use the tag map: `let card: HTMLElementTagNameMap["info-card"] | undefined = $state()` with `bind:this={card}`.
|
|
@@ -69,7 +69,7 @@ Verified on 2026-11-05 with Node 24 and Google Chrome 148, starting from `create
|
|
|
69
69
|
npm pkg set scripts.test="vitest run --project unit"
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
`ts-dom check src/nv` is what reports type errors in the tests and the templates. In a project whose type check is plain `tsc`, the build script `ts-dom check src/nv && vite build` (as `nativele-vite` sets it up) runs it; in a framework project, whose build puts `ts-dom gen src/nv &&` in front of its own type check (`tsc -b`, `vue-tsc -b`, `svelte-check`), run `ts-dom check src/nv` before or after the tests.
|
|
73
73
|
|
|
74
74
|
5. For the real-browser tests, install the Vitest provider and Playwright, and add a second project. It drives the Chrome already installed on the machine (`CHROME_BIN`, or `/usr/bin/google-chrome`, which GitHub's `ubuntu-latest` runners have), so nothing is downloaded and `npx playwright install` is not needed:
|
|
75
75
|
|
|
@@ -17,7 +17,7 @@ metadata:
|
|
|
17
17
|
- A Vite app with no UI framework (for example one started with `npm create vite@latest -- --template vanilla-ts`) that adds components written with Nativele (`@nativele/ts-dom`).
|
|
18
18
|
- Any Vite project with Nativele whose set-up goes beyond the defaults: `root` in a sub-folder, several HTML pages, Vite+, or questions about what `vite dev` does when a template changes.
|
|
19
19
|
- Signals: `vite` or `vite-plus` and `@nativele/ts-dom` in `package.json`, a `vite.config.ts`.
|
|
20
|
-
- With
|
|
20
|
+
- With a UI framework, follow its Skill: `nativele-react`, `nativele-vue`, `nativele-svelte`, `nativele-angular`, or `nativele-integration` for any other. The parts about `dirs` and `root` apply there too; the build script in step 5 does not: it is for a project whose type check is plain `tsc`, while a framework project keeps its own type check (`tsc -b`, `vue-tsc -b`, `svelte-check`) and puts `ts-dom gen <folder> &&` in front of that script. The hot-update behavior below was verified without a framework.
|
|
21
21
|
- Not for writing the components themselves (`npx ts-dom skills add nativele-core`), and not for setting Vite up.
|
|
22
22
|
|
|
23
23
|
## Versions
|
|
@@ -219,7 +219,7 @@ define("nv-light-card", LightCard);
|
|
|
219
219
|
3. **camelCase prop names in templates.** Write Nativele props as they are declared: `:selectedItem="item"`, `:maxCount="3"`. Vue sets a kebab-case binding (`:selected-item`) as an attribute, because the element has no property of that name: a number or string still converts from the attribute, an object or array arrives as `"[object Object]"` and the prop stays `null`. `vue-tsc` does not report this (limitation 8).
|
|
220
220
|
4. **Exact event names.** On a custom element, Vue listens to the event name exactly as written after `@`: `@item-select` hears `item-select`, `@pickedOne` hears `pickedOne`, and `@itemSelect` never hears `item-select`. Name events in kebab-case and write that name. Nativele events do not bubble by default, so the listener goes on the element that emits.
|
|
221
221
|
5. **`v-model` does not work on a Nativele component.** It compiles, `vue-tsc` accepts it and Vue logs no warning; it writes the first value into the element, but typing never updates the Vue state. Use a prop and an event, as `nv-text-field` above: `:value="text" @value-change="(e) => (text = e.detail.value)"`. In the component, compare before writing the input's value, so the cursor does not jump (see "A Small Reminder About Form Inputs" in the components guide).
|
|
222
|
-
6. **Light DOM slots and Vue children do not mix
|
|
222
|
+
6. **Light DOM slots and Vue children do not mix** (https://github.com/Pixmi/nativele/blob/main/docs/components.md#slots-and-children-managed-by-a-framework). When Vue inserts, removes or reorders the children of a Light DOM host with slots (`v-if`, `v-for`), the update throws (`Cannot read properties of null (reading 'insertBefore')`, `Cannot set properties of null (setting '__vnode')`) and the slots show the wrong children. Use `shadow: true`, or one stable element per slot as in the example.
|
|
223
223
|
7. **Lifecycle follows the DOM.** When a keyed `v-for` reorders Nativele elements, Vue moves the same element with `insertBefore`: it runs `disconnected()` then `connected()`, keeps its props and its Light DOM slot children, and `bind` subscriptions end and start again. `<KeepAlive>` deactivation takes the element out of the document (`disconnected()`, no store updates while inactive); activation puts the same element back (`connected()`, and `bind` calls back at once with the current value). `v-if` removal runs `disconnected()` and ends the subscriptions. So subscribe in `connected()`, as the lists guide says for moved items.
|
|
224
224
|
8. **What `vue-tsc` checks.** With the declaration from step 7, it reports a wrong prop type (`count="1"` on a number prop), a wrong object shape and a wrong use of `e.detail`. It does not report an unknown prop name, an unknown tag with the prefix, a kebab-case prop name, `v-model`, or an event name whose case differs from the declared one. In a template, mark an expected error with `<!-- @vue-expect-error -->`; `// @ts-expect-error` is rendered as text and suppresses nothing.
|
|
225
225
|
9. Not verified: server-side rendering and hydration, Nuxt, `<Transition>` around Nativele elements, and Vue versions other than 3.5.
|
|
@@ -235,7 +235,7 @@ npx vue-tsc -b # expect: no output
|
|
|
235
235
|
npx vite build # expect: built
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
-
`ts-dom check` runs `vue-tsc` instead of `tsc` when `vue-tsc`
|
|
238
|
+
`ts-dom check` runs `vue-tsc` instead of `tsc` when the project's `package.json` lists `vue-tsc` (in `dependencies` or `devDependencies`) and it resolves from the project folder, so it also reports type errors in `.vue` files.
|
|
239
239
|
|
|
240
240
|
Prove that the declaration types props: keep a `.vue` file that the app never imports (the example uses `src/TypesCheck.vue`) with one wrong case per kind under `<!-- @vue-expect-error -->`:
|
|
241
241
|
|