@nativele/ts-dom 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
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-wWL9rie7.mjs";
3
- import { c as lintUsage, n as UNUSED_MARKUP_RULES, o as formatIssue, s as lintDataFlow, t as DATA_FLOW_RULES } from "./lint-CP_i4VPA.mjs";
3
+ import { c as lintUsage, n as UNUSED_MARKUP_RULES, o as formatIssue, s as lintDataFlow, t as DATA_FLOW_RULES } from "./lint-5vo_ifIc.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) {
@@ -333,6 +334,94 @@ function runCheck(options) {
333
334
  return !seen.has(k) && seen.add(k) !== void 0;
334
335
  }).sort((x, y) => x.file.localeCompare(y.file) || x.line - y.line || x.col - y.col);
335
336
  }
337
+ // src/lib/skills.ts
338
+ const STALE_DAYS = 183;
339
+ /** The skills folder shipped with this package (next to its package.json). */
340
+ function skillsRoot() {
341
+ let dir = dirname(fileURLToPath(import.meta.url));
342
+ while (!existsSync(join(dir, "package.json")) || !existsSync(join(dir, "skills"))) {
343
+ const up = dirname(dir);
344
+ if (up === dir) return join(dirname(fileURLToPath(import.meta.url)), "skills");
345
+ dir = up;
346
+ }
347
+ return join(dir, "skills");
348
+ }
349
+ const unquote = (v) => v.trim().replace(/^(["'])(.*)\1$/, "$2");
350
+ /** Read the frontmatter fields Nativele uses; returns null when the file has none. */
351
+ function parseSkill(text, dir) {
352
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
353
+ if (!m) return null;
354
+ const top = {};
355
+ const meta = {};
356
+ for (const line of m[1].split(/\r?\n/)) {
357
+ const kv = /^(\s*)([\w-]+):\s?(.*)$/.exec(line);
358
+ if (!kv) continue;
359
+ const value = unquote(kv[3].replace(/\s+#.*$/, ""));
360
+ (kv[1] ? meta : top)[kv[2]] = value;
361
+ }
362
+ if (!top.name) return null;
363
+ const tier = meta.tier === "official" || meta.tier === "community" ? meta.tier : "experimental";
364
+ return {
365
+ name: top.name,
366
+ description: top.description ?? "",
367
+ tier,
368
+ nativele: meta.nativele ?? "",
369
+ verified: meta.verified ?? "",
370
+ tools: meta.tools ?? "",
371
+ author: meta.author ?? "",
372
+ detects: (meta.detects ?? "").split(/[\s,]+/).filter(Boolean),
373
+ dir
374
+ };
375
+ }
376
+ function listSkills(root = skillsRoot()) {
377
+ if (!existsSync(root)) return [];
378
+ 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));
379
+ }
380
+ const parts = (v) => v.split(/[.-]/).slice(0, 3).map((n) => Number.parseInt(n, 10) || 0);
381
+ const compare = (a, b) => {
382
+ const x = parts(a);
383
+ const y = parts(b);
384
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i];
385
+ return 0;
386
+ };
387
+ /** Whether `version` satisfies a range of space-separated comparators such as ">=0.5.0 <0.6.0". */
388
+ function inRange(version, range) {
389
+ return range.split(/\s+/).filter(Boolean).every((c) => {
390
+ const m = /^(>=|<=|>|<|=)?v?(\d[\w.-]*)$/.exec(c);
391
+ if (!m) return false;
392
+ const d = compare(version, m[2]);
393
+ return m[1] === ">=" ? d >= 0 : m[1] === "<=" ? d <= 0 : m[1] === ">" ? d > 0 : m[1] === "<" ? d < 0 : d === 0;
394
+ });
395
+ }
396
+ /** Why a Skill should not be trusted as verified for this version, or null when it is current. */
397
+ function staleReason(skill, version, today = /* @__PURE__ */ new Date()) {
398
+ if (!skill.nativele || !inRange(version, skill.nativele)) return `verified for Nativele ${skill.nativele || "(no range)"}, installed ${version}`;
399
+ const verified = Date.parse(skill.verified);
400
+ if (Number.isNaN(verified)) return "no verification date";
401
+ if ((today.getTime() - verified) / 864e5 > STALE_DAYS) return `last verified ${skill.verified}, more than six months ago`;
402
+ return null;
403
+ }
404
+ /** The folder a project keeps its skills in: .claude/skills when the project has .claude, otherwise .agents/skills. */
405
+ function skillsTarget(cwd, to) {
406
+ if (to) return resolve(cwd, to);
407
+ return existsSync(join(cwd, ".claude")) ? join(cwd, ".claude", "skills") : join(cwd, ".agents", "skills");
408
+ }
409
+ /** Every place a project may already hold a copy of a Skill. */
410
+ function installedCopies(cwd, name) {
411
+ return [join(cwd, ".claude", "skills", name), join(cwd, ".agents", "skills", name)].filter((d) => existsSync(join(d, "SKILL.md")));
412
+ }
413
+ /** Copy a Skill (its files, not its example project) into `target/<name>`; returns the folder written. */
414
+ function addSkill(skill, target) {
415
+ const dest = join(target, skill.name);
416
+ cpSync(skill.dir, dest, {
417
+ recursive: true,
418
+ filter: (src) => !src.startsWith(join(skill.dir, "example"))
419
+ });
420
+ return dest;
421
+ }
422
+ function libraryVersion(root = skillsRoot()) {
423
+ return JSON.parse(readFileSync(join(dirname(root), "package.json"), "utf8")).version ?? "0.0.0";
424
+ }
336
425
  // src/lib/doctor.ts
337
426
  const DOCTOR_RULES = {
338
427
  "dir-not-found": "a folder given on the command line does not exist, so nothing in it was checked",
@@ -346,7 +435,9 @@ const DOCTOR_RULES = {
346
435
  "stale-generated": "a generated file is missing, outdated or orphaned",
347
436
  "vite-plugin-missing": "the Vite config does not use the ts-dom plugin, so templates are not regenerated while the dev server runs",
348
437
  "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"
438
+ "formatter-whitespace": "a formatter is set up while templates keep their whitespace, so reformatting can change the rendered spacing",
439
+ "skill-available": "the project uses a tool that a Nativele Skill covers, and the project has no copy of that Skill",
440
+ "skill-outdated": "the project's copy of a Nativele Skill differs from the one shipped with the installed version"
350
441
  };
351
442
  const readText = (file) => existsSync(file) ? readFileSync(file, "utf8") : null;
352
443
  /** Whether an include pattern (absolute, forward slashes) makes tsc read the files directly in `dir`. */
@@ -447,6 +538,13 @@ function runDoctor(options) {
447
538
  ].some((n) => existsSync(join(cwd, n))) || "prettier" in deps || scripts.some((c) => /(^|[\s;&|])prettier(\s|$)/.test(c)));
448
539
  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
540
  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\" })).");
541
+ for (const skill of listSkills(options.skillsDir ?? skillsRoot())) {
542
+ const copies = installedCopies(cwd, skill.name);
543
+ const shipped = readFileSync(join(skill.dir, "SKILL.md"), "utf8");
544
+ 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")) });
545
+ const found = skill.detects.filter((d) => d in deps);
546
+ 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" });
547
+ }
450
548
  return issues;
451
549
  }
