@nativele/ts-dom 0.4.0 → 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/CHANGELOG.md +84 -0
- package/README.md +95 -811
- package/README.zh-TW.md +95 -816
- package/dist/{a11y-Bn9pDvWV.mjs → a11y-wWL9rie7.mjs} +123 -21
- package/dist/cli.mjs +1326 -22
- package/dist/index.d.mts +31 -13
- package/dist/index.mjs +12 -4
- package/dist/{lint-CqWk4U5P.mjs → lint-5vo_ifIc.mjs} +19 -3
- package/dist/oxlint-plugin.d.mts +12 -1
- package/dist/oxlint-plugin.mjs +59 -19
- package/dist/vite-plugin.mjs +1 -1
- package/llms.txt +21 -8
- package/package.json +6 -3
- package/skills/README.md +114 -0
- package/skills/_template/SKILL.md +57 -0
- package/skills/nativele-core/SKILL.md +92 -0
- package/skills/nativele-react/SKILL.md +197 -0
package/dist/index.d.mts
CHANGED
|
@@ -176,7 +176,15 @@ export declare function event<T = void>(opts?: {
|
|
|
176
176
|
type DetailOf<D> = D extends EventDecl<infer T> ? T : never;
|
|
177
177
|
type EmitArgs<D> = [DetailOf<D>] extends [void] ? [detail?: undefined] : [detail: DetailOf<D>];
|
|
178
178
|
/** The data passed to render(): state, props and derived values merged. */
|
|
179
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Read-only all the way down: arrays, objects, Maps and Sets inside it cannot be changed through it.
|
|
181
|
+
* State, props and derived values are typed this way, so changing an object in place instead of passing a new one is a tsc error.
|
|
182
|
+
*/
|
|
183
|
+
type DeepReadonly<T> = T extends ((...args: never[]) => unknown) ? T : T extends Node | Date | RegExp | Promise<unknown> ? T : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<K, DeepReadonly<V>> : T extends ReadonlySet<infer V> ? ReadonlySet<DeepReadonly<V>> : T extends readonly unknown[] ? { readonly [K in keyof T]: DeepReadonly<T[K]>; } : T extends object ? IsPlainObject<T> extends true ? { readonly [K in keyof T]: DeepReadonly<T[K]>; } : T : T;
|
|
184
|
+
type IsPlainObject<T> = { [K in keyof T]: T[K]; } extends T ? true : false;
|
|
185
|
+
/** Props as the element exposes them: the parent assigns them, but cannot change the objects inside through them. */
|
|
186
|
+
type PropAccessors<P> = { -readonly [K in keyof P]: DeepReadonly<P[K]>; };
|
|
187
|
+
type View<D, R> = DeepReadonly<D> & DeepReadonly<R>;
|
|
180
188
|
interface DataOptions<St extends object, P extends object, R, E extends Record<string, EventDecl<unknown>>> extends ComponentOptions {
|
|
181
189
|
/** Data owned by the component and changed only through set(). Fields named like a {{ hole }} or data-if in the HTML bind automatically. */
|
|
182
190
|
state?: St;
|
|
@@ -190,7 +198,7 @@ interface DataOptions<St extends object, P extends object, R, E extends Record<s
|
|
|
190
198
|
* Derived values: computed from state and props, usable directly by {{ holes }}, data-if and render().
|
|
191
199
|
* Recomputed after every data change (there is no dependency tracking).
|
|
192
200
|
*/
|
|
193
|
-
derive?: { [K in keyof R]: (s:
|
|
201
|
+
derive?: { [K in keyof R]: (s: DeepReadonly<St & P>) => R[K]; };
|
|
194
202
|
/** The events this component emits. */
|
|
195
203
|
emits?: E;
|
|
196
204
|
}
|
|
@@ -210,15 +218,25 @@ type Problems<S extends Schema, St, P, R, E> = ([keyof St | keyof P | keyof R] e
|
|
|
210
218
|
});
|
|
211
219
|
type IsClean<T> = unknown extends T ? true : false;
|
|
212
220
|
type Lists<S extends Schema> = { [K in keyof S["lists"]]: List<S["lists"][K]>; };
|
|
213
|
-
/**
|
|
214
|
-
|
|
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] ? {
|
|
215
226
|
/** The element that carries the data-on-* attribute */
|
|
216
227
|
readonly el: Element;
|
|
217
228
|
/** When the event happened inside a list item: the key of that item */
|
|
218
229
|
readonly key?: PropertyKey;
|
|
219
|
-
/** 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. */
|
|
220
231
|
readonly item?: Item;
|
|
221
|
-
}
|
|
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
|
+
};
|
|
222
240
|
interface ComponentInstance<S extends Schema> extends HTMLElement {
|
|
223
241
|
/** Type inference only (define() uses it to check that data-on-* methods exist); does not exist at runtime. */
|
|
224
242
|
readonly "~events"?: S["events"];
|
|
@@ -246,21 +264,21 @@ interface ComponentInstance<S extends Schema> extends HTMLElement {
|
|
|
246
264
|
/** Members added when the `state` / `props` / `derive` / `emits` options are used. */
|
|
247
265
|
interface StatefulInstance<S extends Schema, St extends object, P extends object, R, E extends Record<string, EventDecl<unknown>>> extends ComponentInstance<S> {
|
|
248
266
|
/** The component's own data (read-only). Use set() to change it. */
|
|
249
|
-
readonly state:
|
|
267
|
+
readonly state: DeepReadonly<St>;
|
|
250
268
|
/** Data provided by the parent (read-only). Do not assign it inside the component; use emit() to notify the parent. */
|
|
251
|
-
readonly props:
|
|
269
|
+
readonly props: DeepReadonly<P>;
|
|
252
270
|
/**
|
|
253
271
|
* The only way to change state: shallow-merges the patch, and does nothing when no field actually changed.
|
|
254
272
|
* When something changed it synchronously updates the same-named {{ holes }} and data-if, then calls render(). It cannot change props.
|
|
255
273
|
*/
|
|
256
|
-
set(patch: Partial<St
|
|
274
|
+
set(patch: Partial<DeepReadonly<St>> | ((s: DeepReadonly<St>) => Partial<DeepReadonly<St>>)): void;
|
|
257
275
|
/** Coalesce several set() calls and prop changes: the view is updated and render() is called once, after fn returns. */
|
|
258
276
|
batch(fn: () => void): void;
|
|
259
277
|
/**
|
|
260
278
|
* Override it to handle updates that automatic binding cannot do (lists, attributes, focus, ...).
|
|
261
|
-
* `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).
|
|
262
280
|
*/
|
|
263
|
-
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;
|
|
264
282
|
/** Emit an event declared in `emits` (it does not bubble by default and only reaches this element itself). */
|
|
265
283
|
emit<K extends keyof E & string>(name: K, ...detail: EmitArgs<E[K]>): void;
|
|
266
284
|
/** Type inference only (EmitEvent uses it to get the detail type of an event); does not exist at runtime. */
|
|
@@ -284,7 +302,7 @@ export declare function Component<S extends Schema>(tpl: Template<S>, opts?: Com
|
|
|
284
302
|
* `reservedPropNames` (a prop shares a name with HTMLElement or a ts-dom member), `nativeEventNames` (emits shares a name with a native event).
|
|
285
303
|
* (The check lives in the return type, not in the parameter: in the parameter it prevents TypeScript from inferring the type of derive.)
|
|
286
304
|
*/
|
|
287
|
-
export declare function Component<S extends Schema, St extends object = Record<never, never>, P extends object = Record<never, never>, R = Record<never, never>, E extends Record<string, EventDecl<unknown>> = Record<never, never>>(tpl: Template<S>, opts: DataOptions<St, P, R, E>): IsClean<Problems<S, St, P, R, E>> extends true ? new () => StatefulInstance<S, St, P, R, E> & P : Problems<S, St, P, R, E>;
|
|
305
|
+
export declare function Component<S extends Schema, St extends object = Record<never, never>, P extends object = Record<never, never>, R = Record<never, never>, E extends Record<string, EventDecl<unknown>> = Record<never, never>>(tpl: Template<S>, opts: DataOptions<St, P, R, E>): IsClean<Problems<S, St, P, R, E>> extends true ? new () => StatefulInstance<S, St, P, R, E> & PropAccessors<P> : Problems<S, St, P, R, E>;
|
|
288
306
|
type CheckHandlers<I> = I extends {
|
|
289
307
|
readonly "~events"?: infer E;
|
|
290
308
|
} ? [Exclude<NonNullable<E>, keyof I>] extends [never] ? unknown : {
|
|
@@ -295,4 +313,4 @@ type CheckHandlers<I> = I extends {
|
|
|
295
313
|
* component as a public method, otherwise tsc reports an error that lists the missing names (`missingHandlers`).
|
|
296
314
|
*/
|
|
297
315
|
export declare function define<C extends abstract new (...args: never[]) => HTMLElement>(name: string, ctor: C & CheckHandlers<InstanceType<C>>): void;
|
|
298
|
-
export type { ComponentInstance, ComponentOptions, DataOptions, El, EmitEvent, EventDecl, EventHit, ItemContext, ListItem, ListSchema, ReadableStore, ReplaceOptions, Schema, StatefulInstance, Store, Template, TemplateInstance, TextOf, TextValue, Update, View };
|
|
316
|
+
export type { ComponentInstance, ComponentOptions, DataOptions, DeepReadonly, El, EmitEvent, EventDecl, EventHit, ItemContext, ListItem, ListSchema, PropAccessors, ReadableStore, ReplaceOptions, Schema, StatefulInstance, Store, Template, TemplateInstance, TextOf, TextValue, Update, View };
|
package/dist/index.mjs
CHANGED
|
@@ -228,7 +228,7 @@ var List = class {
|
|
|
228
228
|
if (!this.binding) throw new Error("[list-not-bound] this list has no data-key and data-prop, so its items cannot be assigned. Fix: add them to the <template data-each>, e.g. data-key=\"id\" data-prop=\"item\", or call replace().");
|
|
229
229
|
if (!Array.isArray(items)) throw new Error(`[invalid-items] items must be an array of objects, but got ${items === null ? "null" : typeof items}. Fix: assign an array, for example items = view.todos ?? [].`);
|
|
230
230
|
if (items.length) this.#checkProp();
|
|
231
|
-
this
|
|
231
|
+
this.#reconcile(items, this.#keyOf, this.#assign, true, true);
|
|
232
232
|
}
|
|
233
233
|
#checkProp() {
|
|
234
234
|
const prop = this.binding?.prop;
|
|
@@ -263,7 +263,7 @@ var List = class {
|
|
|
263
263
|
return this.#items.get(key);
|
|
264
264
|
}
|
|
265
265
|
#create(key) {
|
|
266
|
-
const el = this.proto.cloneNode(true);
|
|
266
|
+
const el = this.binding ? document.importNode(this.proto, true) : this.proto.cloneNode(true);
|
|
267
267
|
const holder = document.createDocumentFragment();
|
|
268
268
|
holder.append(el);
|
|
269
269
|
const conds = mountConds(holder);
|
|
@@ -324,6 +324,10 @@ var List = class {
|
|
|
324
324
|
}
|
|
325
325
|
/** Keyed reconciliation: remove extra items, create missing ones, reorder, and call update() for every item. */
|
|
326
326
|
replace(items, key, update, opts = {}) {
|
|
327
|
+
this.#reconcile(items, key, update, opts.skipUnchanged === true, false);
|
|
328
|
+
}
|
|
329
|
+
/** `updateFirst`: a new item is updated before it is inserted, so an item component is connected with its prop already set. */
|
|
330
|
+
#reconcile(items, key, update, skipUnchanged, updateFirst) {
|
|
327
331
|
const next = /* @__PURE__ */ new Set();
|
|
328
332
|
for (const it of items) {
|
|
329
333
|
const k = key(it);
|
|
@@ -340,13 +344,14 @@ var List = class {
|
|
|
340
344
|
if (!entry) {
|
|
341
345
|
entry = this.#create(k);
|
|
342
346
|
this.#items.set(k, entry);
|
|
347
|
+
if (updateFirst) this.#run(entry, k, it, update);
|
|
343
348
|
}
|
|
344
349
|
if (entry.el.nextSibling !== after || entry.el.parentNode !== parent) {
|
|
345
350
|
if (entry.el.parentNode === parent && parent?.moveBefore) parent.moveBefore(entry.el, after);
|
|
346
351
|
else parent?.insertBefore(entry.el, after);
|
|
347
352
|
}
|
|
348
353
|
after = entry.el;
|
|
349
|
-
this.#run(entry, k, it, update,
|
|
354
|
+
this.#run(entry, k, it, update, skipUnchanged);
|
|
350
355
|
}
|
|
351
356
|
}
|
|
352
357
|
};
|
|
@@ -657,7 +662,10 @@ function Component(tpl, opts = {}) {
|
|
|
657
662
|
for (const [name, fn] of Object.entries(opts.derive ?? {})) {
|
|
658
663
|
const v = fn(to);
|
|
659
664
|
derived[name] = v;
|
|
660
|
-
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
|
+
}
|
|
661
669
|
}
|
|
662
670
|
this.#derived = derived;
|
|
663
671
|
this.render({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as analyze,
|
|
1
|
+
import { a as analyze, f as listHtml, h as scanDefines } from "./a11y-wWL9rie7.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, relative } from "node:path";
|
|
4
4
|
// src/lib/lint.ts
|
|
@@ -43,7 +43,8 @@ function componentClasses(code, htmlSpec) {
|
|
|
43
43
|
const bodyEnd = matchClose(code, bodyStart, "{", "}");
|
|
44
44
|
if (bodyEnd > 0) out.push({
|
|
45
45
|
call: code.slice(call, callEnd + 1),
|
|
46
|
-
body: code.slice(bodyStart, bodyEnd + 1)
|
|
46
|
+
body: code.slice(bodyStart, bodyEnd + 1),
|
|
47
|
+
end: bodyEnd
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
return out;
|
|
@@ -144,7 +145,8 @@ const DATA_FLOW_RULES = {
|
|
|
144
145
|
"custom-event-on-native": "a custom event belongs on the child component tag that emits it, not on a native element",
|
|
145
146
|
"unknown-prop": "the attribute is not a declared prop of the child component",
|
|
146
147
|
"invalid-prop-value": "the attribute value cannot be converted to the prop's type",
|
|
147
|
-
"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"
|
|
148
150
|
};
|
|
149
151
|
/** Common native events. A data-on-* on a native element whose event is not listed here is treated as a custom event. */
|
|
150
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+/));
|
|
@@ -307,6 +309,20 @@ function dataFlow(html, classes, ignore = [], contracts = /* @__PURE__ */ new Ma
|
|
|
307
309
|
const tag = /El<"([^"]+)">/.exec(type)?.[1];
|
|
308
310
|
if (tag?.includes("-")) customRefs.set(name, tag);
|
|
309
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
|
+
}
|
|
310
326
|
for (const { call, body } of classes) {
|
|
311
327
|
const props = optionKeys(call, "props");
|
|
312
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.d.mts
CHANGED
|
@@ -4,6 +4,13 @@ interface ImportNode {
|
|
|
4
4
|
value: string;
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
|
+
interface Fixer {
|
|
8
|
+
insertTextBeforeRange(range: [number, number], text: string): unknown;
|
|
9
|
+
}
|
|
10
|
+
interface Suggestion {
|
|
11
|
+
desc: string;
|
|
12
|
+
fix(fixer: Fixer): unknown;
|
|
13
|
+
}
|
|
7
14
|
interface Context {
|
|
8
15
|
options?: [{
|
|
9
16
|
ignore?: string[];
|
|
@@ -15,6 +22,7 @@ interface Context {
|
|
|
15
22
|
report(d: {
|
|
16
23
|
message: string;
|
|
17
24
|
node: unknown;
|
|
25
|
+
suggest?: Suggestion[];
|
|
18
26
|
}): void;
|
|
19
27
|
}
|
|
20
28
|
declare const _default: {
|
|
@@ -22,7 +30,7 @@ declare const _default: {
|
|
|
22
30
|
name: string;
|
|
23
31
|
};
|
|
24
32
|
rules: {
|
|
25
|
-
/** The HTML declares markup that this file never uses.
|
|
33
|
+
/** The HTML declares markup that this file never uses. Reported as errors. */
|
|
26
34
|
"unused-markup": {
|
|
27
35
|
meta: {
|
|
28
36
|
type: "suggestion";
|
|
@@ -41,6 +49,7 @@ declare const _default: {
|
|
|
41
49
|
};
|
|
42
50
|
additionalProperties: boolean;
|
|
43
51
|
}[];
|
|
52
|
+
hasSuggestions: boolean;
|
|
44
53
|
};
|
|
45
54
|
create(context: Context): {
|
|
46
55
|
ImportDeclaration(node: ImportNode): void;
|
|
@@ -65,6 +74,7 @@ declare const _default: {
|
|
|
65
74
|
};
|
|
66
75
|
additionalProperties: boolean;
|
|
67
76
|
}[];
|
|
77
|
+
hasSuggestions: boolean;
|
|
68
78
|
};
|
|
69
79
|
create(context: Context): {
|
|
70
80
|
ImportDeclaration(node: ImportNode): void;
|
|
@@ -89,6 +99,7 @@ declare const _default: {
|
|
|
89
99
|
};
|
|
90
100
|
additionalProperties: boolean;
|
|
91
101
|
}[];
|
|
102
|
+
hasSuggestions: boolean;
|
|
92
103
|
};
|
|
93
104
|
create(context: Context): {
|
|
94
105
|
ImportDeclaration(node: ImportNode): void;
|
package/dist/oxlint-plugin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as checkA11y } from "./a11y-
|
|
2
|
-
import { a as dataFlow, i as contractsFor, l as templateCode, r as componentClasses, u as unusedMarkup } from "./lint-
|
|
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-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
|
|
@@ -14,46 +14,86 @@ const ignoreSchema = [{
|
|
|
14
14
|
/** Child component contracts, scanned once per process from the working directory. */
|
|
15
15
|
let contracts = null;
|
|
16
16
|
const getContracts = () => contracts ??= contractsFor([process.cwd()]);
|
|
17
|
+
const cache = /* @__PURE__ */ new Map();
|
|
18
|
+
function groupDiagnostics(group, html, code, spec, filename) {
|
|
19
|
+
const key = `${group}\0${filename}\0${spec}`;
|
|
20
|
+
const hit = cache.get(key);
|
|
21
|
+
if (hit && hit.code === code && hit.html === html) return hit.diags;
|
|
22
|
+
const diags = group === "unused-markup" ? unusedMarkup(html, templateCode(code, spec)) : group === "a11y" ? checkA11y(html) : dataFlow(html, componentClasses(code, spec), [], getContracts());
|
|
23
|
+
cache.set(key, {
|
|
24
|
+
code,
|
|
25
|
+
html,
|
|
26
|
+
diags
|
|
27
|
+
});
|
|
28
|
+
return diags;
|
|
29
|
+
}
|
|
30
|
+
function readTemplate(filename, spec) {
|
|
31
|
+
try {
|
|
32
|
+
return readFileSync(resolve(dirname(filename), spec.slice(0, -3)), "utf8");
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** A missing handler can be added to the class that uses the template: offered as a suggestion, since the method body is the developer's to write. */
|
|
38
|
+
function suggestionsFor(d, code, spec) {
|
|
39
|
+
if (d.rule !== "missing-handler") return void 0;
|
|
40
|
+
const name = /="([^"]+)"/.exec(d.message)?.[1];
|
|
41
|
+
const target = componentClasses(code, spec)[0];
|
|
42
|
+
if (!name || !target) return void 0;
|
|
43
|
+
return [{
|
|
44
|
+
desc: `Add a method ${name}() to the component class`,
|
|
45
|
+
fix: (fixer) => fixer.insertTextBeforeRange([target.end, target.end], `\n ${name}(_e: Event): void {}\n`)
|
|
46
|
+
}];
|
|
47
|
+
}
|
|
17
48
|
/**
|
|
18
|
-
* Build an oxlint rule that
|
|
49
|
+
* Build an oxlint rule that checks every `import x from "./name.html.js"` and reports the findings
|
|
19
50
|
* on the import line (an oxlint plugin can only report inside the file being linted).
|
|
20
51
|
* The message starts with the HTML location, the rule id and what to do: `./x.html.js:3:5 [unused-ref] ...`.
|
|
52
|
+
* `only` limits the rule to one rule id of the group; without it the rule reports the whole group, minus `ignore`.
|
|
21
53
|
*/
|
|
22
|
-
function templateRule(description,
|
|
54
|
+
function templateRule(group, description, only) {
|
|
23
55
|
return {
|
|
24
56
|
meta: {
|
|
25
57
|
type: "suggestion",
|
|
26
58
|
docs: { description },
|
|
27
|
-
schema: ignoreSchema
|
|
59
|
+
schema: only ? [] : ignoreSchema,
|
|
60
|
+
hasSuggestions: true
|
|
28
61
|
},
|
|
29
62
|
create(context) {
|
|
30
63
|
return { ImportDeclaration(node) {
|
|
31
64
|
const spec = node.source.value;
|
|
32
65
|
if (!spec.endsWith(".html.js")) return;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
const html = readTemplate(context.filename, spec);
|
|
67
|
+
if (html === null) return;
|
|
68
|
+
const code = context.sourceCode.text;
|
|
69
|
+
const ignore = context.options?.[0]?.ignore ?? [];
|
|
70
|
+
for (const d of groupDiagnostics(group, html, code, spec, context.filename)) {
|
|
71
|
+
if (only ? d.rule !== only : ignore.includes(d.rule)) continue;
|
|
72
|
+
const suggest = suggestionsFor(d, code, spec);
|
|
73
|
+
context.report({
|
|
74
|
+
message: `${spec}:${d.line}:${d.col} [${d.rule}] ${d.message}`,
|
|
75
|
+
node,
|
|
76
|
+
...suggest ? { suggest } : {}
|
|
77
|
+
});
|
|
38
78
|
}
|
|
39
|
-
for (const d of check(html, context, spec)) context.report({
|
|
40
|
-
message: `${spec}:${d.line}:${d.col} [${d.rule}] ${d.message}`,
|
|
41
|
-
node
|
|
42
|
-
});
|
|
43
79
|
} };
|
|
44
80
|
}
|
|
45
81
|
};
|
|
46
82
|
}
|
|
47
|
-
const
|
|
83
|
+
const perRule = (group, rules) => Object.fromEntries(Object.entries(rules).map(([id, description]) => [id, templateRule(group, description, id)]));
|
|
84
|
+
const { "no-importer": _noImporter, ...unusedMarkupRules } = UNUSED_MARKUP_RULES;
|
|
48
85
|
var oxlint_plugin_default = {
|
|
49
86
|
meta: { name: "ts-dom" },
|
|
50
87
|
rules: {
|
|
51
|
-
/** The HTML declares markup that this file never uses.
|
|
52
|
-
"unused-markup": templateRule("Markup declared in the HTML (data-ref, data-if, data-each, {{ }}, data-on-*) must be used by the component class"
|
|
88
|
+
/** The HTML declares markup that this file never uses. Reported as errors. */
|
|
89
|
+
"unused-markup": templateRule("unused-markup", "Markup declared in the HTML (data-ref, data-if, data-each, {{ }}, data-on-*) must be used by the component class"),
|
|
53
90
|
/** ARIA and basic accessibility problems in the template. Off by default. */
|
|
54
|
-
a11y: templateRule("Check the template for ARIA and basic accessibility problems"
|
|
91
|
+
a11y: templateRule("a11y", "Check the template for ARIA and basic accessibility problems"),
|
|
55
92
|
/** Data flow suggestions and child component contract checks. Off by default. */
|
|
56
|
-
"data-flow": templateRule("Keep props flowing down and events flowing up one level at a time, and check child component attributes and events"
|
|
93
|
+
"data-flow": templateRule("data-flow", "Keep props flowing down and events flowing up one level at a time, and check child component attributes and events"),
|
|
94
|
+
...perRule("unused-markup", unusedMarkupRules),
|
|
95
|
+
...perRule("data-flow", DATA_FLOW_RULES),
|
|
96
|
+
...perRule("a11y", Object.fromEntries(Object.entries(A11Y_RULES).map(([k, v]) => [k, v.split(". Fix:")[0]])))
|
|
57
97
|
}
|
|
58
98
|
};
|
|
59
99
|
export { oxlint_plugin_default as default };
|
package/dist/vite-plugin.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as
|
|
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-wWL9rie7.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
|
|
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
|
|
|
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.
|
|
@@ -38,11 +38,14 @@ define("my-counter", MyCounter);
|
|
|
38
38
|
Rules the compiler enforces: every `{{ hole }}` / `data-if` name must exist in state, props or derive (error `missingStateKeys`); state/props/derive names must be unique (`conflictingKeys`); a prop must not be named like an HTMLElement/ts-dom member such as `title`, `hidden`, `render` (`reservedPropNames`); emitted event names must not be native events (`nativeEventNames`); every `data-on-*` method must exist as a public method (`missingHandlers`, reported by `define()`).
|
|
39
39
|
|
|
40
40
|
## Data flow (one direction)
|
|
41
|
+
- `state`, `props`, `render`'s `view` and `derive` arguments are `DeepReadonly`: changing an object or array inside them is a tsc error. Replace with new objects (`s.items.map((i) => (i.id === id ? { ...i, done: true } : i))`). Annotate with read-only types (`readonly Item[]`, `DeepReadonly<State>`).
|
|
41
42
|
- State: changed only by its owner with `this.set(patch | s => patch)`; no change => no render. `this.batch(fn)` merges several updates. Rendering is synchronous.
|
|
42
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.
|
|
43
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">`.
|
|
44
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`).
|
|
45
|
-
-
|
|
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.
|
|
46
49
|
|
|
47
50
|
## Minimal example
|
|
48
51
|
```html
|
|
@@ -68,13 +71,21 @@ define("my-counter", MyCounter);
|
|
|
68
71
|
`const view = tpl.instantiate()` returns `{ root: DocumentFragment, rootElement, refs, lists, text, show }` typed like a component (`rootElement` exists in the type only when the template has exactly one top-level element, `<style>` excluded), for pieces created in code. No host: `data-on-*` is inert (use `refs.x.addEventListener`) and `<slot>` is not projected.
|
|
69
72
|
|
|
70
73
|
## Lists
|
|
71
|
-
-
|
|
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()`.
|
|
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.
|
|
76
|
+
- Full example (add, remove, sort, toggle, child emit): `examples/readme/task-board.ts` and `task-row.ts` in the repository.
|
|
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.
|
|
72
78
|
- Items that are plain elements: `<template data-each="x">` with `this.lists.x.replace(items, keyFn, (item, { text, show }) => ...)`.
|
|
73
79
|
- `data-each` is never nested: a list inside an item is the item component's own `data-each`.
|
|
74
80
|
|
|
75
81
|
## Rules for agents
|
|
76
|
-
-
|
|
77
|
-
-
|
|
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.
|
|
83
|
+
- `npx ts-dom rules --explain=<id>` (or `rules --json`) explains any rule id from a diagnostic: why it exists, a wrong example and the fix.
|
|
84
|
+
- Markup well-formedness is a formatter's job: `oxfmt --check` reports misnested tags that the library's parser would silently repair. Generate with `--whitespace=collapse` when a formatter is used.
|
|
85
|
+
- 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.
|
|
86
|
+
- `npx ts-dom gen --check` (writes nothing, exit 1) tells whether the generated files are stale before you trust `tsc` errors.
|
|
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.
|
|
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.
|
|
78
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.
|
|
79
90
|
|
|
80
91
|
## Setup
|
|
@@ -87,9 +98,11 @@ In a browser without a bundler map the bare name once: `<script type="importmap"
|
|
|
87
98
|
## Pitfalls
|
|
88
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).
|
|
89
100
|
- Exports: `export class` the component to get typed `refs.child` in parent templates (`define("tag", Class)` is scanned).
|
|
90
|
-
- `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"`.
|
|
91
103
|
- `slotchange()` is Light-DOM only; with `shadow: true` use the native `slotchange` event.
|
|
92
104
|
|
|
93
105
|
## Docs
|
|
94
|
-
- [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).
|
|
95
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.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
|
],
|
|
@@ -68,7 +70,8 @@
|
|
|
68
70
|
"test:browser": "vp test run browser/",
|
|
69
71
|
"verify:pack": "vp pack --publint --attw",
|
|
70
72
|
"smoke": "bash scripts/consumer-smoke.sh",
|
|
71
|
-
"smoke:no-bundler": "bash scripts/no-bundler-smoke.sh"
|
|
73
|
+
"smoke:no-bundler": "bash scripts/no-bundler-smoke.sh",
|
|
74
|
+
"bench": "node scripts/bench.ts"
|
|
72
75
|
},
|
|
73
76
|
"dependencies": {
|
|
74
77
|
"parse5": "^8.1.0"
|
package/skills/README.md
ADDED
|
@@ -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 | |
|