@nativele/ts-dom 0.5.5 → 0.6.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 +60 -0
- package/README.md +5 -0
- package/README.zh-TW.md +5 -0
- package/dist/{a11y-CiEJj_e6.mjs → a11y-KXbo-8o3.mjs} +119 -27
- package/dist/cli.mjs +273 -50
- package/dist/{lint-Did4w-NL.mjs → lint-DUsdDKZ-.mjs} +33 -8
- package/dist/oxlint-plugin.mjs +3 -3
- package/dist/vite-plugin.mjs +1 -1
- package/docs/architecture.md +188 -0
- package/docs/components.md +367 -0
- package/docs/contributing.md +80 -0
- package/docs/design.md +55 -0
- package/docs/lists.md +150 -0
- package/docs/performance.md +24 -0
- package/docs/recipes.md +189 -0
- package/docs/templates.md +73 -0
- package/docs/tooling.md +251 -0
- package/llms.txt +5 -5
- package/package.json +2 -1
- package/skills/README.md +19 -2
- package/skills/nativele-angular/SKILL.md +15 -11
- package/skills/nativele-architecture/SKILL.md +14 -11
- package/skills/nativele-ci/SKILL.md +148 -0
- package/skills/nativele-core/SKILL.md +16 -13
- package/skills/nativele-formatting/SKILL.md +167 -0
- package/skills/nativele-integration/SKILL.md +11 -8
- package/skills/nativele-linting/SKILL.md +174 -0
- package/skills/nativele-monorepo/SKILL.md +298 -0
- package/skills/nativele-react/SKILL.md +12 -9
- package/skills/nativele-state-management/SKILL.md +4 -4
- package/skills/nativele-svelte/SKILL.md +10 -8
- package/skills/nativele-tailwind/SKILL.md +8 -7
- package/skills/nativele-tanstack-query/SKILL.md +5 -5
- package/skills/nativele-testing/SKILL.md +19 -13
- package/skills/nativele-typescript/SKILL.md +186 -0
- package/skills/nativele-vite/SKILL.md +18 -14
- package/skills/nativele-vue/SKILL.md +9 -7
- package/skills/nativele-zod/SKILL.md +6 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as analyze, g as scanDefines, p as listHtml } from "./a11y-
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { basename, dirname, relative } from "node:path";
|
|
1
|
+
import { a as analyze, g as scanDefines, p as listHtml } from "./a11y-KXbo-8o3.mjs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
// src/lib/lint.ts
|
|
5
5
|
const esc = (s) => s.replace(/[$]/g, "\\$&");
|
|
6
6
|
/** Marker that templateCode() appends when it sees `Component(tpl, { state | props | derive })`. */
|
|
@@ -224,13 +224,20 @@ function kindOf(valueText) {
|
|
|
224
224
|
if (/^(true|false)$/.test(v)) return "boolean";
|
|
225
225
|
return "other";
|
|
226
226
|
}
|
|
227
|
+
/** The nearest folder at or above `file` that holds a package.json, or "" when there is none. */
|
|
228
|
+
function packageOf(file) {
|
|
229
|
+
for (let dir = dirname(resolve(file));; dir = dirname(dir)) {
|
|
230
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
231
|
+
if (dirname(dir) === dir) return "";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
227
234
|
/**
|
|
228
235
|
* Read the declared props and emitted events of every component defined (via define("tag", Class)) under `dirs`.
|
|
229
236
|
* This is what lets the linter check the attributes and data-on-* written on a child tag in a parent template.
|
|
230
237
|
*/
|
|
231
238
|
function contractsFor(dirs) {
|
|
232
239
|
const out = /* @__PURE__ */ new Map();
|
|
233
|
-
for (const
|
|
240
|
+
for (const def of scanDefines(dirs)) {
|
|
234
241
|
const code = readFileSync(def.file, "utf8");
|
|
235
242
|
const m = new RegExp(`class\\s+${esc(def.className)}\\s+extends\\s+Component\\s*\\(`).exec(code);
|
|
236
243
|
if (!m) continue;
|
|
@@ -238,10 +245,28 @@ function contractsFor(dirs) {
|
|
|
238
245
|
const close = matchClose(code, open, "(", ")");
|
|
239
246
|
if (close < 0) continue;
|
|
240
247
|
const call = code.slice(open, close + 1);
|
|
241
|
-
|
|
248
|
+
const contract = {
|
|
242
249
|
props: new Map(optionEntries(call, "props").map((e) => [e.key, kindOf(e.value)])),
|
|
243
250
|
emits: new Set(optionKeys(call, "emits"))
|
|
244
|
-
}
|
|
251
|
+
};
|
|
252
|
+
out.set(def.tag, [...out.get(def.tag) ?? [], {
|
|
253
|
+
pkg: packageOf(def.file),
|
|
254
|
+
contract
|
|
255
|
+
}]);
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The contracts that apply to a template in `file`. A tag defined once uses that definition. A tag defined several times
|
|
261
|
+
* (separate apps or packages in one folder) uses the one in the same package as `file`, and is left out when that is not
|
|
262
|
+
* exactly one, so a template is never checked against a component it does not use.
|
|
263
|
+
*/
|
|
264
|
+
function contractsAt(all, file) {
|
|
265
|
+
const pkg = packageOf(file);
|
|
266
|
+
const out = /* @__PURE__ */ new Map();
|
|
267
|
+
for (const [tag, defs] of all) {
|
|
268
|
+
const own = defs.length === 1 ? defs : defs.filter((d) => d.pkg === pkg);
|
|
269
|
+
if (own.length === 1) out.set(tag, own[0].contract);
|
|
245
270
|
}
|
|
246
271
|
return out;
|
|
247
272
|
}
|
|
@@ -358,7 +383,7 @@ function lintDataFlow(dirs, tsFiles) {
|
|
|
358
383
|
const file = relative(process.cwd(), html);
|
|
359
384
|
let classes = [];
|
|
360
385
|
for (const [f, src] of sources) if (dirname(f) === dirname(html) && importsHtml(src, name)) classes = classes.concat(componentClasses(src, spec));
|
|
361
|
-
for (const d of dataFlow(readFileSync(html, "utf8"), classes, [], contracts)) issues.push({
|
|
386
|
+
for (const d of dataFlow(readFileSync(html, "utf8"), classes, [], contractsAt(contracts, html))) issues.push({
|
|
362
387
|
file,
|
|
363
388
|
...d
|
|
364
389
|
});
|
|
@@ -369,4 +394,4 @@ function lintDataFlow(dirs, tsFiles) {
|
|
|
369
394
|
function formatIssue(i) {
|
|
370
395
|
return `${i.file}:${i.line}:${i.col} ${i.severity} [${i.rule}] ${i.message}`;
|
|
371
396
|
}
|
|
372
|
-
export {
|
|
397
|
+
export { contractsFor as a, lintDataFlow as c, unusedMarkup as d, contractsAt as i, lintUsage as l, UNUSED_MARKUP_RULES as n, dataFlow as o, componentClasses as r, formatIssue as s, DATA_FLOW_RULES as t, templateCode as u };
|
package/dist/oxlint-plugin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as checkA11y, t as A11Y_RULES } from "./a11y-
|
|
2
|
-
import { a as
|
|
1
|
+
import { n as checkA11y, t as A11Y_RULES } from "./a11y-KXbo-8o3.mjs";
|
|
2
|
+
import { a as contractsFor, d as unusedMarkup, i as contractsAt, n as UNUSED_MARKUP_RULES, o as dataFlow, r as componentClasses, t as DATA_FLOW_RULES, u as templateCode } from "./lint-DUsdDKZ-.mjs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
// src/lib/oxlint-plugin.ts
|
|
@@ -19,7 +19,7 @@ function groupDiagnostics(group, html, code, spec, filename) {
|
|
|
19
19
|
const key = `${group}\0${filename}\0${spec}`;
|
|
20
20
|
const hit = cache.get(key);
|
|
21
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());
|
|
22
|
+
const diags = group === "unused-markup" ? unusedMarkup(html, templateCode(code, spec)) : group === "a11y" ? checkA11y(html) : dataFlow(html, componentClasses(code, spec), [], contractsAt(getContracts(), filename));
|
|
23
23
|
cache.set(key, {
|
|
24
24
|
code,
|
|
25
25
|
html,
|
package/dist/vite-plugin.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as formatDiagnostics, d as isFullPage, f as isSelected, g as scanDefines, i as CodegenError, l as generateDirs, n as checkA11y, p as listHtml, u as generateFile } from "./a11y-
|
|
1
|
+
import { c as formatDiagnostics, d as isFullPage, f as isSelected, g as scanDefines, i as CodegenError, l as generateDirs, n as checkA11y, p as listHtml, u as generateFile } from "./a11y-KXbo-8o3.mjs";
|
|
2
2
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
4
|
// src/lib/vite-plugin.ts
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Application architecture
|
|
2
|
+
|
|
3
|
+
Part of the [Nativele](../README.md) documentation.
|
|
4
|
+
|
|
5
|
+
Nativele provides components. An application also needs a router, shared data, data loading and tests, and those come from other tools or from a few lines of your own code. This guide shows how to lay out a medium to large app so that the pieces stay apart and the checks keep working as it grows. Everything here is taken from the example project of the [architecture Skill](../skills/nativele-architecture/SKILL.md) (`skills/nativele-architecture/example`), a small project tracker (a list of projects, a project page with its tasks, a settings page) built with Vite, whose layout, routing and stores are checked by tests in happy-dom, in Chrome and against the built app.
|
|
6
|
+
|
|
7
|
+
## Folders by feature
|
|
8
|
+
|
|
9
|
+
Group the code by feature, not by kind of file:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
index.html # the shell's tags and the outlet the router fills
|
|
13
|
+
src/
|
|
14
|
+
main.ts # imports the shell and starts the router
|
|
15
|
+
app/ # knows every feature: the router, the shell, app-level stores
|
|
16
|
+
router.ts current-path.ts app-nav.html app-nav.ts route-message.html route-message.ts
|
|
17
|
+
shared/ # knows no feature
|
|
18
|
+
text-entry.html text-entry.ts progress-meter.html progress-meter.ts
|
|
19
|
+
stores/preferences.ts # an app-wide store
|
|
20
|
+
features/
|
|
21
|
+
projects/ # one feature: its pages, its components, its store, its tests
|
|
22
|
+
projects-page.* project-page.* project-card.* task-row.*
|
|
23
|
+
projects-store.ts
|
|
24
|
+
projects-store.test.ts projects-page.test.ts project-page.test.ts project-page.browser.test.ts
|
|
25
|
+
settings/
|
|
26
|
+
settings-page.html settings-page.ts settings-page.test.ts
|
|
27
|
+
ts-dom-tags.ts # generated
|
|
28
|
+
e2e/
|
|
29
|
+
app.spec.ts # tests of the built app
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **A feature folder** holds what changes together: its page components, the components only it uses (`project-card`, `task-row`), its store, and the tests of all of them, next to the code.
|
|
33
|
+
- **`shared/`** holds what several features use. A shared component's interface is its props and its events: `text-entry` takes `label` and `action` and emits `entry-submit` with the text, and the projects list and the project page each decide what that text becomes. It imports no feature and no feature store, so any feature can place it. App-wide stores live in `shared/stores/`.
|
|
34
|
+
- **`app/`** is the only code that knows every feature: the router's route table, the shell (`app-nav`), and stores that belong to the app itself, such as the current path.
|
|
35
|
+
- **Imports go one way**: `app` → `features` → `shared`. A feature does not import another feature; when two features need the same component or store, move it to `shared/`. Kept this way, a feature's page modules pull in only that feature and `shared/`: in the example, opening the settings page loads the main chunk and the settings page's chunk, nothing of the projects feature.
|
|
36
|
+
- **A template uses another component by its tag**, in any of these folders. `ts-dom gen` adds the import of the module that defines the tag to the generated `x.html.ts` (`import "../../shared/text-entry.js"` in `project-page.html.ts`), so a page module brings its components with it. This works across folders only inside one scanned folder (see [Generated files, checks and tsconfig](#generated-files-checks-and-tsconfig)).
|
|
37
|
+
- **One registry**: every tag in the app shares one custom element registry. `ts-dom check` reports a tag defined twice in the scanned folders as `duplicate-define`: the first `define()` to run wins and the other is skipped without an error. Names that say their feature (`project-card`, `task-row`) keep tags apart as the app grows.
|
|
38
|
+
|
|
39
|
+
## Pages and routing
|
|
40
|
+
|
|
41
|
+
Nativele has no router. A page is a component like any other, the router is code you choose, and the two meet at a small contract:
|
|
42
|
+
|
|
43
|
+
1. A route maps a URL pattern to a page tag and a function that imports the page's module.
|
|
44
|
+
2. On navigation, the router awaits that import, which runs the module's `define()`. It then creates the element with `document.createElement(tag)`, sets the route params as props, and replaces the content of the outlet with it.
|
|
45
|
+
3. If another navigation started while the import was still loading, it mounts nothing.
|
|
46
|
+
|
|
47
|
+
The example's router uses the browser's Navigation API and `URLPattern`, with no dependency:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// src/app/router.ts
|
|
51
|
+
import { currentPath } from "./current-path.ts";
|
|
52
|
+
import "./route-message.ts";
|
|
53
|
+
|
|
54
|
+
type Tag = keyof HTMLElementTagNameMap;
|
|
55
|
+
type Params = Record<string, string | undefined>;
|
|
56
|
+
|
|
57
|
+
interface Route {
|
|
58
|
+
pattern: URLPattern;
|
|
59
|
+
tag: Tag;
|
|
60
|
+
load: () => Promise<unknown>;
|
|
61
|
+
props: (params: Params) => object;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function page<K extends Tag>(
|
|
65
|
+
pathname: string,
|
|
66
|
+
tag: K,
|
|
67
|
+
load: () => Promise<unknown>,
|
|
68
|
+
props: (params: Params) => Partial<HTMLElementTagNameMap[K]> = () => ({}),
|
|
69
|
+
): Route {
|
|
70
|
+
return { pattern: new URLPattern({ pathname }), tag, load, props };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const routes: Route[] = [
|
|
74
|
+
page("/", "projects-page", () => import("../features/projects/projects-page.ts")),
|
|
75
|
+
page("/projects/:id", "project-page", () => import("../features/projects/project-page.ts"), (p) => ({ projectId: p.id ?? "" })),
|
|
76
|
+
page("/settings", "settings-page", () => import("../features/settings/settings-page.ts")),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
function mount<K extends Tag>(outlet: Element, tag: K, props: Partial<HTMLElementTagNameMap[K]>): void {
|
|
80
|
+
const el = document.createElement(tag);
|
|
81
|
+
Object.assign(el, props);
|
|
82
|
+
outlet.replaceChildren(el);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function loaded(route: Route): Promise<boolean> {
|
|
86
|
+
try {
|
|
87
|
+
await route.load();
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function show(outlet: Element, url: string, signal?: AbortSignal): Promise<void> {
|
|
95
|
+
const route = routes.find((r) => r.pattern.test(url));
|
|
96
|
+
const ok = route !== undefined && (await loaded(route));
|
|
97
|
+
if (signal?.aborted) return;
|
|
98
|
+
const path = new URL(url).pathname;
|
|
99
|
+
if (route && ok) mount(outlet, route.tag, route.props(route.pattern.exec(url)?.pathname.groups ?? {}));
|
|
100
|
+
else if (route) mount(outlet, "route-message", { heading: "This page could not be loaded", detail: "Check the connection and reload the page." });
|
|
101
|
+
else mount(outlet, "route-message", { heading: "Page not found", detail: `Nothing lives at ${path}.` });
|
|
102
|
+
currentPath.set(path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function startRouter(outlet: Element): Promise<void> {
|
|
106
|
+
if ("navigation" in window) {
|
|
107
|
+
navigation.addEventListener("navigate", (e) => {
|
|
108
|
+
if (!e.canIntercept || e.hashChange || e.downloadRequest !== null || e.formData !== null) return;
|
|
109
|
+
e.intercept({ handler: () => show(outlet, e.destination.url, e.signal) });
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return show(outlet, location.href);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```html
|
|
117
|
+
<!-- index.html (body) -->
|
|
118
|
+
<app-nav></app-nav>
|
|
119
|
+
<main id="outlet"></main>
|
|
120
|
+
<script type="module" src="/src/main.ts"></script>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// src/main.ts
|
|
125
|
+
import "./app/app-nav.ts";
|
|
126
|
+
import { startRouter } from "./app/router.ts";
|
|
127
|
+
|
|
128
|
+
void startRouter(document.querySelector("#outlet")!);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
What each part does, as the example's tests show:
|
|
132
|
+
|
|
133
|
+
- **Lazy pages**: each `import()` becomes its own chunk in the build, and a page's chunk is requested only when its route is first shown. The route table names pages by tag, and `Partial<HTMLElementTagNameMap[K]>` types their props through the generated `ts-dom-tags.ts`, which imports the page classes as types only: `tsc` checks `projectId` against the page's props, and the page module stays out of the main chunk.
|
|
134
|
+
- **Define before mounting**: the page module calls `define()` when it is imported, so once `load()` has resolved, `document.createElement(tag)` returns the page's class, and the props set on it are typed properties.
|
|
135
|
+
- **Route params are props**: URL params are strings, so the page declares them as string props (`props: { projectId: "" }`) and decides itself what an unknown id shows. Each navigation creates a new page element.
|
|
136
|
+
- **Leaving a page**: `replaceChildren` removes the old page, and removal ends every `bind`, `on` and `onCleanup` it registered: a removed page no longer renders when its stores change. A page bound in `connected()` that is shown again is a new element and starts from the stores' current values. Data that must outlive a page therefore belongs in a store, not in the page's `state`.
|
|
137
|
+
- **Superseded navigations**: without the `signal.aborted` check, a slow page module that finishes loading after the user has gone on mounts over the newer page while the address bar shows the newer URL.
|
|
138
|
+
- **A failed import** (a chunk that cannot be fetched) shows a message in the outlet instead of leaving the old page under the new URL.
|
|
139
|
+
- **What the Navigation API handles**: clicks on links, back and forward, and `navigation.navigate()` all arrive as `navigate` events, with no click handler of your own, and a modifier-click still opens a new tab.
|
|
140
|
+
- **Without the Navigation API**, `startRouter` attaches nothing: links load the next document, and the router renders the route at start-up. `URLPattern` has no such fallback: a browser without it stops at start-up with `URLPattern is not defined`. The example was verified in Chrome only.
|
|
141
|
+
- **The server** must answer every app path with `index.html` (deep links, reloads, and every link when the Navigation API is missing). `vite dev` and `vite preview` do this; configure the production server the same way.
|
|
142
|
+
|
|
143
|
+
The app shell shows the current section from a store that the router sets (`currentPath`), so the shell needs no props from the pages.
|
|
144
|
+
|
|
145
|
+
A router library works with the same contract: call `show()` from its route handler, or resolve the route with it and keep `mount()` and the abort check. A History API router does not intercept links, so it needs a click handler and a `popstate` listener of its own; the architecture Skill shows one with `universal-router`.
|
|
146
|
+
|
|
147
|
+
## Where state lives
|
|
148
|
+
|
|
149
|
+
| The data is | It lives in | Its lifetime | In the example |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| shown and changed by one component, or by it and the children it passes props to | the component's `state` (and `derive`) | the element: gone when the page is left | the settings page's "Saved." flag; the project page's sorted, filtered tasks (derived) |
|
|
152
|
+
| shared by the pages and components of one feature | a feature store: `createStore` in the feature folder, with its actions as functions | the module: created when the feature is first loaded, kept until the document is reloaded | `features/projects/projects-store.ts`: projects and tasks |
|
|
153
|
+
| read by the shell or by several features | an app-wide store in `shared/stores/` (or `app/` when only the app uses it) | the session | `preferences` (user name, show done tasks); `currentPath` |
|
|
154
|
+
| already kept in Zustand, Redux, nanostores or another library, shared with code that is not Nativele, or in need of a library's middleware, devtools or persistence | that library's store, connected through `bind` | the library's | see the [state management Skill](../skills/nativele-state-management/SKILL.md) |
|
|
155
|
+
|
|
156
|
+
- A store module exports the store and its actions (`addProject`, `addTask`, `toggleTask`) as plain functions. Components call the actions from their `data-on-*` handlers and `bind` the store in `connected()`; tests call the same actions with no element at all.
|
|
157
|
+
- Props cross one layer: the projects page gives each `project-card` its summary through `data-prop`. Data that a deeper component or another feature needs comes from a store, which is how the settings page's "show done tasks" reaches the project page without a prop on the way.
|
|
158
|
+
- Update stores immutably (`{ ...task, done: true }`), because lists and child props compare items by reference.
|
|
159
|
+
- Data from a server enters through a store module too: [Loading and error states](recipes.md#loading-and-error-states) shows the status, the error and the retry; a data library such as TanStack Query has its own Skill (`npx ts-dom skills nativele-tanstack-query`).
|
|
160
|
+
|
|
161
|
+
The rules for `state`, `set`, `bind` and `createStore` are in [Components](components.md#data-and-view-state-optional) and [Shared stores](components.md#avoiding-layer-by-layer-passing-lint-ts-domdata-flow).
|
|
162
|
+
|
|
163
|
+
## Testing
|
|
164
|
+
|
|
165
|
+
Each kind of behavior goes to the cheapest test that can show it:
|
|
166
|
+
|
|
167
|
+
| Test | Tool | What goes there | In the example |
|
|
168
|
+
|---|---|---|---|
|
|
169
|
+
| Unit and component tests, next to the code (`*.test.ts`) | Vitest with happy-dom | store actions; components with their props, events and stores; a page given a route param as a prop; that a removed page no longer renders on store changes; the route table through `show()` (matching, props, not found, an aborted navigation) | 7 files, 19 tests, about half a second |
|
|
170
|
+
| Real-browser tests (`*.browser.test.ts`) | Vitest browser mode in headless Chrome | what a DOM shim does not have: the Navigation API (happy-dom has no `navigation`), `moveBefore` (a reordered list item stays connected and keeps focus), and styles, layout and real input | the router with `navigation.navigate()`, link clicks, back and forward; a task that moves to the end of the list keeps focus |
|
|
171
|
+
| End-to-end tests (`e2e/`) | Playwright Test against the built app on `vite preview` | what only the whole app shows: deep links, navigation without a new document, back and forward, page chunks loaded on demand, a store keeping data across pages, an app-wide store reaching another feature, a superseded or failed page load, the fallback without the Navigation API | 8 tests |
|
|
172
|
+
|
|
173
|
+
- Reset every store a test touches in `beforeEach` (`projects.set(seedProjects())`), and empty the document in `afterEach`: each test file shares one document, one registry and its module-level stores.
|
|
174
|
+
- Import a component's module for its side effect (`import "./project-page.ts"`) before creating its element; a page's own components come with it through its template.
|
|
175
|
+
- Test files next to the code need the test runner's patterns to match them: `include: ["src/**/*.test.ts"]` with `exclude: ["src/**/*.browser.test.ts"]` for happy-dom, and `include: ["src/**/*.browser.test.ts"]` for the browser project.
|
|
176
|
+
|
|
177
|
+
How to write each kind of test, the Vitest and Playwright configuration, and what happy-dom gets wrong are in the [testing Skill](../skills/nativele-testing/SKILL.md).
|
|
178
|
+
|
|
179
|
+
## Generated files, checks and tsconfig
|
|
180
|
+
|
|
181
|
+
- **Scan one folder that holds `app/`, `shared/` and `features/`**, and give the same folder to everything: `tsDom({ dirs: ["src"] })` in `vite.config.ts`, `ts-dom check src`, `ts-dom doctor src`. There is then one `src/ts-dom-tags.ts`, and an `x.html.ts` next to every template.
|
|
182
|
+
- **Why one folder**: several scanned folders work too (`dirs: ["src/shared", "src/features"]`): a template imports a child component defined in any of them, and a tag defined in two of them is reported as `duplicate-define`. But each folder must then be named the same way in `tsDom({ dirs })`, every `ts-dom check` and `ts-dom doctor` call and the tsconfig `include`, and a folder missing from one of them is silently not generated or not checked there. One folder that holds everything gives one `ts-dom-tags.ts` and one argument everywhere. A component outside every scanned folder (another package, for example) is not imported automatically: import its module where it is used.
|
|
183
|
+
- **Tests inside `src/`** are scanned too; that changes nothing, because they define no components.
|
|
184
|
+
- **`tsconfig.json`** includes `src` (the app, the colocated tests and `ts-dom-tags.ts`) and `e2e`, so `ts-dom check src`, which runs the project's `tsc` over the whole program, type-checks the tests and the end-to-end specs as well: `"include": ["src", "e2e"]`.
|
|
185
|
+
- **The generated files** (`*.html.ts`, `ts-dom-tags.ts`) are not committed. `ts-dom check src` writes them before it type-checks, removes an `x.html.ts` whose template is gone, and the Vite plugin writes them when Vite or Vitest starts, so a clean checkout builds and tests with nothing generated. When the scanned folder changes (from `src/nv` to `src`, for example), delete the `ts-dom-tags.ts` left in the old folder: nothing regenerates it, and once its components have moved, `tsc` reports its imports as `TS2307`.
|
|
186
|
+
- **Scripts**: `"build": "ts-dom check src && vite build"`, so a build type-checks first; neither `vite build` nor Vitest type-checks.
|
|
187
|
+
|
|
188
|
+
Setting up Vite and the plugin is in the [Vite Skill](../skills/nativele-vite/SKILL.md); what each check does is in [Setup, checks and lint](tooling.md#checks).
|