452
550
  function formatDoctorIssue(i) {
@@ -739,6 +837,17 @@ onChange(e) { this.set({ value: e.detail.value }); }`
739
837
  inc() { this.count = this.count + 1; }`,
740
838
  good: `// counter.ts
741
839
  inc() { this.emit("count-change", { value: this.props.count + 1 }); }`
840
+ },
841
+ "unguarded-input-value": {
842
+ 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.",
843
+ bad: `// form.ts
844
+ override render(view: { name: string }) {
845
+ this.refs.name.value = view.name;
846
+ }`,
847
+ good: `// form.ts
848
+ override render(view: { name: string }) {
849
+ if (this.refs.name.value !== view.name) this.refs.name.value = view.name;
850
+ }`
742
851
  },
743
852
  "custom-event-on-native": {
744
853
  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 +1149,17 @@ export default defineConfig({ plugins: [tsDom()] });`
1040
1149
  npx ts-dom gen`,
1041
1150
  good: `npx ts-dom gen --whitespace=collapse
1042
1151
  // or in vite.config.ts: tsDom({ whitespace: "collapse" })`
1152
+ },
1153
+ "skill-available": {
1154
+ 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.",
1155
+ bad: `// package.json lists "react", and the project has no copy of the nativele-react Skill`,
1156
+ good: `npx ts-dom skills add nativele-react # copies it into .claude/skills/ or .agents/skills/
1157
+ npx ts-dom skills nativele-react # or just read it`
1158
+ },
1159
+ "skill-outdated": {
1160
+ 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.",
1161
+ bad: `// .claude/skills/nativele-core/SKILL.md was copied from an older @nativele/ts-dom`,
1162
+ good: `npx ts-dom skills add nativele-core # replaces the copy with the one shipped with the installed version`
1043
1163
  }
