@nativele/ts-dom 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ name: nativele-react
3
3
  description: "Use Nativele web components inside a React 19 app: passing props, receiving events, children and slots, lifecycle, and TSX types. Use when a project depends on react and @nativele/ts-dom."
4
4
  metadata:
5
5
  tier: official
6
- nativele: ">=0.5.0 <0.6.0"
6
+ nativele: ">=0.5.1 <0.6.0"
7
7
  verified: "2026-11-05"
8
8
  tools: "react@19.3.1 react-dom@19.3.1 @types/react@19.3.0 @types/react-dom@19.3.0 vite@8.3.5 @vitejs/plugin-react@6.2.1 typescript@7.0.2"
9
9
  author: "Nativele maintainers"
@@ -24,20 +24,21 @@ Nativele components are native custom elements. React renders them like any othe
24
24
 
25
25
  | Tool | Verified version |
26
26
  |---|---|
27
- | @nativele/ts-dom | 0.5.0 |
27
+ | @nativele/ts-dom | 0.5.1 |
28
28
  | react, react-dom | 19.3.1 |
29
29
  | @types/react, @types/react-dom | 19.3.0 |
30
30
  | vite | 8.3.5 |
31
31
  | @vitejs/plugin-react | 6.2.1 |
32
32
  | typescript | 7.0.2 |
33
33
 
34
- Verified on 2026-11-05 in Google Chrome 148 with the example project in the Nativele repository (`skills/nativele-react/example`), which CI runs again when the Skill or the library changes and once a month. With a different major version of React, treat every step as unverified.
34
+ Verified on 2026-11-05 in Google Chrome 148 with the example project in the Nativele repository (`skills/nativele-react/example`), which CI runs again when the example or the library changes and once a month. With a different major version of React, treat every step as unverified.
35
35
 
36
36
  ## Set-up
37
37
 
38
- 1. Keep the Nativele components in their own folder, for example `src/nv/`, each as `x.html` plus `x.ts` (see the core Skill or the guides).
38
+ 1. Keep the Nativele components in their own folder, for example `src/nv/`, each as `x.html` plus `x.ts`. Write them with the core Skill: `npx ts-dom skills add nativele-core`.
39
39
  2. Install: `npm i @nativele/ts-dom`.
