@nativele/ts-dom 0.4.6 → 0.5.1
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 +59 -0
- package/README.md +46 -891
- package/README.zh-TW.md +46 -896
- package/dist/{a11y-wWL9rie7.mjs → a11y-CV3nBgBn.mjs} +1 -1
- package/dist/cli.mjs +227 -22
- package/dist/index.d.mts +16 -6
- package/dist/index.mjs +4 -1
- package/dist/{lint-CP_i4VPA.mjs → lint-DfUAh7t3.mjs} +17 -2
- package/dist/oxlint-plugin.mjs +2 -2
- package/dist/vite-plugin.mjs +1 -1
- package/llms.txt +12 -7
- package/package.json +4 -2
- package/skills/README.md +135 -0
- package/skills/_template/SKILL.md +57 -0
- package/skills/nativele-angular/SKILL.md +339 -0
- package/skills/nativele-core/SKILL.md +177 -0
- package/skills/nativele-integration/SKILL.md +125 -0
- package/skills/nativele-react/SKILL.md +213 -0
- package/skills/nativele-svelte/SKILL.md +259 -0
- package/skills/nativele-vite/SKILL.md +197 -0
- package/skills/nativele-vue/SKILL.md +279 -0
|
@@ -482,7 +482,7 @@ function renderTags(dir) {
|
|
|
482
482
|
const entries = [...seen.values()].map((d) => ` ${JSON.stringify(d.tag)}: ${alias(d)};`);
|
|
483
483
|
return {
|
|
484
484
|
out,
|
|
485
|
-
next: `${TAGS_HEADER}\n${imports.join("\n")}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n${entries.join("\n")}\n }\n}\n
|
|
485
|
+
next: `${TAGS_HEADER}\n${imports.join("\n")}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n${entries.join("\n")}\n }\n}\n`
|
|
486
486
|
};
|
|
487
487
|
}
|
|
488
488
|
/**
|
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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-
|
|
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 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";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { cpSync, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
6
|
import { dirname, join, relative, resolve } from "node:path";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
8
9
|
// src/lib/tsconfig.ts
|
|
9
10
|
/** tsconfig is JSONC: drop comments and trailing commas before parsing. */
|
|
10
11
|
function parseJsonc(text) {
|
|
@@ -45,7 +46,7 @@ function readTsConfig(file, depth = 0) {
|
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
const own = json.include;
|
|
48
|
-
const include = own ? own.map((p) => resolve(dirname(file), p).split("\\").join("/")) : merged.include;
|
|
49
|
+
const include = own ? own.map((p) => resolve(dirname(file), p).split("\\").join("/")) : merged.include ?? (Array.isArray(json.files) ? [] : void 0);
|
|
49
50
|
return {
|
|
50
51
|
compilerOptions: {
|
|
51
52
|
...merged.compilerOptions,
|
|
@@ -212,13 +213,25 @@ function mapTscErrors(errors, cwd) {
|
|
|
212
213
|
return out.filter((i) => i.rule !== "tsc" || !followsBroken(i.message));
|
|
213
214
|
}
|
|
214
215
|
function findTsc(cwd) {
|
|
216
|
+
const req = createRequire(join(cwd, "package.json"));
|
|
217
|
+
let deps = {};
|
|
215
218
|
try {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
219
|
+
const json = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
220
|
+
deps = {
|
|
221
|
+
...json.dependencies,
|
|
222
|
+
...json.devDependencies
|
|
223
|
+
};
|
|
224
|
+
} catch {}
|
|
225
|
+
const candidates = "vue-tsc" in deps ? [["vue-tsc", "vue-tsc"], ["typescript", "tsc"]] : [["typescript", "tsc"]];
|
|
226
|
+
for (const [pkg, name] of candidates) try {
|
|
227
|
+
const pkgPath = req.resolve(`${pkg}/package.json`);
|
|
228
|
+
const bin = JSON.parse(readFileSync(pkgPath, "utf8")).bin?.[name];
|
|
229
|
+
if (bin) return {
|
|
230
|
+
name,
|
|
231
|
+
bin: resolve(dirname(pkgPath), bin)
|
|
232
|
+
};
|
|
233
|
+
} catch {}
|
|
234
|
+
return null;
|
|
222
235
|
}
|
|
223
236
|
/**
|
|
224
237
|
* Everything an agent needs in one pass: template diagnostics, generated files, TypeScript errors mapped back to the templates, and the lint rules.
|
|
@@ -287,7 +300,7 @@ function runCheck(options) {
|
|
|
287
300
|
const parsed = [];
|
|
288
301
|
for (const extra of runs) {
|
|
289
302
|
const run = spawnSync(process.execPath, [
|
|
290
|
-
tsc,
|
|
303
|
+
tsc.bin,
|
|
291
304
|
"--noEmit",
|
|
292
305
|
"--pretty",
|
|
293
306
|
"false",
|
|
@@ -308,7 +321,7 @@ function runCheck(options) {
|
|
|
308
321
|
col: 1,
|
|
309
322
|
rule: "tsc-failed",
|
|
310
323
|
severity: "error",
|
|
311
|
-
message:
|
|
324
|
+
message: `${tsc.name} did not check the types: ${detail} Fix: make \`npx ${tsc.name} --noEmit\` run in this folder (a tsconfig.json, or --project=<path>).`
|
|
312
325
|
});
|
|
313
326
|
}
|
|
314
327
|
}
|
|
@@ -333,6 +346,94 @@ function runCheck(options) {
|
|
|
333
346
|
return !seen.has(k) && seen.add(k) !== void 0;
|
|
334
347
|
}).sort((x, y) => x.file.localeCompare(y.file) || x.line - y.line || x.col - y.col);
|
|
335
348
|
}
|
|
349
|
+
// src/lib/skills.ts
|
|
350
|
+
const STALE_DAYS = 183;
|
|
351
|
+
/** The skills folder shipped with this package (next to its package.json). */
|
|
352
|
+
function skillsRoot() {
|
|
353
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
354
|
+
while (!existsSync(join(dir, "package.json")) || !existsSync(join(dir, "skills"))) {
|
|
355
|
+
const up = dirname(dir);
|
|
356
|
+
if (up === dir) return join(dirname(fileURLToPath(import.meta.url)), "skills");
|
|
357
|
+
dir = up;
|
|
358
|
+
}
|
|
359
|
+
return join(dir, "skills");
|
|
360
|
+
}
|
|
361
|
+
const unquote = (v) => v.trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
362
|
+
/** Read the frontmatter fields Nativele uses; returns null when the file has none. */
|
|
363
|
+
function parseSkill(text, dir) {
|
|
364
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
365
|
+
if (!m) return null;
|
|
366
|
+
const top = {};
|
|
367
|
+
const meta = {};
|
|
368
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
369
|
+
const kv = /^(\s*)([\w-]+):\s?(.*)$/.exec(line);
|
|
370
|
+
if (!kv) continue;
|
|
371
|
+
const value = unquote(kv[3].replace(/\s+#.*$/, ""));
|
|
372
|
+
(kv[1] ? meta : top)[kv[2]] = value;
|
|
373
|
+
}
|
|
374
|
+
if (!top.name) return null;
|
|
375
|
+
const tier = meta.tier === "official" || meta.tier === "community" ? meta.tier : "experimental";
|
|
376
|
+
return {
|
|
377
|
+
name: top.name,
|
|
378
|
+
description: top.description ?? "",
|
|
379
|
+
tier,
|
|
380
|
+
nativele: meta.nativele ?? "",
|
|
381
|
+
verified: meta.verified ?? "",
|
|
382
|
+
tools: meta.tools ?? "",
|
|
383
|
+
author: meta.author ?? "",
|
|
384
|
+
detects: (meta.detects ?? "").split(/[\s,]+/).filter(Boolean),
|
|
385
|
+
dir
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function listSkills(root = skillsRoot()) {
|
|
389
|
+
if (!existsSync(root)) return [];
|
|
390
|
+
return readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_") && existsSync(join(root, e.name, "SKILL.md"))).map((e) => parseSkill(readFileSync(join(root, e.name, "SKILL.md"), "utf8"), join(root, e.name))).filter((s) => s !== null).sort((a, b) => a.name.localeCompare(b.name));
|
|
391
|
+
}
|
|
392
|
+
const parts = (v) => v.split(/[.-]/).slice(0, 3).map((n) => Number.parseInt(n, 10) || 0);
|
|
393
|
+
const compare = (a, b) => {
|
|
394
|
+
const x = parts(a);
|
|
395
|
+
const y = parts(b);
|
|
396
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i];
|
|
397
|
+
return 0;
|
|
398
|
+
};
|
|
399
|
+
/** Whether `version` satisfies a range of space-separated comparators such as ">=0.5.0 <0.6.0". */
|
|
400
|
+
function inRange(version, range) {
|
|
401
|
+
return range.split(/\s+/).filter(Boolean).every((c) => {
|
|
402
|
+
const m = /^(>=|<=|>|<|=)?v?(\d[\w.-]*)$/.exec(c);
|
|
403
|
+
if (!m) return false;
|
|
404
|
+
const d = compare(version, m[2]);
|
|
405
|
+
return m[1] === ">=" ? d >= 0 : m[1] === "<=" ? d <= 0 : m[1] === ">" ? d > 0 : m[1] === "<" ? d < 0 : d === 0;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/** Why a Skill should not be trusted as verified for this version, or null when it is current. */
|
|
409
|
+
function staleReason(skill, version, today = /* @__PURE__ */ new Date()) {
|
|
410
|
+
if (!skill.nativele || !inRange(version, skill.nativele)) return `verified for Nativele ${skill.nativele || "(no range)"}, installed ${version}`;
|
|
411
|
+
const verified = Date.parse(skill.verified);
|
|
412
|
+
if (Number.isNaN(verified)) return "no verification date";
|
|
413
|
+
if ((today.getTime() - verified) / 864e5 > STALE_DAYS) return `last verified ${skill.verified}, more than six months ago`;
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
/** The folder a project keeps its skills in: .claude/skills when the project has .claude, otherwise .agents/skills. */
|
|
417
|
+
function skillsTarget(cwd, to) {
|
|
418
|
+
if (to) return resolve(cwd, to);
|
|
419
|
+
return existsSync(join(cwd, ".claude")) ? join(cwd, ".claude", "skills") : join(cwd, ".agents", "skills");
|
|
420
|
+
}
|
|
421
|
+
/** Every place a project may already hold a copy of a Skill. */
|
|
422
|
+
function installedCopies(cwd, name) {
|
|
423
|
+
return [join(cwd, ".claude", "skills", name), join(cwd, ".agents", "skills", name)].filter((d) => existsSync(join(d, "SKILL.md")));
|
|
424
|
+
}
|
|
425
|
+
/** Copy a Skill (its files, not its example project) into `target/<name>`; returns the folder written. */
|
|
426
|
+
function addSkill(skill, target) {
|
|
427
|
+
const dest = join(target, skill.name);
|
|
428
|
+
cpSync(skill.dir, dest, {
|
|
429
|
+
recursive: true,
|
|
430
|
+
filter: (src) => !src.startsWith(join(skill.dir, "example"))
|
|
431
|
+
});
|
|
432
|
+
return dest;
|
|
433
|
+
}
|
|
434
|
+
function libraryVersion(root = skillsRoot()) {
|
|
435
|
+
return JSON.parse(readFileSync(join(dirname(root), "package.json"), "utf8")).version ?? "0.0.0";
|
|
436
|
+
}
|
|
336
437
|
// src/lib/doctor.ts
|
|
337
438
|
const DOCTOR_RULES = {
|
|
338
439
|
"dir-not-found": "a folder given on the command line does not exist, so nothing in it was checked",
|
|
@@ -346,7 +447,9 @@ const DOCTOR_RULES = {
|
|
|
346
447
|
"stale-generated": "a generated file is missing, outdated or orphaned",
|
|
347
448
|
"vite-plugin-missing": "the Vite config does not use the ts-dom plugin, so templates are not regenerated while the dev server runs",
|
|
348
449
|
"oxlint-plugin-missing": "the lint config does not load the ts-dom oxlint plugin, so the template rules do not run in the editor",
|
|
349
|
-
"formatter-whitespace": "a formatter is set up while templates keep their whitespace, so reformatting can change the rendered spacing"
|
|
450
|
+
"formatter-whitespace": "a formatter is set up while templates keep their whitespace, so reformatting can change the rendered spacing",
|
|
451
|
+
"skill-available": "the project uses a tool that a Nativele Skill covers, and the project has no copy of that Skill",
|
|
452
|
+
"skill-outdated": "the project's copy of a Nativele Skill differs from the one shipped with the installed version"
|
|
350
453
|
};
|
|
351
454
|
const readText = (file) => existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
352
455
|
/** Whether an include pattern (absolute, forward slashes) makes tsc read the files directly in `dir`. */
|
|
@@ -381,24 +484,34 @@ function runDoctor(options) {
|
|
|
381
484
|
if (!configs.length) add("no-tsconfig", "error", "There is no tsconfig.json. Fix: add one with \"strict\": true, \"lib\": [\"ES2022\", \"DOM\"] and \"moduleResolution\": \"bundler\" (or \"NodeNext\").");
|
|
382
485
|
const includes = [];
|
|
383
486
|
let everyConfigIncludes = true;
|
|
384
|
-
|
|
385
|
-
|
|
487
|
+
const scanned = dirs.map((dir) => ({
|
|
488
|
+
dir,
|
|
489
|
+
folders: templateFolders(dir)
|
|
490
|
+
}));
|
|
491
|
+
const targets = scanned.flatMap(({ dir, folders }) => folders.length ? folders : [dir]);
|
|
492
|
+
const read = configs.map((path) => ({
|
|
493
|
+
path,
|
|
494
|
+
config: readTsConfig(path)
|
|
495
|
+
}));
|
|
496
|
+
const covering = read.filter(({ config: { include } }) => !include || targets.some((t) => include.some((p) => covers(p, t))));
|
|
497
|
+
for (const { path: configPath, config } of read) {
|
|
386
498
|
const o = config.compilerOptions;
|
|
387
499
|
const file = rel(configPath);
|
|
388
500
|
paths = {
|
|
389
501
|
...paths,
|
|
390
502
|
...o.paths
|
|
391
503
|
};
|
|
504
|
+
if (config.include) includes.push(...config.include);
|
|
505
|
+
else everyConfigIncludes = false;
|
|
506
|
+
if (covering.length && !covering.some((r) => r.path === configPath)) continue;
|
|
392
507
|
if (o.strict !== true) add("tsconfig-not-strict", "warning", "compilerOptions.strict is not true, so tsc lets wrong names and null values through. Fix: set \"strict\": true.", { file });
|
|
393
508
|
const lib = o.lib;
|
|
394
509
|
if (lib && !lib.some((l) => l.toLowerCase() === "dom")) add("tsconfig-no-dom-lib", "error", "compilerOptions.lib leaves out DOM, so element types such as HTMLButtonElement are missing. Fix: add \"DOM\" to lib.", { file });
|
|
395
510
|
if (String(o.moduleResolution ?? "").toLowerCase() === "classic") add("tsconfig-classic-resolution", "error", "moduleResolution \"classic\" cannot resolve ./x.html.js to the generated x.html.ts. Fix: use \"bundler\" or \"NodeNext\".", { file });
|
|
396
|
-
if (config.include) includes.push(...config.include);
|
|
397
|
-
else everyConfigIncludes = false;
|
|
398
511
|
}
|
|
399
512
|
if (configs.length && everyConfigIncludes) {
|
|
400
513
|
const missed = /* @__PURE__ */ new Set();
|
|
401
|
-
for (const
|
|
514
|
+
for (const folder of scanned.flatMap((s) => s.folders)) if (!includes.some((p) => covers(p, folder))) missed.add(rel(folder));
|
|
402
515
|
for (const folder of missed) add("tsconfig-misses-templates", "error", `No tsconfig include covers ${folder}, which has templates, so their generated types are never checked. Fix: add "${folder === "." ? "**/*" : `${folder}/**/*`}" to include.`, { file: rel(configs[0]) });
|
|
403
516
|
}
|
|
404
517
|
let resolvable = Object.keys(paths).includes(runtime);
|
|
@@ -447,6 +560,13 @@ function runDoctor(options) {
|
|
|
447
560
|
].some((n) => existsSync(join(cwd, n))) || "prettier" in deps || scripts.some((c) => /(^|[\s;&|])prettier(\s|$)/.test(c)));
|
|
448
561
|
const collapsed = scripts.some((c) => c.includes("--whitespace=collapse")) || vite !== null && /whitespace\s*:\s*["']collapse["']/.test(vite) || dirs.some((d) => listHtml(d).some((f) => (readText(`${f}.ts`) ?? "").split("\n", 3).includes("// whitespace: collapse")));
|
|
449
562
|
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\" })).");
|
|
563
|
+
for (const skill of listSkills(options.skillsDir ?? skillsRoot())) {
|
|
564
|
+
const copies = installedCopies(cwd, skill.name);
|
|
565
|
+
const shipped = readFileSync(join(skill.dir, "SKILL.md"), "utf8");
|
|
566
|
+
for (const copy of copies) if (readFileSync(join(copy, "SKILL.md"), "utf8") !== shipped) add("skill-outdated", "info", `${rel(copy)} differs from the ${skill.name} Skill shipped with the installed Nativele. Fix: run ts-dom skills add ${skill.name} to replace it with the shipped version.`, { file: rel(join(copy, "SKILL.md")) });
|
|
567
|
+
const found = skill.detects.filter((d) => d in deps);
|
|
568
|
+
if (!copies.length && found.length) add("skill-available", "info", `${found.join(", ")} ${found.length > 1 ? "are dependencies" : "is a dependency"}, and the ${skill.name} Skill covers it: ${skill.description} Fix: run ts-dom skills add ${skill.name}, or read it with ts-dom skills ${skill.name}.`, { file: "package.json" });
|
|
569
|
+
}
|
|
450
570
|
return issues;
|
|
451
571
|
}
|
|
452
572
|
function formatDoctorIssue(i) {
|
|
@@ -739,6 +859,17 @@ onChange(e) { this.set({ value: e.detail.value }); }`
|
|
|
739
859
|
inc() { this.count = this.count + 1; }`,
|
|
740
860
|
good: `// counter.ts
|
|
741
861
|
inc() { this.emit("count-change", { value: this.props.count + 1 }); }`
|
|
862
|
+
},
|
|
863
|
+
"unguarded-input-value": {
|
|
864
|
+
why: "render() runs on every change, including the change the user is typing. Assigning a form control's value, even to the same text, moves the caret to the end, so the write must be skipped when nothing differs.",
|
|
865
|
+
bad: `// form.ts
|
|
866
|
+
override render(view: { name: string }) {
|
|
867
|
+
this.refs.name.value = view.name;
|
|
868
|
+
}`,
|
|
869
|
+
good: `// form.ts
|
|
870
|
+
override render(view: { name: string }) {
|
|
871
|
+
if (this.refs.name.value !== view.name) this.refs.name.value = view.name;
|
|
872
|
+
}`
|
|
742
873
|
},
|
|
743
874
|
"custom-event-on-native": {
|
|
744
875
|
why: "A child's custom event does not bubble, so a handler on a native element would never receive it. Events are handled on the tag of the component that emits them.",
|
|
@@ -1040,6 +1171,17 @@ export default defineConfig({ plugins: [tsDom()] });`
|
|
|
1040
1171
|
npx ts-dom gen`,
|
|
1041
1172
|
good: `npx ts-dom gen --whitespace=collapse
|
|
1042
1173
|
// or in vite.config.ts: tsDom({ whitespace: "collapse" })`
|
|
1174
|
+
},
|
|
1175
|
+
"skill-available": {
|
|
1176
|
+
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.",
|
|
1177
|
+
bad: `// package.json lists "react", and the project has no copy of the nativele-react Skill`,
|
|
1178
|
+
good: `npx ts-dom skills add nativele-react # copies it into .claude/skills/ or .agents/skills/
|
|
1179
|
+
npx ts-dom skills nativele-react # or just read it`
|
|
1180
|
+
},
|
|
1181
|
+
"skill-outdated": {
|
|
1182
|
+
why: "A copied Skill does not update with the package. After an upgrade the copy may describe an older version of Nativele or of the tool it covers.",
|
|
1183
|
+
bad: `// .claude/skills/nativele-core/SKILL.md was copied from an older @nativele/ts-dom`,
|
|
1184
|
+
good: `npx ts-dom skills add nativele-core # replaces the copy with the one shipped with the installed version`
|
|
1043
1185
|
}
|
|
1044
1186
|
};
|
|
1045
1187
|
// src/cli.ts
|
|
@@ -1059,8 +1201,9 @@ Commands
|
|
|
1059
1201
|
--strict exit with code 1 on warnings too (errors always do)
|
|
1060
1202
|
--json print the issues as JSON instead of text (also for gen --check)
|
|
1061
1203
|
check [dirs...] One pass for agents and CI: template diagnostics, generated files, tsc errors mapped back
|
|
1062
|
-
to the templates, and the lint rules (default dir: src)
|
|
1063
|
-
--no-write
|
|
1204
|
+
to the templates, and the lint rules (default dir: src). It rewrites the generated files first.
|
|
1205
|
+
--no-write write nothing: report stale generated files instead (for a CI job that must not
|
|
1206
|
+
change the working tree; gen --check only checks the generated files)
|
|
1064
1207
|
--project=<path> tsconfig for tsc (default: tsc's own lookup)
|
|
1065
1208
|
--a11y also check ARIA / accessibility (warnings)
|
|
1066
1209
|
--strict exit with code 1 on warnings too (errors always do)
|
|
@@ -1068,6 +1211,13 @@ Commands
|
|
|
1068
1211
|
doctor [dirs...] Check the project set-up: tsconfig, template imports, generated files, the runtime package,
|
|
1069
1212
|
and the Vite, oxlint and formatter configuration (default dir: src)
|
|
1070
1213
|
--json print the issues as JSON instead of text
|
|
1214
|
+
skills List the Nativele Skills shipped with this version: task procedures for coding agents
|
|
1215
|
+
(set-up, a complete example, known limitations, verification), with the versions verified
|
|
1216
|
+
<name> print one Skill
|
|
1217
|
+
add <name...> copy Skills into the project: .claude/skills/ when the project has .claude,
|
|
1218
|
+
otherwise .agents/skills/
|
|
1219
|
+
--to=<dir> copy into this folder instead
|
|
1220
|
+
--json print the list as JSON
|
|
1071
1221
|
rules List every rule id with its description
|
|
1072
1222
|
--explain=<id> why the rule exists, a wrong example and the fix (the id as printed in [brackets])
|
|
1073
1223
|
--json print every rule as JSON: { id, group, description, why, bad, good }
|
|
@@ -1138,6 +1288,60 @@ function printRules() {
|
|
|
1138
1288
|
}
|
|
1139
1289
|
console.log("\nts-dom rules --explain=<id> shows why a rule exists, with a wrong and a right example.");
|
|
1140
1290
|
}
|
|
1291
|
+
function skillsCommand() {
|
|
1292
|
+
const skills = listSkills();
|
|
1293
|
+
const version = libraryVersion();
|
|
1294
|
+
const names = args.filter((a) => !a.startsWith("--"));
|
|
1295
|
+
const find = (name) => {
|
|
1296
|
+
const skill = skills.find((s) => s.name === name);
|
|
1297
|
+
if (!skill) {
|
|
1298
|
+
console.error(`Unknown Skill "${name}". Fix: run ts-dom skills to see the Skills shipped with this version.`);
|
|
1299
|
+
process.exitCode = 1;
|
|
1300
|
+
}
|
|
1301
|
+
return skill;
|
|
1302
|
+
};
|
|
1303
|
+
if (names[0] === "add") {
|
|
1304
|
+
if (names.length < 2) {
|
|
1305
|
+
console.error("Name the Skills to add. Fix: ts-dom skills add <name...> (ts-dom skills lists them).");
|
|
1306
|
+
process.exitCode = 1;
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const to = args.find((a) => a.startsWith("--to="))?.slice(5);
|
|
1310
|
+
const target = skillsTarget(process.cwd(), to);
|
|
1311
|
+
for (const name of names.slice(1)) {
|
|
1312
|
+
const skill = find(name);
|
|
1313
|
+
if (skill) console.log(`added ${relative(process.cwd(), addSkill(skill, target))}`);
|
|
1314
|
+
}
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (names.length) {
|
|
1318
|
+
for (const name of names) {
|
|
1319
|
+
const skill = find(name);
|
|
1320
|
+
if (skill) console.log(readFileSync(`${skill.dir}/SKILL.md`, "utf8"));
|
|
1321
|
+
}
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const rows = skills.map((skill) => {
|
|
1325
|
+
const { dir: _dir, ...s } = skill;
|
|
1326
|
+
return {
|
|
1327
|
+
...s,
|
|
1328
|
+
stale: staleReason(skill, version)
|
|
1329
|
+
};
|
|
1330
|
+
});
|
|
1331
|
+
if (flags.has("--json")) {
|
|
1332
|
+
console.log(JSON.stringify({
|
|
1333
|
+
version,
|
|
1334
|
+
skills: rows
|
|
1335
|
+
}, null, 2));
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
console.log(`Nativele Skills shipped with @nativele/ts-dom ${version}:\n`);
|
|
1339
|
+
for (const s of rows) {
|
|
1340
|
+
console.log(` ${s.name.padEnd(28)} ${s.tier.padEnd(13)} verified ${s.verified}${s.stale ? ` (stale: ${s.stale})` : ""}`);
|
|
1341
|
+
console.log(` ${"".padEnd(28)} ${s.description}`);
|
|
1342
|
+
}
|
|
1343
|
+
console.log("\nts-dom skills <name> prints one; ts-dom skills add <name> copies it into the project for your agent.");
|
|
1344
|
+
}
|
|
1141
1345
|
const [cmd, ...args] = process.argv.slice(2);
|
|
1142
1346
|
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
1143
1347
|
const globs = (name) => args.filter((a) => a.startsWith(`--${name}=`)).map((a) => a.slice(name.length + 3));
|
|
@@ -1204,9 +1408,9 @@ try {
|
|
|
1204
1408
|
generate
|
|
1205
1409
|
});
|
|
1206
1410
|
if (flags.has("--json")) console.log(JSON.stringify({ issues }, null, 2));
|
|
1207
|
-
else for (const i of issues) (i.severity === "error" ? console.error : console.warn)(formatDoctorIssue(i));
|
|
1411
|
+
else for (const i of issues) (i.severity === "error" ? console.error : i.severity === "warning" ? console.warn : console.log)(formatDoctorIssue(i));
|
|
1208
1412
|
if (issues.some((i) => i.severity === "error")) process.exitCode = 1;
|
|
1209
|
-
if (!flags.has("--json") && !issues.
|
|
1413
|
+
if (!flags.has("--json") && !issues.some((i) => i.severity !== "info")) console.log("ts-dom doctor: the set-up looks right");
|
|
1210
1414
|
} else if (cmd === "lint") {
|
|
1211
1415
|
const files = dirs.flatMap(listTs);
|
|
1212
1416
|
const issues = [...lintUsage(dirs, files), ...lintDataFlow(dirs, files)];
|
|
@@ -1220,6 +1424,7 @@ try {
|
|
|
1220
1424
|
if (issues.filter((i) => i.severity === "error").length || flags.has("--strict") && issues.length) process.exitCode = 1;
|
|
1221
1425
|
if (!flags.has("--json") && !issues.length) console.log("ts-dom lint: no issues");
|
|
1222
1426
|
} else if (cmd === "rules") printRules();
|
|
1427
|
+
else if (cmd === "skills") skillsCommand();
|
|
1223
1428
|
else {
|
|
1224
1429
|
console.log(HELP);
|
|
1225
1430
|
if (cmd && cmd !== "--help" && cmd !== "-h" && cmd !== "help") process.exitCode = 1;
|
package/dist/index.d.mts
CHANGED
|
@@ -218,15 +218,25 @@ type Problems<S extends Schema, St, P, R, E> = ([keyof St | keyof P | keyof R] e
|
|
|
218
218
|
});
|
|
219
219
|
type IsClean<T> = unknown extends T ? true : false;
|
|
220
220
|
type Lists<S extends Schema> = { [K in keyof S["lists"]]: List<S["lists"][K]>; };
|
|
221
|
-
/**
|
|
222
|
-
|
|
221
|
+
/**
|
|
222
|
+
* The second argument received by a data-on-* handler method.
|
|
223
|
+
* In a handler for events inside list items, name the item type and its key field, e.g. `hit: EventHit<Todo, "id">`: `hit.item` is then a `Todo` and `hit.key` a `Todo["id"]`.
|
|
224
|
+
*/
|
|
225
|
+
type EventHit<Item = unknown, KeyField extends keyof Item = never> = [KeyField] extends [never] ? {
|
|
223
226
|
/** The element that carries the data-on-* attribute */
|
|
224
227
|
readonly el: Element;
|
|
225
228
|
/** When the event happened inside a list item: the key of that item */
|
|
226
229
|
readonly key?: PropertyKey;
|
|
227
|
-
/** When the event happened inside a list item: the data
|
|
230
|
+
/** When the event happened inside a list item: the data of that item. Annotate the handler, e.g. `hit: EventHit<Todo, "id">`, to type it. */
|
|
228
231
|
readonly item?: Item;
|
|
229
|
-
}
|
|
232
|
+
} : {
|
|
233
|
+
/** The element that carries the data-on-* attribute */
|
|
234
|
+
readonly el: Element;
|
|
235
|
+
/** The key of the list item the event happened in */
|
|
236
|
+
readonly key: Item[KeyField];
|
|
237
|
+
/** The data of the list item the event happened in */
|
|
238
|
+
readonly item: Item;
|
|
239
|
+
};
|
|
230
240
|
interface ComponentInstance<S extends Schema> extends HTMLElement {
|
|
231
241
|
/** Type inference only (define() uses it to check that data-on-* methods exist); does not exist at runtime. */
|
|
232
242
|
readonly "~events"?: S["events"];
|
|
@@ -266,9 +276,9 @@ interface StatefulInstance<S extends Schema, St extends object, P extends object
|
|
|
266
276
|
batch(fn: () => void): void;
|
|
267
277
|
/**
|
|
268
278
|
* Override it to handle updates that automatic binding cannot do (lists, attributes, focus, ...).
|
|
269
|
-
* `view` is state, props and derived values merged; `changed` holds the fields that changed (all of them on the first render).
|
|
279
|
+
* `view` is state, props and derived values merged; `changed` holds the names of the state fields, props and derived values that changed (all of them on the first render).
|
|
270
280
|
*/
|
|
271
|
-
render(view: View<St & P, R>, changed: ReadonlySet<keyof St | keyof P>): void;
|
|
281
|
+
render(view: View<St & P, R>, changed: ReadonlySet<keyof St | keyof P | keyof R>): void;
|
|
272
282
|
/** Emit an event declared in `emits` (it does not bubble by default and only reaches this element itself). */
|
|
273
283
|
emit<K extends keyof E & string>(name: K, ...detail: EmitArgs<E[K]>): void;
|
|
274
284
|
/** Type inference only (EmitEvent uses it to get the detail type of an event); does not exist at runtime. */
|
package/dist/index.mjs
CHANGED
|
@@ -662,7 +662,10 @@ function Component(tpl, opts = {}) {
|
|
|
662
662
|
for (const [name, fn] of Object.entries(opts.derive ?? {})) {
|
|
663
663
|
const v = fn(to);
|
|
664
664
|
derived[name] = v;
|
|
665
|
-
if (!from || !Object.is(this.#derived[name], v))
|
|
665
|
+
if (!from || !Object.is(this.#derived[name], v)) {
|
|
666
|
+
bindName(name, v);
|
|
667
|
+
keys.push(name);
|
|
668
|
+
}
|
|
666
669
|
}
|
|
667
670
|
this.#derived = derived;
|
|
668
671
|
this.render({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as analyze, f as listHtml, h as scanDefines } from "./a11y-
|
|
1
|
+
import { a as analyze, f as listHtml, h as scanDefines } from "./a11y-CV3nBgBn.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, relative } from "node:path";
|
|
4
4
|
// src/lib/lint.ts
|
|
@@ -145,7 +145,8 @@ const DATA_FLOW_RULES = {
|
|
|
145
145
|
"custom-event-on-native": "a custom event belongs on the child component tag that emits it, not on a native element",
|
|
146
146
|
"unknown-prop": "the attribute is not a declared prop of the child component",
|
|
147
147
|
"invalid-prop-value": "the attribute value cannot be converted to the prop's type",
|
|
148
|
-
"unknown-event": "the child component does not emit this event"
|
|
148
|
+
"unknown-event": "the child component does not emit this event",
|
|
149
|
+
"unguarded-input-value": "render() writes a form control's value without comparing it first, which moves the caret while the user types"
|
|
149
150
|
};
|
|
150
151
|
/** Common native events. A data-on-* on a native element whose event is not listed here is treated as a custom event. */
|
|
151
152
|
const NATIVE_EVENTS = new Set(`click dblclick auxclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave contextmenu wheel keydown keyup keypress input beforeinput change submit reset invalid select focus blur focusin focusout scroll resize load unload error abort toggle cancel close drag dragstart dragend dragenter dragleave dragover drop copy cut paste compositionstart compositionupdate compositionend touchstart touchmove touchend touchcancel pointerdown pointerup pointermove pointerover pointerout pointerenter pointerleave pointercancel gotpointercapture lostpointercapture animationstart animationend animationiteration transitionstart transitionend transitionrun transitioncancel play pause ended playing timeupdate volumechange seeking seeked loadeddata loadedmetadata canplay waiting slotchange fullscreenchange online offline hashchange popstate storage message`.split(/\s+/));
|
|
@@ -308,6 +309,20 @@ function dataFlow(html, classes, ignore = [], contracts = /* @__PURE__ */ new Ma
|
|
|
308
309
|
const tag = /El<"([^"]+)">/.exec(type)?.[1];
|
|
309
310
|
if (tag?.includes("-")) customRefs.set(name, tag);
|
|
310
311
|
}
|
|
312
|
+
const formControls = new Set([...a.scope.refs].filter(([, type]) => /El<"(input|textarea|select)">/.test(type)).map(([name]) => name));
|
|
313
|
+
for (const { body } of classes) {
|
|
314
|
+
const render = methodBody(body, "render");
|
|
315
|
+
if (!render || !formControls.size) continue;
|
|
316
|
+
const lines = render.split("\n");
|
|
317
|
+
lines.forEach((line, i) => {
|
|
318
|
+
for (const m of line.matchAll(/\brefs\.(\w+)\.value\s*=(?!=)/g)) {
|
|
319
|
+
const ref = m[1];
|
|
320
|
+
if (!formControls.has(ref)) continue;
|
|
321
|
+
if ([line, lines[i - 1] ?? ""].some((l) => new RegExp(`refs\\.${esc(ref)}\\.value\\s*!==?`).test(l))) continue;
|
|
322
|
+
report("unguarded-input-value", `render() assigns this.refs.${ref}.value without comparing it first. Writing a form control's value while the user types moves the caret to the end. Fix: if (this.refs.${ref}.value !== next) this.refs.${ref}.value = next;`, `ref:${ref}`);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
311
326
|
for (const { call, body } of classes) {
|
|
312
327
|
const props = optionKeys(call, "props");
|
|
313
328
|
for (const k of props) if (new RegExp(`\\bthis\\.${esc(k)}\\s*(?:[-+*/%]?=(?!=)|\\+\\+|--)`).test(body)) report("props-mutation", `The prop "${k}" is controlled by the parent component; do not assign this.${k} inside the component. Fix: emit an event so the parent changes it, or keep the value in state instead.`);
|
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-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";
|
|
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,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-
|
|
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";
|
|
2
2
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { resolve, sep } from "node:path";
|
|
4
4
|
// src/lib/vite-plugin.ts
|
package/llms.txt
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Nativele
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Designed for AI agents. Driven by TypeScript. Defined in vanilla HTML. A lightweight library for building web components: bind HTML templates to TypeScript custom elements. HTML describes structure and where data goes; TypeScript owns state and behavior; the DOM is the only view. Fully typed by tsc, no virtual DOM, no bundler required.
|
|
4
4
|
|
|
5
|
-
Workflow: write `x.html` + `x.ts`, run `npx ts-dom check` (regenerates `x.html.ts`, runs tsc and lint, points errors at the template). Import a template as `import tpl from "./x.html.js"` (note the `.js`). Lint with `npx ts-dom lint` (add `--a11y`). `npx ts-dom rules` lists every rule id. Every diagnostic is `file:line:col severity [rule-id] message` and ends with a `Fix:` hint: read the hint and apply it.
|
|
5
|
+
Workflow: write `x.html` + `x.ts`, run `npx ts-dom check` (regenerates `x.html.ts`, runs tsc, or vue-tsc when the project has it, and lint, points errors at the template). Import a template as `import tpl from "./x.html.js"` (note the `.js`). Lint with `npx ts-dom lint` (add `--a11y`). `npx ts-dom rules` lists every rule id. Every diagnostic is `file:line:col severity [rule-id] message` and ends with a `Fix:` hint: read the hint and apply it.
|
|
6
6
|
|
|
7
7
|
## Template syntax (HTML only names things; no expressions)
|
|
8
8
|
- `{{ name }}`: text hole, a single identifier only. NOT `{{ a.b }}` / `{{ a + 1 }}`. Not allowed in attributes.
|
|
@@ -43,7 +43,9 @@ Rules the compiler enforces: every `{{ hole }}` / `data-if` name must exist in s
|
|
|
43
43
|
- Props go DOWN: the parent sets them with an attribute or a property (`this.refs.child.value = v.x`, typed). A component never assigns its own props; to ask for a change it `emit`s an event.
|
|
44
44
|
- Events go UP: `emit` does not bubble; the parent handles it with `data-on-<event>` on the child tag, casting with `EmitEvent<Child, "event-name">`.
|
|
45
45
|
- Data needed several levels away: share a `createStore` module and `this.bind(store, fn)`; do not forward props or re-emit events through middle components (lint rules `prop-forwarding`, `emit-forwarding`).
|
|
46
|
-
-
|
|
46
|
+
- `store.set` REPLACES the value (a component's `this.set` merges): `store.set((s) => ({ ...s, status: "loading" }))`. In a component with state/props/derive every `{{ hole }}` must be one of them, so copy store data into state in the `bind` callback: `this.bind(store, (s) => this.set({ total: s.items.length }))`.
|
|
47
|
+
- Loading/error states, modal dialogs (a public `open()` called through a typed ref), form validation messages, filtered lists: `docs/recipes.md` and `examples/recipes/` in the repository.
|
|
48
|
+
- Writing form values back (lint rule `unguarded-input-value`): only assign `input.value` when it differs (`if (input.value !== v.value) input.value = v.value`), otherwise the caret jumps.
|
|
47
49
|
|
|
48
50
|
## Minimal example
|
|
49
51
|
```html
|
|
@@ -72,17 +74,18 @@ define("my-counter", MyCounter);
|
|
|
72
74
|
- Rule: an item with anything of its own (state, events, a nested list) is a component filled with `items =`; an item of a few texts and `data-if` is plain elements filled with `replace()`.
|
|
73
75
|
- Subscribe in `connected()`, never in the constructor: without `moveBefore` a moved item is disconnected (its `bind`/`on`/`onCleanup` released) and connected again. Data every item needs goes in a `createStore` the item binds.
|
|
74
76
|
- Full example (add, remove, sort, toggle, child emit): `examples/readme/task-board.ts` and `task-row.ts` in the repository.
|
|
75
|
-
- Items that are components: `<template data-each="todos" data-key="id" data-prop="todo"><todo-row></todo-row></template>`, then `this.lists.todos.items = view.todos` (inside `render`, guarded by `changed.has("todos")`). Keeps the element of each key, creates and removes the rest, sets the `todo` prop of each changed item; `hit.key` in handlers. The item component declares `todo` in its `props`; `tsc` checks the items against that prop type and the `data-key` field; an item component defined in a scanned folder must be exported and listed in `ts-dom-tags.ts` (run `ts-dom gen`), otherwise the assignment is a type error that names the problem.
|
|
77
|
+
- Items that are components: `<template data-each="todos" data-key="id" data-prop="todo"><todo-row></todo-row></template>`, then `this.lists.todos.items = view.todos` (inside `render`, guarded by `changed.has("todos")`; `changed` holds the state, prop and derived names that changed, so a derived list is guarded by its own name). Keeps the element of each key, creates and removes the rest, sets the `todo` prop of each changed item; `hit.key` in handlers (`hit: EventHit<Todo, "id">` types `hit.key` and `hit.item`). The item component declares `todo` in its `props`; `tsc` checks the items against that prop type and the `data-key` field; an item component defined in a scanned folder must be exported and listed in `ts-dom-tags.ts` (run `ts-dom gen`), otherwise the assignment is a type error that names the problem.
|
|
76
78
|
- Items that are plain elements: `<template data-each="x">` with `this.lists.x.replace(items, keyFn, (item, { text, show }) => ...)`.
|
|
77
79
|
- `data-each` is never nested: a list inside an item is the item component's own `data-each`.
|
|
78
80
|
|
|
79
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.
|
|
80
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.
|
|
81
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.
|
|
82
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.
|
|
83
86
|
- `npx ts-dom gen --check` (writes nothing, exit 1) tells whether the generated files are stale before you trust `tsc` errors.
|
|
84
87
|
- Never edit `x.html.ts` or `ts-dom-tags.ts` (generated). After any `.html` or component change run `npx ts-dom check --json`: it regenerates, runs tsc, maps template contract errors to the template line/col (`missing-binding`, `missing-handler`, item rules), and runs lint. Fix every issue by its `Fix:` hint; never cast a type error away.
|
|
85
|
-
- No expressions in HTML; computed values go in `derive`. Change state only with `this.set()`. Lists, attributes and focus go in `render(view, changed)`; check `changed.has("items")` before rebuilding a list.
|
|
88
|
+
- No expressions in HTML; computed values go in `derive`. Change state only with `this.set()`. Lists, attributes and focus go in `render(view, changed)`; check `changed.has("items")` before rebuilding a list, and annotate `changed` with the names you check (`ReadonlySet<"items" | "visible">`), not `ReadonlySet<string>`, so tsc checks them.
|
|
86
89
|
- Props go down (parent sets them), events go up (`emit`, handled with `data-on-<event>` on the child tag). `data-each` cannot be nested: make the inner list its own component.
|
|
87
90
|
|
|
88
91
|
## Setup
|
|
@@ -95,9 +98,11 @@ In a browser without a bundler map the bare name once: `<script type="importmap"
|
|
|
95
98
|
## Pitfalls
|
|
96
99
|
- Import templates with `./x.html.js`, not `./x.html`. Run `ts-dom gen` before `tsc` (it creates `x.html.ts` and `ts-dom-tags.ts`; both are generated, do not edit or commit).
|
|
97
100
|
- Exports: `export class` the component to get typed `refs.child` in parent templates (`define("tag", Class)` is scanned).
|
|
98
|
-
- `data-if` content is hidden until the name is truthy; `{{ hole }}` stays empty until filled.
|
|
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
|
+
- With Vite, `package.json` needs `"type": "module"`.
|
|
99
103
|
- `slotchange()` is Light-DOM only; with `shadow: true` use the native `slotchange` event.
|
|
100
104
|
|
|
101
105
|
## Docs
|
|
102
|
-
- [README](README.md):
|
|
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).
|
|
103
108
|
- `npx ts-dom rules`: every rule id with a description.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nativele/ts-dom",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
7
7
|
"typescript",
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
"README.md",
|
|
50
50
|
"README.zh-TW.md",
|
|
51
51
|
"llms.txt",
|
|
52
|
+
"skills/README.md",
|
|
53
|
+
"skills/*/SKILL.md",
|
|
52
54
|
"LICENSE",
|
|
53
55
|
"CHANGELOG.md"
|
|
54
56
|
],
|