1044
1164
  };
1045
1165
  // src/cli.ts
@@ -1059,8 +1179,9 @@ Commands
1059
1179
  --strict exit with code 1 on warnings too (errors always do)
1060
1180
  --json print the issues as JSON instead of text (also for gen --check)
1061
1181
  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 do not regenerate; report stale generated files instead
1182
+ to the templates, and the lint rules (default dir: src). It rewrites the generated files first.
1183
+ --no-write write nothing: report stale generated files instead (for a CI job that must not
1184
+ change the working tree; gen --check only checks the generated files)
1064
1185
  --project=<path> tsconfig for tsc (default: tsc's own lookup)
1065
1186
  --a11y also check ARIA / accessibility (warnings)
1066
1187
  --strict exit with code 1 on warnings too (errors always do)
@@ -1068,6 +1189,13 @@ Commands
1068
1189
  doctor [dirs...] Check the project set-up: tsconfig, template imports, generated files, the runtime package,
1069
1190
  and the Vite, oxlint and formatter configuration (default dir: src)
1070
1191
  --json print the issues as JSON instead of text
1192
+ skills List the Nativele Skills shipped with this version: task procedures for coding agents
1193
+ (set-up, a complete example, known limitations, verification), with the versions verified
1194
+ <name> print one Skill
1195
+ add <name...> copy Skills into the project: .claude/skills/ when the project has .claude,
1196
+ otherwise .agents/skills/
1197
+ --to=<dir> copy into this folder instead
1198
+ --json print the list as JSON
1071
1199
  rules List every rule id with its description
1072
1200
  --explain=<id> why the rule exists, a wrong example and the fix (the id as printed in [brackets])
1073
1201
  --json print every rule as JSON: { id, group, description, why, bad, good }
@@ -1138,6 +1266,60 @@ function printRules() {
1138
1266
  }
1139
1267
  console.log("\nts-dom rules --explain=<id> shows why a rule exists, with a wrong and a right example.");
1140
1268
  }
