@nativele/ts-dom 0.4.0 → 0.4.6
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 +48 -0
- package/README.md +150 -16
- package/README.zh-TW.md +150 -16
- package/dist/{a11y-Bn9pDvWV.mjs → a11y-wWL9rie7.mjs} +123 -21
- package/dist/cli.mjs +1143 -22
- package/dist/index.d.mts +15 -7
- package/dist/index.mjs +8 -3
- package/dist/{lint-CqWk4U5P.mjs → lint-CP_i4VPA.mjs} +3 -2
- 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 +11 -3
- package/package.json +4 -3
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
|
}
|
|
@@ -246,14 +254,14 @@ interface ComponentInstance<S extends Schema> extends HTMLElement {
|
|
|
246
254
|
/** Members added when the `state` / `props` / `derive` / `emits` options are used. */
|
|
247
255
|
interface StatefulInstance<S extends Schema, St extends object, P extends object, R, E extends Record<string, EventDecl<unknown>>> extends ComponentInstance<S> {
|
|
248
256
|
/** The component's own data (read-only). Use set() to change it. */
|
|
249
|
-
readonly state:
|
|
257
|
+
readonly state: DeepReadonly<St>;
|
|
250
258
|
/** Data provided by the parent (read-only). Do not assign it inside the component; use emit() to notify the parent. */
|
|
251
|
-
readonly props:
|
|
259
|
+
readonly props: DeepReadonly<P>;
|
|
252
260
|
/**
|
|
253
261
|
* The only way to change state: shallow-merges the patch, and does nothing when no field actually changed.
|
|
254
262
|
* When something changed it synchronously updates the same-named {{ holes }} and data-if, then calls render(). It cannot change props.
|
|
255
263
|
*/
|
|
256
|
-
set(patch: Partial<St
|
|
264
|
+
set(patch: Partial<DeepReadonly<St>> | ((s: DeepReadonly<St>) => Partial<DeepReadonly<St>>)): void;
|
|
257
265
|
/** Coalesce several set() calls and prop changes: the view is updated and render() is called once, after fn returns. */
|
|
258
266
|
batch(fn: () => void): void;
|
|
259
267
|
/**
|
|
@@ -284,7 +292,7 @@ export declare function Component<S extends Schema>(tpl: Template<S>, opts?: Com
|
|
|
284
292
|
* `reservedPropNames` (a prop shares a name with HTMLElement or a ts-dom member), `nativeEventNames` (emits shares a name with a native event).
|
|
285
293
|
* (The check lives in the return type, not in the parameter: in the parameter it prevents TypeScript from inferring the type of derive.)
|
|
286
294
|
*/
|
|
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>;
|
|
295
|
+
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
296
|
type CheckHandlers<I> = I extends {
|
|
289
297
|
readonly "~events"?: infer E;
|
|
290
298
|
} ? [Exclude<NonNullable<E>, keyof I>] extends [never] ? unknown : {
|
|
@@ -295,4 +303,4 @@ type CheckHandlers<I> = I extends {
|
|
|
295
303
|
* component as a public method, otherwise tsc reports an error that lists the missing names (`missingHandlers`).
|
|
296
304
|
*/
|
|
297
305
|
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 };
|
|
306
|
+
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
|
};
|
|
@@ -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;
|
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-CP_i4VPA.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
|
-
> A TypeScript-driven, HTML-first UI library. 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
|
+
> 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.
|
|
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,6 +38,7 @@ 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">`.
|
|
@@ -68,12 +69,19 @@ define("my-counter", MyCounter);
|
|
|
68
69
|
`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
70
|
|
|
70
71
|
## Lists
|
|
72
|
+
- 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
|
+
- 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
|
+
- Full example (add, remove, sort, toggle, child emit): `examples/readme/task-board.ts` and `task-row.ts` in the repository.
|
|
71
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.
|
|
72
76
|
- Items that are plain elements: `<template data-each="x">` with `this.lists.x.replace(items, keyFn, (item, { text, show }) => ...)`.
|
|
73
77
|
- `data-each` is never nested: a list inside an item is the item component's own `data-each`.
|
|
74
78
|
|
|
75
79
|
## Rules for agents
|
|
76
|
-
-
|
|
80
|
+
- `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
|
+
- 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
|
+
- 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
|
+
- `npx ts-dom gen --check` (writes nothing, exit 1) tells whether the generated files are stale before you trust `tsc` errors.
|
|
84
|
+
- 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.
|
|
77
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.
|
|
78
86
|
- 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
87
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nativele/ts-dom",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "A TypeScript-driven, HTML-first UI library
|
|
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"html",
|
|
7
7
|
"typescript",
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"test:browser": "vp test run browser/",
|
|
69
69
|
"verify:pack": "vp pack --publint --attw",
|
|
70
70
|
"smoke": "bash scripts/consumer-smoke.sh",
|
|
71
|
-
"smoke:no-bundler": "bash scripts/no-bundler-smoke.sh"
|
|
71
|
+
"smoke:no-bundler": "bash scripts/no-bundler-smoke.sh",
|
|
72
|
+
"bench": "node scripts/bench.ts"
|
|
72
73
|
},
|
|
73
74
|
"dependencies": {
|
|
74
75
|
"parse5": "^8.1.0"
|