40
- 3. Add the Nativele Vite plugin next to the React plugin, pointing at that folder:
40
+ 3. Set `"strict": true` in the tsconfig whose `include` covers that folder (`tsconfig.app.json` in Vite's React template, which leaves it off); Nativele's type checks depend on it. Other configs, such as the one for `vite.config.ts`, stay as they are.
41
+ 4. Add the Nativele Vite plugin next to the React plugin, pointing at that folder:
41
42
 
42
43
  ```ts
43
44
  // vite.config.ts
@@ -48,7 +49,11 @@ Verified on 2026-11-05 in Google Chrome 148 with the example project in the Nati
48
49
  export default defineConfig({ plugins: [tsDom({ dirs: ["src/nv"] }), react()] });
49
50
  ```
50
51
 
51
- 4. Import every Nativele component module in the entry file, before React renders anything:
52
+ Pass the same folder to every `ts-dom` command (`npx ts-dom check src/nv`), so there is one `ts-dom-tags.ts`, in `src/nv`.
53
+
54
+ 5. If the project lints with oxlint (`.oxlintrc.json`, as in Vite's React template), load the ts-dom rules there: add `{ "name": "ts-dom", "specifier": "@nativele/ts-dom/oxlint" }` to `jsPlugins` and `"ts-dom/unused-markup": "error"` to `rules`.
55
+
56
+ 6. Import every Nativele component module in the entry file, before React renders anything:
52
57
 
53
58
  ```tsx
54
59
  // src/main.tsx
@@ -59,7 +64,7 @@ Verified on 2026-11-05 in Google Chrome 148 with the example project in the Nati
59
64
 
60
65
  React 19 sets a value as a property only when the element is already defined; before that it writes an attribute, and an object prop becomes the text `"[object Object]"`.
61
66
 
62
- 5. Add the TSX declaration below as `src/nativele-react.d.ts`. It reads the tag map that `ts-dom gen` writes (`ts-dom-tags.ts`), so every exported component gets typed props and events, and new components need no edit:
67
+ 7. Add the TSX declaration below as `src/nativele-react.d.ts`. It reads the tag map that `ts-dom gen` writes (`ts-dom-tags.ts`), so every exported component gets typed props and events, and new components need no edit:
63
68
 
64
69
  ```ts
65
70
  // src/nativele-react.d.ts
@@ -84,6 +89,8 @@ Verified on 2026-11-05 in Google Chrome 148 with the example project in the Nati
84
89
 
85
90
  The component class must be exported and passed to `define()` in a folder that `ts-dom gen` scans, so it appears in `ts-dom-tags.ts`.
86
91
 
92
+ The generated files are not committed, so on a clean checkout the type check in `npm run build` runs before anything has written them and fails with `TS2307: Cannot find module './x.html.js'`. Generate them first: `"build": "ts-dom gen src/nv && tsc -b && vite build"` (the template's script with `ts-dom gen src/nv &&` in front).
93
+
87
94
  ## Example
88
95
 
89
96
  A Nativele component with props and an event:
@@ -132,6 +139,12 @@ Used from React:
132
139
 
133
140
  A component with slots that receives React children: give it `shadow: true`, then pass children directly.
134
141
 
142
+ ```html
143
+ <!-- src/nv/shadow-card.html -->
144
+ <h2><slot name="title">Untitled</slot></h2>
145
+ <div><slot></slot></div>
146
+ ```
147
+
135
148
  ```ts
136
149
  // src/nv/shadow-card.ts
137
150
  export class ShadowCard extends Component(tpl, { shadow: true }) {}
@@ -147,9 +160,9 @@ define("shadow-card", ShadowCard);
147
160
 
148
161
  ## Known limitations
149
162
 
150
- 1. **Define before render.** Import every Nativele module before React first renders its tag (step 4). Otherwise props become attributes and object props are lost.
163
+ 1. **Define before render.** Import every Nativele module before React first renders its tag (step 6). Otherwise props become attributes and object props are lost.
151
164
  2. **Event prop names.** Only `on` plus the exact event name works (`onitem-select`); `onItemSelect` and `onItem-select` never fire. Nativele events do not bubble by default, so put the handler on the element that emits. A ref with `addEventListener` in `useEffect` also works (remove it in the cleanup).
152
- 3. **Light DOM slots and React children do not mix.** In the default Light DOM mode, Nativele moves the host's children into its slot positions. When React later inserts, removes or reorders those children it fails (`insertBefore`/`removeChild` errors that unmount the root) or leaves nodes outside the slot. Changing text in place and appending at the end happen to work, but do not rely on that. Use one of:
165
+ 3. **Light DOM slots and React children do not mix.** In the default Light DOM mode (`Component(tpl)` with no `shadow`), Nativele moves the host's children into its slot positions. When React later inserts, removes or reorders those children it fails (`insertBefore`/`removeChild` errors that unmount the root) or leaves nodes outside the slot. Changing text in place and appending at the end happen to work, but do not rely on that. Use one of:
153
166
  - `shadow: true` on the component (children can then change freely), or
154
167
  - one stable React element per slot, and let React change only what is inside it:
155
168
 
@@ -160,9 +173,9 @@ define("shadow-card", ShadowCard);
160
173
  </light-card>
161
174
  ```
162
175
 
163
- A stable but empty wrapper still fills its slot, so the slot's fallback content does not show.
176
+ A stable but empty wrapper still fills its slot, so the slot's fallback content does not show; when the fallback matters, use `shadow: true`.
164
177
  4. **New objects for changed props.** A Nativele component compares props by reference; pass a new object when the data changes (React state updates usually do).
165
- 5. **TSX types need the declaration** in step 5; without it, every Nativele tag is `TS2339: Property 'info-card' does not exist on type 'JSX.IntrinsicElements'`. Check it with a `.tsx` that has a wrong prop under `// @ts-expect-error`, because `skipLibCheck` can hide mistakes inside the declaration itself.
178
+ 5. **TSX types need the declaration** in step 7; without it, every Nativele tag is `TS2339: Property 'info-card' does not exist on type 'JSX.IntrinsicElements'`. Check it with a `.tsx` that has a wrong prop under `// @ts-expect-error`, because `skipLibCheck` can hide mistakes inside the declaration itself. Keep these cases in a `.tsx` file of their own that the app never imports (the example uses `src/types-check.tsx`).
166
179
  6. Not verified: server-side rendering, form controls inside Nativele components submitting with a React form, and React versions other than 19.3.
167
180
 
168
181
  React StrictMode is fine: it runs effects twice but does not insert the custom element twice, so `connected()` runs once, and `bind` subscriptions are released when React unmounts the element.
@@ -174,16 +187,19 @@ Run in the project:
174
187
  ```sh
175
188
  npx ts-dom check src/nv # expect: ts-dom check: no issues
176
189
  npx ts-dom doctor src/nv # expect: ts-dom doctor: the set-up looks right
177
- npx tsc --noEmit # expect: no errors (keep one @ts-expect-error case per component kind)
178
- npx vite build # expect: built
190
+ npx tsc --noEmit -p tsconfig.app.json # expect: no errors (keep one @ts-expect-error case per component kind)
191
+ npm run build # expect: ✓ built (it runs ts-dom gen, tsc -b and vite build)
179
192
  ```
180
193
 
181
- In the browser, render a component with an object prop and an event handler, and check that the element has no `item` attribute (`el.getAttributeNames()`), that `el.item` is the object, and that the handler fires once per click. The example project's `verify.mjs` does these checks with Playwright, plus children that React reorders, adds and removes inside a `shadow: true` component and inside stable wrappers in a Light DOM component.
194
+ Pass `tsc` the tsconfig from step 3. A root `tsconfig.json` that only lists `references` (as in Vite's template) checks nothing itself, so `tsc --noEmit` without `-p` passes even with type errors.
195
+
196
+ In the browser, render a component with an object prop and an event handler, and check that the element has no `item` attribute (`el.getAttributeNames()`), that `el.item` is the object, and that the handler fires once per click. The example project's [`verify.mjs`](https://github.com/Pixmi/nativele/blob/main/skills/nativele-react/example/verify.mjs) does these checks with Playwright, plus children that React reorders, adds and removes inside a `shadow: true` component and inside stable wrappers in a Light DOM component. A Light DOM component has no `<slot>` elements once rendered; check where the children ended up with `el.assignedNodes(name)`, where `""` is the default slot.
182
197
 
183
198
  ## Troubleshooting
184
199
 
185
200
  | Symptom | Cause | Fix |
186
201
  |---|---|---|
202
+ | `TS2307: Cannot find module './x.html.js'` from `npm run build` on a clean checkout | `tsc -b` runs before the generated files exist | start the script with `ts-dom gen src/nv &&` |
187
203
  | An object prop shows `[object Object]` or is missing | the component was rendered before `define()` ran | import the component module in the entry file before rendering |
188
204
  | The event handler never runs | the prop name is camelCased (`onItemSelect`) | use `on` plus the exact event name (`onitem-select`) |
189
205
  | `insertBefore`/`removeChild` errors, the React root disappears | React children changed inside a Light DOM component with slots | use `shadow: true`, or stable wrapper elements per slot |
@@ -0,0 +1,259 @@
1
+ ---
2
+ name: nativele-svelte
3
+ description: "Use Nativele web components inside an existing Svelte 5 app: passing props, listening to events, children and slots, lifecycle, template types with svelte-check, and ts-dom check. Use when a project depends on svelte and @nativele/ts-dom."
4
+ metadata:
5
+ tier: experimental
6
+ nativele: ">=0.5.1 <0.6.0"
7
+ verified: "2026-11-05"
8
+ tools: "svelte@5.57.3 vite@8.3.6 @sveltejs/vite-plugin-svelte@7.3.1 svelte-check@4.7.8 @tsconfig/svelte@5.0.8 typescript@6.0.3"
9
+ author: "Nativele maintainers"
10
+ detects: "svelte"
11
+ ---
12
+
13
+ # Nativele + Svelte
14
+
15
+ ## When to use
16
+
17
+ - The project renders with Svelte 5 (with Vite) and wants to use components written with Nativele (`@nativele/ts-dom`), for example a design-system layer or widgets shared with non-Svelte pages.
18
+ - Signals: `svelte` and `@nativele/ts-dom` in `package.json`, `.svelte` files that render tags with a hyphen (`<info-card>`).
19
+ - Not for Svelte 4 or earlier, and not for SvelteKit (server rendering and hydration were not verified). Not for compiling Svelte components into custom elements (`<svelte:options customElement>`): that is the opposite direction.
20
+ - Not for writing the Nativele components themselves: use `nativele-core` (`npx ts-dom skills add nativele-core`).
21
+
22
+ Nativele components are native custom elements. Svelte renders them like any other tag; nothing wraps them.
23
+
24
+ ## Versions
25
+
26
+ | Tool | Verified version |
27
+ |---|---|
28
+ | @nativele/ts-dom | 0.5.1 |
29
+ | svelte | 5.57.3 |
30
+ | @sveltejs/vite-plugin-svelte | 7.3.1 |
31
+ | svelte-check | 4.7.8 |
32
+ | @tsconfig/svelte | 5.0.8 |
33
+ | vite | 8.3.6 |
34
+ | typescript | 6.0.3 |
35
+
36
+ Verified on 2026-11-05 in Google Chrome 148, against the Vite dev server (so Svelte's development warnings would show), in a fresh app made with `npm create vite@latest -- --template svelte-ts`. Runes mode (`$state`) is what was verified; legacy mode was checked only for the `on:` directive (limitation 2). With a different major version of Svelte, treat every step as unverified.
37
+
38
+ ## Set-up
39
+
40
+ 1. Keep the Nativele components in their own folder, for example `src/nv/`, each as `x.html` plus `x.ts`. Write them with the core Skill: `npx ts-dom skills add nativele-core`.
41
+ 2. Install: `npm i @nativele/ts-dom`.
42
+ 3. Leave the tsconfig as it is: Vite's Svelte template has `tsconfig.app.json` extend `@tsconfig/svelte`, which sets `"strict": true`, and its `include` already covers `src/nv`. If the project turned `strict` off, set it back in that file.
43
+ 4. Add the Nativele Vite plugin before the Svelte plugin, pointing at that folder:
44
+
45
+ ```ts
46
+ // vite.config.ts
47
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
48
+ import { defineConfig } from "vite";
49
+ import { tsDom } from "@nativele/ts-dom/vite";
50
+
51
+ export default defineConfig({ plugins: [tsDom({ dirs: ["src/nv"] }), svelte()] });
52
+ ```
53
+
54
+ Pass the same folder to every `ts-dom` command (`npx ts-dom check src/nv`), so there is one `ts-dom-tags.ts`, in `src/nv`. Svelte needs no option to accept hyphenated tags.
55
+
56
+ 5. Add the generated files to `.gitignore`:
57
+
58
+ ```
59
+ *.html.ts
60
+ ts-dom-tags.ts
61
+ ```
62
+
63
+ 6. Import every Nativele component module in `src/main.ts`, before `mount`:
64
+
65
+ ```ts
66
+ // src/main.ts
67
+ import "./nv/info-card.js";
68
+ import "./nv/shadow-card.js";
69
+ import { mount } from "svelte";
70
+ import App from "./App.svelte";
71
+
72
+ mount(App, { target: document.getElementById("app")! });
73
+ ```
74
+
75
+ Svelte 5 sets a value as a property when the element is defined and has that property (limitation 1).
76
+
77
+ 7. Add the template declaration below as `src/nativele-svelte.d.ts`. It reads the tag map that `ts-dom gen` writes (`ts-dom-tags.ts`), so every exported component gets typed props and events in `.svelte` files, and new components need no edit:
78
+
79
+ ```ts
80
+ // src/nativele-svelte.d.ts
81
+ import type { HTMLAttributes } from "svelte/elements";
82
+
83
+ type NativeleTag = Extract<keyof HTMLElementTagNameMap, `${string}-${string}`>;
84
+ type NativeleProps<E> = E extends { readonly props: infer P } ? { [K in keyof P & keyof E]?: E[K] } : unknown;
85
+ type NativeleEvents<E> = E extends { readonly "~emits"?: infer M }
86
+ ? { [K in keyof NonNullable<M> & string as `on${K}`]?: (event: CustomEvent<NonNullable<M>[K]>) => void }
87
+ : unknown;
88
+ type NativeleElementProps<E extends HTMLElement> = Omit<HTMLAttributes<E>, keyof NativeleProps<E>> & NativeleProps<E> & NativeleEvents<E>;
89
+ type NativeleElements = { [K in NativeleTag]: NativeleElementProps<HTMLElementTagNameMap[K]> };
90
+
91
+ declare module "svelte/elements" {
92
+ interface SvelteHTMLElements extends NativeleElements {}
93
+ }
94
+ ```
95
+
96
+ The component class must be exported and passed to `define()` in a folder that `ts-dom gen` scans, so it appears in `ts-dom-tags.ts`.
97
+
98
+ 8. Prove the declaration types props with a `.ts` file that the app never imports, one wrong case per component kind under `// @ts-expect-error` (an expect-error comment in Svelte markup does not work, limitation 6):
99
+
100
+ ```ts
101
+ // src/types-check.ts
102
+ import type { SvelteHTMLElements } from "svelte/elements";
103
+
104
+ // @ts-expect-error an object prop given a string
105
+ export const badProp: SvelteHTMLElements["info-card"] = { item: "apple" };
106
+ // @ts-expect-error a prop name the component does not declare
107
+ export const badName: SvelteHTMLElements["info-card"] = { lable: "x" };
108
+ // @ts-expect-error a detail field the event does not have
109
+ export const badEvent: SvelteHTMLElements["info-card"] = { "onitem-select": (e) => e.detail.name };
110
+ ```
111
+
112
+ The generated files are not committed, so on a clean checkout the type check in `npm run check` runs before anything has written them and fails with `TS2307: Cannot find module './x.html.js'`. Generate them first: `"check": "ts-dom gen src/nv && svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"` (the template's script with `ts-dom gen src/nv &&` in front). `npm run build` needs no change: the Vite plugin generates the files.
113
+
114
+ ## Example
115
+
116
+ A Nativele component with props and an event:
117
+
118
+ ```html
119
+ <!-- src/nv/info-card.html -->
120
+ <span>{{ label }}</span>
121
+ <b>{{ count }}</b>
122
+ <i data-ref="itemName"></i>
123
+ <button type="button" data-on-click="pick">Pick</button>
124
+ ```
125
+
126
+ ```ts
127
+ // src/nv/info-card.ts
128
+ import { Component, define, event } from "@nativele/ts-dom";
129
+ import tpl from "./info-card.html.js";
130
+
131
+ export interface Item {
132
+ id: number;
133
+ name: string;
134
+ }
135
+
136
+ export class InfoCard extends Component(tpl, {
137
+ props: { label: "", count: 0, item: null as Item | null },
138
+ emits: { "item-select": event<{ id: number }>() },
139
+ }) {
140
+ pick(): void {
141
+ const item = this.props.item;
142
+ if (item) this.emit("item-select", { id: item.id });
143
+ }
144
+ override render(view: { item: Item | null }): void {
145
+ this.refs.itemName.textContent = view.item ? view.item.name : "-";
146
+ }
147
+ }
148
+ define("info-card", InfoCard);
149
+ ```
150
+
151
+ A component with slots, in Shadow DOM:
152
+
153
+ ```html
154
+ <!-- src/nv/shadow-card.html -->
155
+ <h2><slot name="title">Untitled</slot></h2>
156
+ <div><slot></slot></div>
157
+ ```
158
+
159
+ ```ts
160
+ // src/nv/shadow-card.ts
161
+ import { Component, define } from "@nativele/ts-dom";
162
+ import tpl from "./shadow-card.html.js";
163
+
164
+ export class ShadowCard extends Component(tpl, { shadow: true }) {}
165
+ define("shadow-card", ShadowCard);
166
+ ```
167
+
168
+ Used from Svelte:
169
+
170
+ ```svelte
171
+ <!-- src/App.svelte -->
172
+ <script lang="ts">
173
+ import type { Item } from "./nv/info-card.js";
174
+
175
+ let count = $state(0);
176
+ let item = $state<Item>({ id: 1, name: "apple" });
177
+ let picked = $state<number[]>([]);
178
+ let rows = $state(["a", "b", "c"]);
179
+ let showTitle = $state(true);
180
+ </script>
181
+
182
+ <info-card label="Fruit" {count} {item} onitem-select={(e) => picked.push(e.detail.id)}></info-card>
183
+ <button type="button" onclick={() => { count++; item = { ...item, name: "pear" }; }}>Next</button>
184
+
185
+ <shadow-card>
186
+ {#if showTitle}<span slot="title">Title</span>{/if}
187
+ {#each rows as r (r)}<p>{r}</p>{/each}
188
+ </shadow-card>
189
+ ```
190
+
191
+ - `label`, `count` and `item` arrive as properties; the element has no attribute for them. Changing them from Svelte state re-renders the Nativele component.
192
+ - The event attribute is `on` followed by the exact event name: `onitem-select`. `e.detail` is typed from the component's `emits`.
193
+ - In a `shadow: true` component, `{#if}` and keyed `{#each}` children can change freely.
194
+
195
+ ## Known limitations
196
+
197
+ 1. **Define before mount.** Import the component modules before `mount` (step 6). Svelte 5 decides per value: when the element is defined, it sets a property the element has; when it is not defined yet (for example a module loaded later with `import()`), it sets objects and arrays as properties and writes every other value as an attribute string. Nativele reads both when the element is upgraded, which was verified for string, number and boolean props with one-word names (`count={0}` becomes `count="0"`, `false` becomes `open="false"`, both arrive with the right type). Other cases were not verified, so define first.
198
+ 2. **Event attribute names.** Only `on` plus the exact event name works (`onitem-select`). `onItemSelect` listens to an event named `ItemSelect` and never fires; with the declaration it is also a `svelte-check` error. The legacy directive `on:item-select` fires too, but only use the event attributes: in a runes component `on:` is deprecated (compiler warning `event_directive_deprecated`), a component cannot mix it with event attributes (compile error `mixed_event_handler_syntaxes`), and the declaration in step 7 does not type it. Nativele events do not bubble, so put the handler on the element that emits.
199
+ 3. **Light DOM slots and Svelte-managed children do not mix.** In the default Light DOM mode (`Component(tpl)` with no `shadow`), Nativele moves the host's children into its slot positions, together with the comment nodes Svelte uses as anchors. Svelte does not throw, but it goes wrong silently: an `{#if}` that adds `<span slot="title">` back inserts it next to its anchor, which now sits in the default slot, so the title slot shows its fallback; and `assignedNodes()` stops matching what is on the page. Keyed `{#each}` reordering happened to look right, but do not rely on it. Use one of:
200
+ - `shadow: true` on the component (children can then change freely), or
201
+ - one stable element per slot, and let Svelte change only what is inside it:
202
+
203
+ ```svelte
204
+ <light-card>
205
+ <div slot="title">{#if showTitle}<span>Title</span>{/if}</div>
206
+ <div>{#each rows as r (r)}<p>{r}</p>{/each}</div>
207
+ </light-card>
208
+ ```
209
+
210
+ A stable but empty wrapper still fills its slot, so the slot's fallback content does not show; when the fallback matters, use `shadow: true`.
211
+ 4. **New objects for changed props.** A Nativele component compares props by reference. Changing a `$state` object in place (`item.name = "kiwi"`) does not re-render it; assign a new object (`item = { ...item, name: "kiwi" }`).
212
+ 5. **Write closing tags.** `<info-card ... />` works, but the Svelte compiler warns `element_invalid_self_closing_tag` (shown in the dev server's terminal); write `<info-card ...></info-card>`.
213
+ 6. **Template types need the declaration** in step 7. Without it, Svelte accepts any attribute on a hyphenated tag, and an inline handler's parameter is `any` (`Parameter 'e' implicitly has an 'any' type` under `strict`). With it, `svelte-check` reports a wrong prop type, an unknown prop name, a wrong `e.detail` field and a camelCase handler name. `<!-- @ts-expect-error -->` in markup does not silence an error, so keep the expected-error cases in a `.ts` file (step 8). To type an element reference, use the tag map: `let card: HTMLElementTagNameMap["info-card"] | undefined = $state()` with `bind:this={card}`.
214
+ 7. **`ts-dom check` does not read `.svelte` files.** It runs the project's `tsc`, which checks the Nativele folder and the `.ts` files (including `types-check.ts`); `.svelte` imports resolve through the `*.svelte` module declaration in Svelte's own types, so they cause no error, but the markup and scripts inside `.svelte` files are checked only by `svelte-check`. Run both (Verify).
215
+ 8. **Lifecycle.** Removing an element with `{#if}` runs `disconnected()` and ends its `bind` subscriptions; adding it back runs `connected()` and subscribes again. Reordering a keyed `{#each}` of Nativele elements moves them, which runs `disconnected()` and then `connected()`; each element keeps its slotted children, and every `bind` callback still runs once per store change.
216
+ 9. Not verified: SvelteKit, server rendering and hydration, form controls inside Nativele components submitting with a Svelte form, `$state.raw`, hot updates of `.svelte` files that contain Nativele tags, and Svelte versions other than 5.57.
217
+
218
+ ## Verify
219
+
220
+ Run in the project:
221
+
222
+ ```sh
223
+ npx ts-dom check src/nv # expect: ts-dom check: no issues
224
+ npx ts-dom doctor src/nv # expect: ts-dom doctor: the set-up looks right (info lines are suggestions)
225
+ npm run check # svelte-check plus tsc for vite.config.ts; expect: svelte-check found 0 errors and 0 warnings
226
+ npm run build # expect: built
227
+ ```
228
+
229
+ When `npm run check` is not run in a terminal (an agent, CI), `svelte-check` prints `COMPLETED ... 0 ERRORS 0 WARNINGS ...` instead. Delete `src/nativele-svelte.d.ts` once to see the check work: `svelte-check` must then report `Unused '@ts-expect-error' directive` in `src/types-check.ts`.
230
+
231
+ In the browser, against the dev server (production builds drop Svelte's warnings), render a component with an object prop and an event attribute, and check:
232
+
233
+ - the element has no attribute for the object prop (`el.getAttributeNames()`) and `el.item` is the object;
234
+ - the handler runs once per event;
235
+ - children that Svelte toggles with `{#if}` and reorders, adds and removes with keyed `{#each}` stay in the right slot, both in a `shadow: true` component and in stable wrappers in a Light DOM component, with no errors or warnings in the console. Read a slot with `el.assignedNodes(name)` (`""` is the default slot), which works in both modes; a Light DOM component has no `<slot>` elements once rendered. Svelte leaves comment nodes as anchors among the children, so compare elements, not all nodes.
236
+
237
+ ## Troubleshooting
238
+
239
+ | Symptom | Cause | Fix |
240
+ |---|---|---|
241
+ | `svelte-check` reports `Cannot find module './x.html.js'` on a clean checkout | it runs before the generated files exist | start the `check` script with `ts-dom gen src/nv &&` |
242
+ | The event handler never runs | the attribute is camelCased (`onItemSelect`), or it is on an ancestor | use `on` plus the exact event name (`onitem-select`) on the element that emits |
243
+ | `svelte-check`: `'"on:item-select"' does not exist in type 'NativeleElementProps<...>'` | the legacy `on:` directive, which the declaration does not type | use the event attribute `onitem-select` |
244
+ | `Mixing old (on:item-select) and new syntaxes for event handling is not allowed` | `on:` and event attributes in the same component | use only event attributes |
245
+ | `svelte-check`: `Parameter 'e' implicitly has an 'any' type` on a handler of a Nativele tag | the declaration file is missing, or the component class is not exported | add `src/nativele-svelte.d.ts`, export the class, run `npx ts-dom check src/nv` |
246
+ | `svelte-check`: `Unused '@ts-expect-error' directive` in `types-check.ts` | the declaration is not applied | same as above |
247
+ | A slotted child shows up in the wrong slot, or the fallback shows although Svelte rendered the child | Svelte-managed children directly inside a Light DOM component with slots | `shadow: true`, or a stable wrapper element per slot |
248
+ | The component does not update after `item.name = ...` | the object was changed in place; props compare by reference | assign a new object |
249
+ | `Self-closing HTML tags for non-void elements are ambiguous` | `<info-card />` | write `<info-card></info-card>` |
250
+ | `ts-dom check` reports `unused-ref` | a `data-ref` the class never reads | use it in the class or remove the attribute |
251
+
252
+ ## See also
253
+
254
+ - Components, props and events: https://github.com/Pixmi/nativele/blob/main/docs/components.md
255
+ - How attributes convert to props: https://github.com/Pixmi/nativele/blob/main/docs/components.md#data-flow-props-down-events-up
256
+ - Slots, Light DOM and Shadow DOM: https://github.com/Pixmi/nativele/blob/main/docs/components.md#light-default-and-shadow
257
+ - Stores and `bind`: https://github.com/Pixmi/nativele/blob/main/docs/components.md#state-store-when-not-using-state
258
+ - Setup and checks: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md
259
+ - The framework-neutral rules: `npx ts-dom skills nativele-integration`
@@ -0,0 +1,197 @@
1
+ ---
2
+ name: nativele-vite
3
+ description: "Add Nativele to an existing Vite app without a UI framework, and set up the tsDom() Vite plugin correctly: dirs and Vite's root, multi-page apps, the build script, and what vite dev does when a template or component changes. Use when a project depends on vite (or vite-plus) and @nativele/ts-dom."
4
+ metadata:
5
+ tier: official
6
+ nativele: ">=0.5.1 <0.6.0"
7
+ verified: "2026-11-05"
8
+ tools: "vite@8.3.6 typescript@7.0.2 create-vite@9.3.4 vite-plus@1.5.0"
9
+ author: "Nativele maintainers"
10
+ detects: "vite vite-plus"
11
+ ---
12
+
13
+ # Nativele + Vite
14
+
15
+ ## When to use
16
+
17
+ - A Vite app with no UI framework (for example one started with `npm create vite@latest -- --template vanilla-ts`) that adds components written with Nativele (`@nativele/ts-dom`).
18
+ - Any Vite project with Nativele whose set-up goes beyond the defaults: `root` in a sub-folder, several HTML pages, Vite+, or questions about what `vite dev` does when a template changes.
19
+ - Signals: `vite` or `vite-plus` and `@nativele/ts-dom` in `package.json`, a `vite.config.ts`.
20
+ - With React, follow `nativele-react`; with another framework, `nativele-integration`. The parts about `dirs`, `root` and the build script apply there too, but the hot-update behavior below was verified without a framework.
21
+ - Not for writing the components themselves (`npx ts-dom skills add nativele-core`), and not for setting Vite up.
22
+
23
+ ## Versions
24
+
25
+ | Tool | Verified version |
26
+ |---|---|
27
+ | @nativele/ts-dom | 0.5.1 |
28
+ | vite | 8.3.6 |
29
+ | typescript | 7.0.2 |
30
+ | create-vite (`vanilla-ts` template, the starting point) | 9.3.4 |
31
+ | vite-plus | 1.5.0 (checked by hand with `vp dev`, `vp build` and `vp lint`; the example project does not run it) |
32
+
33
+ Verified on 2026-11-05 in Google Chrome 148 with the example project in the Nativele repository (`skills/nativele-vite/example`), which CI runs again when the example or the library changes and once a month. With a different major version of Vite, treat every step as unverified.
34
+
35
+ ## Set-up
36
+
37
+ Starting from the `vanilla-ts` template, which already has `"type": "module"`, a `tsconfig.json` with `"include": ["src"]` and no `strict`, no `vite.config.ts`, and `"build": "tsc && vite build"`:
38
+
39
+ 1. `npm i @nativele/ts-dom`.
40
+ 2. Keep the components in their own folder, for example `src/nv/`, each as `x.html` plus `x.ts` (write them with `nativele-core`).
41
+ 3. Add `"strict": true` to `compilerOptions` in `tsconfig.json`. The template's other options (`verbatimModuleSyntax`, `erasableSyntaxOnly`, `noUnusedLocals`, `allowImportingTsExtensions`) work with the generated files as they are, and `"include": ["src"]` already covers `src/nv`.
42
+ 4. Create `vite.config.ts` (the template has none, so `ts-dom doctor` cannot report the plugin as missing yet):
43
+
44
+ ```ts
45
+ import { defineConfig } from "vite";
46
+ import { tsDom } from "@nativele/ts-dom/vite";
47
+
48
+ export default defineConfig({ plugins: [tsDom({ dirs: ["src/nv"] })] });
49
+ ```
50
+
51
+ `dirs` is resolved against Vite's `root`, not against the folder you run commands in (see step 8). The other options are in the setup guide.
52
+
53
+ 5. Replace `tsc` in the build script with `ts-dom check`, given the same folder:
54
+
55
+ ```sh
56
+ npm pkg set scripts.build="ts-dom check src/nv && vite build"
57
+ ```
58
+
59
+ The generated files are not committed, so on a clean checkout `tsc` runs before anything has written them and fails with `TS2307: Cannot find module './x.html.js'` and follow-on errors. `ts-dom check` writes them, then runs `tsc` and the lint rules. `vite build` writes them too, but it never type-checks.
60
+
61
+ 6. Add `*.html.ts` and `ts-dom-tags.ts` to `.gitignore`.
62
+ 7. Import each page's components from that page's entry module, and write the tags straight into the page's HTML (`<hello-card name="home"></hello-card>`; attributes reach string props). Import the component modules the way the project imports its own (`./nv/hello-card.ts` in this template), and templates always as `./x.html.js`.
63
+ 8. When `root` is a sub-folder (a multi-page app or a monorepo app kept in `web/`), each path is relative to a different folder:
64
+
65
+ | Setting | Relative to | With `root: "web"` |
66
+ |---|---|---|
67
+ | `tsDom({ dirs })` | Vite's `root` | `["src/nv"]` |
68
+ | `ts-dom check`, `doctor`, `gen`, `lint` arguments | the folder they run in (the one with `package.json` and `tsconfig.json`) | `web/src/nv` |
69
+ | `include` in `tsconfig.json` | the tsconfig's folder | `["web/src"]` |
70
+ | `build.rolldownOptions.input` | write absolute paths (see Troubleshooting) | `resolve(import.meta.dirname, "web/about.html")` |
71
+
72
+ Nativele needs nothing per page: each page's entry imports the components that page uses.
73
+
74
+ 9. With Vite+ instead of Vite, import `defineConfig` from `"vite-plus"`; the plugin, `dirs` and the build script are the same, and the lint settings in the setup guide work with `vp lint`. If `vp build` then stops with `Cannot find package 'vite'`, see Troubleshooting.
75
+
76
+ ## Example
77
+
78
+ A two-page app with `root: "web"`, as in the example project:
79
+
80
+ ```
81
+ package.json tsconfig.json vite.config.ts
82
+ web/index.html web/about.html
83
+ web/src/main.ts web/src/about.ts
84
+ web/src/nv/hello-card.html web/src/nv/hello-card.ts web/src/nv/about-card.html web/src/nv/about-card.ts
85
+ ```
86
+
87
+ ```jsonc
88
+ // package.json (scripts)
89
+ "dev": "vite",
90
+ "build": "ts-dom check web/src/nv && vite build"
91
+ ```
92
+
93
+ ```jsonc
94
+ // tsconfig.json: the template's, with "strict": true and
95
+ "include": ["web/src"]
96
+ ```
97
+
98
+ ```ts
99
+ // vite.config.ts
100
+ import { resolve } from "node:path";
101
+ import { defineConfig } from "vite";
102
+ import { tsDom } from "@nativele/ts-dom/vite";
103
+
104
+ const page = (file: string) => resolve(import.meta.dirname, "web", file);
105
+
106
+ export default defineConfig({
107
+ root: "web",
108
+ plugins: [tsDom({ dirs: ["src/nv"] })],
109
+ build: {
110
+ outDir: "../dist",
111
+ emptyOutDir: true,
112
+ rolldownOptions: { input: { main: page("index.html"), about: page("about.html") } },
113
+ },
114
+ });
115
+ ```
116
+
117
+ ```html
118
+ <!-- web/index.html (body); web/about.html has <about-card></about-card> and /src/about.ts -->
119
+ <hello-card name="home"></hello-card>
120
+ <script type="module" src="/src/main.ts"></script>
121
+ ```
122
+
123
+ ```ts
124
+ // web/src/main.ts
125
+ import "./nv/hello-card.ts";
126
+ ```
127
+
128
+ ```html
129
+ <!-- web/src/nv/hello-card.html -->
130
+ <p>Hello, {{ name }}</p>
131
+ <button type="button" data-on-click="bump">clicked {{ count }}</button>
132
+ ```
133
+
134
+ ```ts
135
+ // web/src/nv/hello-card.ts
136
+ import { Component, define } from "@nativele/ts-dom";
137
+ import tpl from "./hello-card.html.js";
138
+
139
+ export class HelloCard extends Component(tpl, { props: { name: "" }, state: { count: 0 } }) {
140
+ bump(): void {
141
+ this.set({ count: this.state.count + 1 });
142
+ }
143
+ }
144
+ define("hello-card", HelloCard);
145
+ ```
146
+
147
+ ## Known limitations
148
+
149
+ 1. **Every change reloads the page; component state is lost.** A custom element tag can be defined only once, so there is no hot swap of a component. During `vite dev`:
150
+
151
+ | Change | What happens |
152
+ |---|---|
153
+ | A template is edited | its `x.html.ts` is regenerated and the browser reloads; the terminal prints `[vite] (client) page reload src/nv/x.html` |
154
+ | A template is saved with no change | no reload (the terminal still prints the `page reload` line) |
155
+ | A template no page has loaded since the dev server started is edited | no reload |
156
+ | A template some page has loaded is edited | every open page reloads, also pages that do not use it |
157
+ | A template that `ts-dom gen` rejects is saved | no reload: the page keeps the last good version, and the error (`file:line:col error [rule-id] ... Fix: ...`) is printed only in the terminal, with no overlay in the browser |
158
+ | A template uses a name the component does not have (`{{ nope }}`) | it is regenerated and reloads, the hole stays empty, and nothing reports it: only `ts-dom check` does |
159
+ | A component `.ts` is edited | Vite reloads the page, and the new class runs |
160
+
161
+ 2. **Do not call `import.meta.hot.accept()` in a module that imports components.** Vite then re-runs the modules instead of reloading, `define()` finds the tag already defined and keeps the old class, and the edit silently has no effect until a manual reload.
162
+ 3. **Neither `vite dev` nor `vite build` type-checks.** Run `ts-dom check` (the build script in step 5 does), and run it after a template change that `vite dev` accepted without complaint.
163
+ 4. **A wrong `dirs` fails without a warning from the plugin.** If `dirs` does not point at the templates from Vite's `root` (for example `["web/src/nv"]` with `root: "web"`), nothing is generated, and the first sign is an unresolved `./x.html.js` (see Troubleshooting).
164
+ 5. **Pass one folder to every `ts-dom` command.** `ts-dom-tags.ts` is written into the folder a command is given, so `ts-dom check web` next to `ts-dom check web/src/nv` leaves a second `ts-dom-tags.ts` in `web/`; delete it and use the same folder everywhere.
165
+ 6. Not verified: hot updates under a framework plugin (React Fast Refresh, Vue), server-side rendering, Vite library mode, an npm workspaces monorepo where the app has its own `package.json` (only `root` in a sub-folder of one package was verified), and Vite versions other than 8.3.
166
+
167
+ ## Verify
168
+
169
+ Run in the project (with `root: "web"`, the folder is `web/src/nv`):
170
+
171
+ ```sh
172
+ npm run build # expect: ts-dom check: no issues, then ✓ built, and one HTML file per page in the output
173
+ npx ts-dom doctor src/nv # expect: ts-dom doctor: the set-up looks right
174
+ ```
175
+
176
+ Then run `npm run dev`, open every page and check that its components render and respond. Edit a template and check that the page reloads once and shows the change. The example project's [`verify.mjs`](https://github.com/Pixmi/nativele/blob/main/skills/nativele-vite/example/verify.mjs) checks with Playwright that `vite build` alone writes the generated files, that both built pages work under `vite preview`, and every row of the table in Known limitations, restoring the files it edits.
177
+
178
+ ## Troubleshooting
179
+
180
+ | Symptom | Cause | Fix |
181
+ |---|---|---|
182
+ | `TS2307: Cannot find module './x.html.js'` from `npm run build` on a clean checkout | the build script starts with `tsc`, before anything generated the files | `"build": "ts-dom check <folder> && vite build"` |
183
+ | `vite build`: `[UNRESOLVED_IMPORT] Could not resolve './x.html.js'`; `vite dev`: `Failed to resolve import "./x.html.js"` and a 500 for the module | `dirs` is not relative to Vite's `root`, or leaves out the folder | give `dirs` relative to `root` (`["src/nv"]` with `root: "web"`) |
184
+ | `ts-dom check`: `[dir-not-found] The folder src does not exist` | the commands default to `src` in the folder they run in | pass the folder from there: `ts-dom check web/src/nv` |
185
+ | `ts-dom doctor`: `[tsconfig-misses-templates]`; `ts-dom check`: `TS18003: No inputs were found` | `include` is relative to the tsconfig, not to Vite's `root` | `"include": ["web/src"]` |
186
+ | An edit to a component has no effect until the page is reloaded by hand | a module calls `import.meta.hot.accept()` | remove it |
187
+ | A template edit never reaches the page, and the terminal shows `error [rule-id]` | the template breaks a template rule | fix it by the `Fix:` hint; once the template generates again, the page shows it |
188
+ | `vite dev`: `(!) Failed to run dependency scan ... failed to resolve rolldownOptions.input value: "web/index.html"` | `vite dev` resolves a relative `input` against `root` | write absolute paths, as in the example |
189
+ | Vite warns `ESM syntax in a file loaded as CommonJS (vite.config.ts:1:1)` | `package.json` has no `"type": "module"` | `npm pkg set type=module` |
190
+ | `vp build`: `Cannot find package 'vite' imported from .../vitest/dist/config.js` | npm left `vite` nested under `vite-plus` on the first install (seen with npm 11.19, also without Nativele) | run `npm i` once more |
191
+
192
+ ## See also
193
+
194
+ - The Vite plugin, its options and the dev server: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md#with-vite-optional
195
+ - Lint settings for `vite.config.ts` (Vite+): https://github.com/Pixmi/nativele/blob/main/docs/tooling.md#lint-optional
196
+ - What `ts-dom check`, `doctor` and `gen` check and write: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md#checks
197
+ - Writing the components: `npx ts-dom skills nativele-core`