1269
+ function skillsCommand() {
1270
+ const skills = listSkills();
1271
+ const version = libraryVersion();
1272
+ const names = args.filter((a) => !a.startsWith("--"));
1273
+ const find = (name) => {
1274
+ const skill = skills.find((s) => s.name === name);
1275
+ if (!skill) {
1276
+ console.error(`Unknown Skill "${name}". Fix: run ts-dom skills to see the Skills shipped with this version.`);
1277
+ process.exitCode = 1;
1278
+ }
1279
+ return skill;
1280
+ };
1281
+ if (names[0] === "add") {
1282
+ if (names.length < 2) {
1283
+ console.error("Name the Skills to add. Fix: ts-dom skills add <name...> (ts-dom skills lists them).");
1284
+ process.exitCode = 1;
1285
+ return;
1286
+ }
1287
+ const to = args.find((a) => a.startsWith("--to="))?.slice(5);
1288
+ const target = skillsTarget(process.cwd(), to);
1289
+ for (const name of names.slice(1)) {
1290
+ const skill = find(name);
1291
+ if (skill) console.log(`added ${relative(process.cwd(), addSkill(skill, target))}`);
1292
+ }
1293
+ return;
1294
+ }
1295
+ if (names.length) {
1296
+ for (const name of names) {
1297
+ const skill = find(name);
1298
+ if (skill) console.log(readFileSync(`${skill.dir}/SKILL.md`, "utf8"));
1299
+ }
1300
+ return;
1301
+ }
1302
+ const rows = skills.map((skill) => {
1303
+ const { dir: _dir, ...s } = skill;
1304
+ return {
1305
+ ...s,
1306
+ stale: staleReason(skill, version)
1307
+ };
1308
+ });
1309
+ if (flags.has("--json")) {
1310
+ console.log(JSON.stringify({
1311
+ version,
1312
+ skills: rows
1313
+ }, null, 2));
1314
+ return;
1315
+ }
1316
+ console.log(`Nativele Skills shipped with @nativele/ts-dom ${version}:\n`);
1317
+ for (const s of rows) {
1318
+ console.log(` ${s.name.padEnd(28)} ${s.tier.padEnd(13)} verified ${s.verified}${s.stale ? ` (stale: ${s.stale})` : ""}`);
1319
+ console.log(` ${"".padEnd(28)} ${s.description}`);
1320
+ }
1321
+ console.log("\nts-dom skills <name> prints one; ts-dom skills add <name> copies it into the project for your agent.");
1322
+ }
1141
1323
  const [cmd, ...args] = process.argv.slice(2);
1142
1324
  const flags = new Set(args.filter((a) => a.startsWith("--")));
1143
1325
  const globs = (name) => args.filter((a) => a.startsWith(`--${name}=`)).map((a) => a.slice(name.length + 3));
@@ -1204,9 +1386,9 @@ try {
1204
1386
  generate
1205
1387
  });
1206
1388
  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));
1389
+ else for (const i of issues) (i.severity === "error" ? console.error : i.severity === "warning" ? console.warn : console.log)(formatDoctorIssue(i));
1208
1390
  if (issues.some((i) => i.severity === "error")) process.exitCode = 1;
1209
- if (!flags.has("--json") && !issues.length) console.log("ts-dom doctor: the set-up looks right");
1391
+ if (!flags.has("--json") && !issues.some((i) => i.severity !== "info")) console.log("ts-dom doctor: the set-up looks right");
1210
1392
  } else if (cmd === "lint") {
1211
1393
  const files = dirs.flatMap(listTs);
1212
1394
  const issues = [...lintUsage(dirs, files), ...lintDataFlow(dirs, files)];
@@ -1220,6 +1402,7 @@ try {
1220
1402
  if (issues.filter((i) => i.severity === "error").length || flags.has("--strict") && issues.length) process.exitCode = 1;
1221
1403
  if (!flags.has("--json") && !issues.length) console.log("ts-dom lint: no issues");
1222
1404
  } else if (cmd === "rules") printRules();
1405
+ else if (cmd === "skills") skillsCommand();
1223
1406
  else {
1224
1407
  console.log(HELP);
1225
1408
  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
- /** The second argument received by a data-on-* handler method. */
222
- interface EventHit<Item = unknown> {
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 last passed to update() for that item. Annotate the handler, e.g. `hit: EventHit<Todo>`, to type it. */
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)) bindName(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({
@@ -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.`);
@@ -1,5 +1,5 @@
1
1
  import { n as checkA11y, t as A11Y_RULES } from "./a11y-wWL9rie7.mjs";
2
- import { a as dataFlow, i as contractsFor, l as templateCode, n as UNUSED_MARKUP_RULES, r as componentClasses, t as DATA_FLOW_RULES, u as unusedMarkup } from "./lint-CP_i4VPA.mjs";
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-5vo_ifIc.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
  // src/lib/oxlint-plugin.ts
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Nativele
2
2
 
3
- > A TypeScript-driven, HTML-first UI library built for type safety and AI agents. Bind HTML templates to TypeScript custom elements. HTML describes structure and where data goes; TypeScript owns state and behavior; the DOM is the only view. Fully typed by tsc, no virtual DOM, no bundler required.
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
5
  Workflow: write `x.html` + `x.ts`, run `npx ts-dom check` (regenerates `x.html.ts`, runs tsc and lint, points errors at the template). Import a template as `import tpl from "./x.html.js"` (note the `.js`). Lint with `npx ts-dom lint` (add `--a11y`). `npx ts-dom rules` lists every rule id. Every diagnostic is `file:line:col severity [rule-id] message` and ends with a `Fix:` hint: read the hint and apply it.
6
6
 
@@ -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
- - Writing form values back: only assign `input.value` when it differs (`if (input.value !== v.value) input.value = v.value`), otherwise the caret jumps.
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` for React 19, ...), 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): full guide. [README.zh-TW.md](README.zh-TW.md): same in Traditional Chinese.
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.6",
4
- "description": "A TypeScript-driven, HTML-first UI library built for type safety and AI agents: HTML describes the structure, TypeScript owns behavior and state, the native DOM renders it. No virtual DOM, no template expressions, no bundler required.",
3
+ "version": "0.5.0",
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
  ],
@@ -0,0 +1,114 @@
1
+ # Nativele Skills
2
+
3
+ Skills extend what Nativele can be used with, without growing the library: each one is a task procedure for a coding agent, verified with exact versions. This page is the specification every Skill follows and the guide for contributing one.
4
+
5
+ ## What a Skill is
6
+
7
+ A Skill is a task procedure for a coding agent: how to do one integration or one kind of work with Nativele, step by step, with a complete example, the known limitations, and commands that prove it worked. It is not reference documentation: facts about the library's API live in `docs/`, and a Skill links to them instead of restating them.
8
+
9
+ A Skill never changes the library's runtime and never adds a dependency to the library's core. Third-party packages a Skill uses are dependencies of the user's project, installed by the Skill's own steps.
10
+
11
+ ## Layout
12
+
13
+ ```
14
+ skills/
15
+ ├── README.md # this specification and the contribution guide
16
+ ├── nativele-core/
17
+ │ └── SKILL.md
18
+ ├── nativele-react/
19
+ │ ├── SKILL.md
20
+ │ └── example/ # Official only: the project the Skill is verified with
21
+ │ ├── package.json # exact versions of every tool the Skill names
22
+ │ ├── ... # the minimal app the SKILL.md example comes from
23
+ │ └── verify.mjs # the browser checks `npm run verify` ends with
24
+ └── ...
25
+ ```
26
+
27
+ The npm package ships every `SKILL.md` (so the CLI can list and read them offline), not the `example/` folders.
28
+
29
+ ## SKILL.md format
30
+
31
+ `SKILL.md` follows the Agent Skills format (YAML frontmatter with `name` and `description`, then Markdown), so Claude Code and other agents that read skills can load it as it is. Nativele's own fields go under `metadata`:
32
+
33
+ ```markdown
34
+ ---
35
+ name: nativele-react
36
+ description: "Use Nativele web components inside a React 19 app: props, events, children and slots, lifecycle, and TSX types. Use when a project has react and @nativele/ts-dom."
37
+ metadata:
38
+ tier: official # official | community | experimental
39
+ nativele: ">=0.5.0 <0.6.0" # the library versions the Skill was verified with
40
+ verified: "2026-11-05" # date of the last verification
41
+ tools: "react@19.3.1 react-dom@19.3.1 vite@8.3.5 @vitejs/plugin-react@6.2.1 typescript@7.0.2"
42
+ author: "Nativele maintainers" # for community Skills: the contributor
43
+ detects: "react" # package.json dependencies that make ts-dom doctor recommend this Skill
44
+ ---
45
+ ```
46
+
47
+ Quote the `description` and any value that contains `: `, so the frontmatter stays valid YAML for every agent that loads it.
48
+
49
+ Required sections, in this order:
50
+
51
+ 1. **When to use**: the situation and the signals (dependencies, files) that call for this Skill, and when not to use it.
52
+ 2. **Versions**: a table of every tool and the exact version it was verified with, and the date. If the project uses a different major version of a tool, the Skill says so and stops treating its steps as verified.
53
+ 3. **Set-up**: numbered steps with the exact commands and file contents.
54
+ 4. **Example**: a complete, minimal working example (every file it needs, short).
55
+ 5. **Known limitations**: what does not work or needs care, each with what to do instead. An agent must never have to guess these.
56
+ 6. **Verify**: the commands that prove the integration works in the user's project (for example `npx ts-dom check`, `npx tsc --noEmit`, the project's build, a test), with the output to expect.
57
+ 7. **Troubleshooting**: symptoms seen while verifying, their cause and fix.
58
+ 8. **See also**: links to the guides in `docs/` for the facts used.
59
+
60
+ Rules for the content:
61
+
62
+ - Only what was run: every step, example and limitation comes from the verified example, not from memory of how a tool used to work.
63
+ - Version-bound statements say so ("React 19 passes object props as properties; React 18 passes everything as attributes").
64
+ - No promises beyond the verified versions.
65
+ - English, plain and concrete; no restating of `docs/`.
66
+
67
+ ## Tiers
68
+
69
+ | Tier | Who maintains it | Requirements | Shown by `ts-dom skills` |
70
+ |---|---|---|---|
71
+ | Official | the Nativele maintainers | an `example/` project that `npm run verify` checks in CI; verified within the last 6 months | as official |
72
+ | Community | its author | the full SKILL.md format, `author` and `verified` filled in, the verification commands run by the author | as community, with the author |
73
+ | Experimental | its author | the SKILL.md format; verification not complete | as experimental, with a warning |
74
+
75
+ - A new contribution starts as Experimental. It becomes Community when a reviewer has run its verification commands with the stated versions, and Official when the maintainers adopt it with an `example/` project in CI.
76
+ - A Skill whose `verified` date is older than 6 months, or whose `nativele` range does not include the installed version, is shown as stale. A stale Official Skill whose CI check fails is moved to Experimental until fixed.
77
+
78
+ ## Delivering Skills to projects
79
+
80
+ The library already lives in the user's `node_modules`, so its Skills travel with it:
81
+
82
+ - `ts-dom skills` lists the Skills shipped with the installed version: name, tier, description, verified date and versions, whether it is stale, and its path inside `node_modules`. `--json` prints the same as data for agents.
83
+ - `ts-dom skills <name>` prints that Skill (so an agent can read it without knowing the path).
84
+ - `ts-dom skills add <name...>` copies Skills into the project: into `.claude/skills/<name>/` when the project has a `.claude` folder, otherwise into `.agents/skills/<name>/` (`--to=<dir>` for another folder), where agents that load skills pick them up. It only copies; it never installs the third-party packages a Skill mentions.
85
+ - `ts-dom doctor` reads the project's `package.json` and, for each Skill whose `detects` dependency is present and that the project has no copy of, reports it as an `info` issue (`skill-available`); a copy that differs from the shipped one is reported as `skill-outdated`, for example: `package.json:1:1 info [skill-available] react is a dependency: the nativele-react Skill covers using Nativele components in React. Fix: run ts-dom skills add nativele-react, or read it with ts-dom skills nativele-react.` Info issues never change the exit code.
86
+
87
+ None of this adds a runtime dependency: listing and copying read the Markdown files with Node's file API, and the frontmatter is parsed with a small reader for the fields above.
88
+
89
+ ## Verification of Official Skills
90
+
91
+ - `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 short real-browser test of what the Skill promises.
92
+ - `nativele-core` has no example of its own: the library's CI, which checks every example in the repository, verifies it.
93
+ - The workflow `.github/workflows/skills.yml` runs `npm run verify` for each Official Skill when anything under that Skill changes, when the library's runtime changes, and once a month, so a third-party release that breaks a Skill is found. It is not a required check for `main`.
94
+ - The monthly run opens nothing by itself; a failure is reported in the workflow run, and the Skill is fixed or moved to Experimental.
95
+
96
+ ## Contributing a Skill
97
+
98
+ - Copy `skills/_template/SKILL.md`, fill in every section, set `tier: experimental`.
99
+ - Run the Skill's verification commands in a fresh project with the versions you list, and paste their output in the PR description.
100
+ - The PR touches only `skills/<name>/`; a PR that changes the library and adds a Skill is split. Community and Experimental Skills live in this repository, in the same `skills/` folder, so they ship with the package and `ts-dom skills` lists them with their tier.
101
+ - Review checklist: frontmatter complete; every step reproducible; limitations listed; verification commands and their output present; no restating of `docs/`; nothing that would add a dependency to the core.
102
+
103
+ ## Skills
104
+
105
+ | Skill | Tier | Status | Covers |
106
+ |---|---|---|---|
107
+ | `nativele-core` | Official | available | building with Nativele itself: the `ts-dom check` loop, components, lists, data flow |
108
+ | `nativele-react` | Official | available | Nativele components in React 19: props, events, children and slots, lifecycle, TSX types |
109
+ | `nativele-integration` | Official | planned | what every framework integration shares: properties and attributes, events, slots, lifecycle, types |
110
+ | `nativele-vite` | Official | planned | the Vite plugin, `root`, hot updates |
111
+ | `nativele-vue` | Official | planned | Nativele components in Vue |
112
+ | `nativele-state-management` | Official | planned | `createStore`, and Zustand or Redux through `bind` |
113
+ | `nativele-testing` | Official | planned | Vitest and Playwright |
114
+ | Svelte, Angular, Tailwind CSS, TanStack Query, Zod | Experimental or Community | open for contributions | |
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: nativele-<tool>
3
+ description: "What the Skill lets an agent do, and when to use it (the dependencies or files that call for it)."
4
+ metadata:
5
+ tier: experimental
6
+ nativele: ">=0.5.0 <0.6.0"
7
+ verified: "YYYY-MM-DD"
8
+ tools: "<tool>@<exact version> <other tool>@<exact version>"
9
+ author: "<your name or GitHub handle>"
10
+ detects: "<package.json dependency names, space-separated>"
11
+ ---
12
+
13
+ # Nativele + <tool>
14
+
15
+ ## When to use
16
+
17
+ - The situation, and the signals (dependencies, files) that call for this Skill.
18
+ - When not to use it.
19
+
20
+ ## Versions
21
+
22
+ | Tool | Verified version |
23
+ |---|---|
24
+ | @nativele/ts-dom | x.y.z |
25
+ | <tool> | x.y.z |
26
+
27
+ Verified on YYYY-MM-DD. With a different major version of <tool>, treat every step as unverified.
28
+
29
+ ## Set-up
30
+
31
+ 1. Numbered steps with the exact commands and file contents.
32
+
33
+ ## Example
34
+
35
+ The complete, minimal working example: every file it needs.
36
+
37
+ ## Known limitations
38
+
39
+ 1. What does not work or needs care, and what to do instead.
40
+
41
+ ## Verify
42
+
43
+ ```sh
44
+ npx ts-dom check # expect: ts-dom check: no issues
45
+ npx tsc --noEmit # expect: no errors
46
+ ```
47
+
48
+ What to check in the browser or with a test.
49
+
50
+ ## Troubleshooting
51
+
52
+ | Symptom | Cause | Fix |
53
+ |---|---|---|
54
+
55
+ ## See also
56
+
57
+ - Links to the guides in https://github.com/Pixmi/nativele/tree/main/docs for the facts used.