vite-plugin-vanjs 0.2.2 → 0.2.4
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/AGENTS.md +72 -0
- package/client/global.d.ts +13 -0
- package/client/index.mjs +111 -58
- package/client/types.d.ts +13 -0
- package/llms.txt +55 -0
- package/meta/tags.mjs +4 -1
- package/package.json +4 -2
- package/plugin/index.mjs +5 -6
- package/plugin/types.d.ts +13 -5
- package/router/global.d.ts +9 -0
- package/router/helpers.mjs +1 -2
- package/router/lazy.mjs +0 -1
- package/router/routeCache.mjs +2 -2
- package/router/router.mjs +14 -3
- package/router/types.d.ts +1 -0
- package/server/types.d.ts +1 -1
- package/setup/helpers.mjs +25 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
**vite-plugin-vanjs** is a mini meta-framework for [VanJS](https://vanjs.org/) powered by Vite. It provides file-system routing, SSR/SSG, JSX support, metadata management, and a data caching layer — all as a Vite plugin.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
| Directory | Package export | Purpose |
|
|
10
|
+
|-----------|---------------|---------|
|
|
11
|
+
| `plugin/` | `vite-plugin-vanjs` | Core Vite plugin — route scanning, virtual modules, build/dev config |
|
|
12
|
+
| `router/` | `@vanjs/router` | Router, Route, lazy, A, navigate, routerState, dataCache, matchRoute, extractParams |
|
|
13
|
+
| `client/` | `@vanjs/client` | Hydration — `hydrate()` for SSR-to-client DOM reconciliation |
|
|
14
|
+
| `server/` | `@vanjs/server` | `renderToString()`, preload link generation, data cache serialization |
|
|
15
|
+
| `meta/` | `@vanjs/meta` | Head/Title/Meta/Link — SSR-safe document metadata |
|
|
16
|
+
| `setup/` | `@vanjs/setup` | User setup file resolution |
|
|
17
|
+
| `jsx/` | `@vanjs/jsx` | JSX runtime (jsx, jsxs, jsxDev) |
|
|
18
|
+
|
|
19
|
+
## Key Concepts
|
|
20
|
+
|
|
21
|
+
- **ESM only** — all source files are `.mjs`, types are `.d.ts`
|
|
22
|
+
- **Isomorphic** — same router code runs on server and client; `isServer` (from `setup/isServer.mjs`) gates SSR vs SPA paths
|
|
23
|
+
- **Reactive state** — `microStore()` (in `router/state.mjs`) creates reactive proxy objects using `van.state()` under the hood
|
|
24
|
+
- **Async-first lazy()** — both server and client return `async () => ComponentModule` with `.isLazy = true`
|
|
25
|
+
- **Data flow** — `route.load()` returns data → cached by `executeLifecycle()` in `dataCache` → read via `useRouteData()`
|
|
26
|
+
- **Cache key** — `""` for empty params, `JSON.stringify(params)` otherwise
|
|
27
|
+
- **No `DynamicModule`** — replaced by `LazyComponent = Promise<{ default?, Page?, route? }>`
|
|
28
|
+
- **`routerState._oldVal`** — non-reactive reads via `rawVal` property (cross-env: writable on client, getter fallback on server). Used to avoid re-triggering derivations
|
|
29
|
+
- **Single Router instance** — `Router()` setup reads via `_oldVal`; only internal `van.derive()` subscribes to `pathname` + `searchParams`
|
|
30
|
+
- **Stale navigation guard** — `navToken` counter re-checked after `executeLifecycle()` before any DOM mutation
|
|
31
|
+
- **Layout chain diffing** — `buildChain()` uses prefix-diff on layout paths to reuse shared layouts across sibling pages; `outlet` swapped on leaf-only changes
|
|
32
|
+
- **JSX Fragment flattening** — `buildChain()` and `resolveChildren()` call `.flat()` on results because JSX Fragments return nested arrays
|
|
33
|
+
- **Live-target hydration** — real DOM adopted after initial render; navigations mutate the live root, not the detached render wrapper
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pnpm test # Run vitest (watch mode)
|
|
39
|
+
pnpm test -- --run # Run vitest once
|
|
40
|
+
pnpm lint # deno lint + tsc -noEmit
|
|
41
|
+
pnpm format # deno fmt
|
|
42
|
+
pnpm check:ts # tsc -noEmit only
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Testing
|
|
46
|
+
|
|
47
|
+
- **Framework**: Vitest 4 with `@vitest/browser` and `happy-dom`
|
|
48
|
+
- **Environments**: `client.test.ts` uses `happy-dom`, `server.test.ts` uses `@vitest-environment node`
|
|
49
|
+
- **Config**: `vitest.config.ts` — routes dir is `tests/routes`
|
|
50
|
+
- **Coverage**: Istanbul, enabled by default, covers `plugin/`, `router/`, `setup/`, `client/`, `server/`, `meta/`, `jsx/`
|
|
51
|
+
- **Test files**: `tests/client.test.ts`, `tests/client.test.tsx`, `tests/server.test.ts`, `tests/server.test.tsx`, `tests/dataCache.test.ts`, `tests/state.test.ts`, `tests/router.helpers.test.ts`, `tests/router.hydration.test.ts`, `tests/router.spa.test.ts`, `tests/router.search.test.ts`, `tests/router.template-pattern.test.ts`, `tests/hydration-diff.test.ts`, `tests/setup.test.ts`
|
|
52
|
+
- **Coverage**: 100% statements / branches / functions / lines for all covered directories
|
|
53
|
+
|
|
54
|
+
## Code Style
|
|
55
|
+
|
|
56
|
+
- **No comments** unless explicitly requested
|
|
57
|
+
- **No semicolons** at line ends (deno fmt default)
|
|
58
|
+
- **JSDoc** on public APIs only — `@typedef` references `./types.d.ts`
|
|
59
|
+
- **Formatting**: `deno fmt` handles all formatting — don't fight it
|
|
60
|
+
- **Linting**: `deno lint` — fix with `pnpm fix:ts`
|
|
61
|
+
|
|
62
|
+
## Type Definitions
|
|
63
|
+
|
|
64
|
+
- `router/global.d.ts` — module declaration for `@vanjs/router` (augments `"@vanjs/router"`)
|
|
65
|
+
- `router/types.d.ts` — standalone type exports (references `global.d.ts`)
|
|
66
|
+
- Each package directory has its own `types.d.ts`
|
|
67
|
+
- **Keep them in sync** — when changing exports, update both `global.d.ts` and `types.d.ts`
|
|
68
|
+
|
|
69
|
+
## Common Pitfalls
|
|
70
|
+
|
|
71
|
+
- `microStore()` skips proxying for empty plain objects (`{}`) — they're assigned directly, so `Object.assign` on `routerState.params` works but changes aren't reactive
|
|
72
|
+
- `lazy()` is **not** synchronous on the client anymore — always `await route.component()`
|
package/client/global.d.ts
CHANGED
|
@@ -58,4 +58,17 @@ declare module "@vanjs/client" {
|
|
|
58
58
|
| JSX.Element
|
|
59
59
|
| Promise<ChildElement | ChildElement[] | JSX.Element>,
|
|
60
60
|
) => T;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Shallow compare two elements, optionally recursing into keyed children.
|
|
64
|
+
*
|
|
65
|
+
* @param el1 the server rendered element
|
|
66
|
+
* @param el2 the client rendered element or elements
|
|
67
|
+
* @param deep when true, compares the hydrated children of both elements
|
|
68
|
+
*/
|
|
69
|
+
export const elementsMatch = (
|
|
70
|
+
el1: HTMLElement,
|
|
71
|
+
el2: HTMLElement | HTMLElement[],
|
|
72
|
+
deep?: boolean,
|
|
73
|
+
) => boolean;
|
|
61
74
|
}
|
package/client/index.mjs
CHANGED
|
@@ -141,37 +141,99 @@ export function elementsMatch(el1, el2, deep) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
function createHydrationContext() {
|
|
144
|
-
/**
|
|
145
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Significant child nodes: elements + non-empty text.
|
|
146
|
+
* SSR omits empty text nodes while the client creates Text("") for
|
|
147
|
+
* empty bindings, so those are filtered out on both sides.
|
|
148
|
+
* @param {Element} el
|
|
149
|
+
* @returns {ChildNode[]}
|
|
150
|
+
*/
|
|
151
|
+
const significantChildren = (el) =>
|
|
152
|
+
Array.from(el.childNodes).filter((n) =>
|
|
153
|
+
n.nodeType !== 3 || n.textContent !== ""
|
|
154
|
+
);
|
|
146
155
|
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Verify the cached parent is still valid for this root
|
|
154
|
-
if (cached && cached.isConnected && root.contains(cached)) {
|
|
155
|
-
return cached;
|
|
156
|
-
}
|
|
157
|
-
// If not valid, remove from cache
|
|
158
|
-
parentCache.delete(cacheKey);
|
|
159
|
-
}
|
|
156
|
+
/**
|
|
157
|
+
* Whether an element carries a hydration key
|
|
158
|
+
* @param {Node} el
|
|
159
|
+
* @returns {el is Element}
|
|
160
|
+
*/
|
|
161
|
+
const isKeyed = (el) => el instanceof Element && el.hasAttribute("data-hk");
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Consume hydration keys under a freshly hydrated root: adoption is done,
|
|
165
|
+
* subsequent renders are born keyless, so keys must not linger.
|
|
166
|
+
* @param {Element} root
|
|
167
|
+
*/
|
|
168
|
+
function stripHydrationKeys(root) {
|
|
169
|
+
// istanbul ignore if - the diffed root is always an Element
|
|
170
|
+
if (!(root instanceof Element)) return;
|
|
171
|
+
if (root.hasAttribute("data-hk")) root.removeAttribute("data-hk");
|
|
172
|
+
root.querySelectorAll("[data-hk]").forEach((el) =>
|
|
173
|
+
el.removeAttribute("data-hk")
|
|
174
|
+
);
|
|
175
|
+
}
|
|
163
176
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Whether two nodes can pair up: same kind, same tag/id/class, same text.
|
|
179
|
+
* className is coerced to String so SVG (SVGAnimatedString) pairs by tag+id.
|
|
180
|
+
* @param {ChildNode} oldN
|
|
181
|
+
* @param {ChildNode} newN
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
function nodesPairable(oldN, newN) {
|
|
185
|
+
if (oldN.nodeType === 3 || newN.nodeType === 3) {
|
|
186
|
+
return oldN.nodeType === newN.nodeType &&
|
|
187
|
+
oldN.textContent === newN.textContent;
|
|
188
|
+
}
|
|
189
|
+
if (oldN.nodeType === 8 || newN.nodeType === 8) {
|
|
190
|
+
return oldN.nodeType === newN.nodeType &&
|
|
191
|
+
oldN.textContent === newN.textContent;
|
|
167
192
|
}
|
|
193
|
+
if (!(oldN instanceof Element) || !(newN instanceof Element)) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return oldN.tagName === newN.tagName &&
|
|
197
|
+
(oldN.id || "") === (newN.id || "") &&
|
|
198
|
+
String(oldN.className || "") === String(newN.className || "");
|
|
199
|
+
}
|
|
168
200
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Adopt a node pair. Keyed SSR elements are replaced with their fresh
|
|
203
|
+
* client render (keys follow the client gate); static subtrees are walked
|
|
204
|
+
* so untouched nodes (e.g. images) are never re-instantiated. Text nodes
|
|
205
|
+
* are swapped for identical client ones so van bindings stay live.
|
|
206
|
+
* @param {ChildNode} oldNode
|
|
207
|
+
* @param {ChildNode} newNode
|
|
208
|
+
*/
|
|
209
|
+
function adoptNode(oldNode, newNode) {
|
|
210
|
+
if (isKeyed(oldNode)) {
|
|
211
|
+
// istanbul ignore else - pairable nodes always share the same tag
|
|
212
|
+
if (newNode instanceof Element && oldNode.tagName === newNode.tagName) {
|
|
213
|
+
oldNode.replaceWith(newNode);
|
|
214
|
+
}
|
|
215
|
+
// else: no trustworthy counterpart — leave the SSR node in place
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!(oldNode instanceof Element) || !(newNode instanceof Element)) return;
|
|
219
|
+
const oldSiblings = significantChildren(oldNode);
|
|
220
|
+
const newSiblings = significantChildren(newNode);
|
|
221
|
+
if (
|
|
222
|
+
oldSiblings.length !== newSiblings.length ||
|
|
223
|
+
!oldSiblings.every((ok, i) => nodesPairable(ok, newSiblings[i]))
|
|
224
|
+
) {
|
|
225
|
+
// structural divergence — swap the whole subtree with the fresh client render
|
|
226
|
+
oldNode.replaceWith(newNode);
|
|
227
|
+
return;
|
|
173
228
|
}
|
|
174
|
-
|
|
229
|
+
oldSiblings.forEach((os, i) => {
|
|
230
|
+
const ns = newSiblings[i];
|
|
231
|
+
if (os.nodeType === 3) {
|
|
232
|
+
os.replaceWith(ns);
|
|
233
|
+
} else {
|
|
234
|
+
adoptNode(os, ns);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
175
237
|
}
|
|
176
238
|
|
|
177
239
|
/** @type {(oldDom: HTMLElement, newDom: HTMLElement | HTMLElement[]) => HTMLElement} */
|
|
@@ -180,47 +242,38 @@ function createHydrationContext() {
|
|
|
180
242
|
// SPA mode
|
|
181
243
|
// istanbul ignore else
|
|
182
244
|
if (!oldDom.children.length && !elementsMatch(oldDom, newDom)) {
|
|
183
|
-
|
|
245
|
+
oldDom.replaceChildren(...unwrap(newDom).children);
|
|
246
|
+
stripHydrationKeys(oldDom);
|
|
247
|
+
return;
|
|
184
248
|
}
|
|
185
249
|
// istanbul ignore else
|
|
186
250
|
if (newDom instanceof Array) {
|
|
187
251
|
oldDom.replaceChildren(...unwrap(newDom).children);
|
|
252
|
+
stripHydrationKeys(oldDom);
|
|
188
253
|
return;
|
|
189
254
|
}
|
|
190
255
|
|
|
191
|
-
// SSR
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const parent = getParent(el, root);
|
|
203
|
-
if (parent && parent !== lastParent) {
|
|
204
|
-
set.add(parent);
|
|
205
|
-
lastParent = parent;
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
processElements(oldDom, oldSet);
|
|
211
|
-
processElements(newDom, newSet);
|
|
212
|
-
|
|
213
|
-
// istanbul ignore else
|
|
214
|
-
if (newSet.size > 0) {
|
|
215
|
-
const newArray = Array.from(newSet);
|
|
216
|
-
oldSet.forEach((el) => {
|
|
217
|
-
const match = newArray.find((m) => elementsMatch(m, el));
|
|
218
|
-
// istanbul ignore else
|
|
219
|
-
if (match) {
|
|
220
|
-
el.replaceWith(match);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
256
|
+
// SSR mode: adopt keyed elements 1:1 with fresh client renders,
|
|
257
|
+
// leave static subtrees untouched
|
|
258
|
+
const oldSiblings = significantChildren(oldDom);
|
|
259
|
+
const newSiblings = significantChildren(newDom);
|
|
260
|
+
if (
|
|
261
|
+
oldSiblings.length !== newSiblings.length ||
|
|
262
|
+
!oldSiblings.every((ok, i) => nodesPairable(ok, newSiblings[i]))
|
|
263
|
+
) {
|
|
264
|
+
oldDom.replaceChildren(...newSiblings);
|
|
265
|
+
stripHydrationKeys(oldDom);
|
|
266
|
+
return;
|
|
223
267
|
}
|
|
268
|
+
oldSiblings.forEach((ok, i) => {
|
|
269
|
+
const nk = newSiblings[i];
|
|
270
|
+
if (ok.nodeType === 3) {
|
|
271
|
+
ok.replaceWith(nk);
|
|
272
|
+
} else {
|
|
273
|
+
adoptNode(ok, nk);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
stripHydrationKeys(oldDom);
|
|
224
277
|
}
|
|
225
278
|
|
|
226
279
|
return { diffAndHydrate };
|
package/client/types.d.ts
CHANGED
|
@@ -52,3 +52,16 @@ export const hydrate: <T = HTMLElement>(
|
|
|
52
52
|
| JSX.Element
|
|
53
53
|
| Promise<HTMLElement | HTMLElement[] | JSX.Element>,
|
|
54
54
|
) => T;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Shallow compare two elements, optionally recursing into keyed children.
|
|
58
|
+
*
|
|
59
|
+
* @param el1 the server rendered element
|
|
60
|
+
* @param el2 the client rendered element or elements
|
|
61
|
+
* @param deep when true, compares the hydrated children of both elements
|
|
62
|
+
*/
|
|
63
|
+
export const elementsMatch: (
|
|
64
|
+
el1: HTMLElement,
|
|
65
|
+
el2: HTMLElement | HTMLElement[],
|
|
66
|
+
deep?: boolean,
|
|
67
|
+
) => boolean;
|
package/llms.txt
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# vite-plugin-vanjs
|
|
2
|
+
|
|
3
|
+
An async-first mini meta-framework for VanJS powered by Vite. Provides file-system routing, SSR/SSG, JSX support, metadata management, and a data caching layer — all as a Vite plugin.
|
|
4
|
+
|
|
5
|
+
## Packages
|
|
6
|
+
|
|
7
|
+
- `vite-plugin-vanjs` — Core Vite plugin: route scanning, virtual modules, build/dev config
|
|
8
|
+
- `@vanjs/router` — Router, Route, lazy, A, navigate, routerState, dataCache, matchRoute, extractParams
|
|
9
|
+
- `@vanjs/client` — Hydration: `hydrate()` for SSR-to-client DOM reconciliation
|
|
10
|
+
- `@vanjs/server` — `renderToString()`, preload link generation, data cache serialization
|
|
11
|
+
- `@vanjs/meta` — Head/Title/Meta/Link: SSR-safe document metadata
|
|
12
|
+
- `@vanjs/setup` — Isomorphic VanJS setup, `needsHydration`, `markHydrationComplete`
|
|
13
|
+
- `@vanjs/jsx` — JSX runtime (jsx, jsxs, jsxDev) with automatic namespace resolution
|
|
14
|
+
|
|
15
|
+
## Key Concepts
|
|
16
|
+
|
|
17
|
+
- ESM only — all source `.mjs`, types `.d.ts`
|
|
18
|
+
- Isomorphic — `isServer` gates SSR vs SPA paths
|
|
19
|
+
- Reactive state — `microStore()` creates reactive proxy objects via `van.state()`
|
|
20
|
+
- `routerState._oldVal` — non-reactive reads via `rawVal` to avoid re-triggering derivations
|
|
21
|
+
- Single Router instance — only internal `van.derive()` subscribes to pathname + searchParams
|
|
22
|
+
- Stale navigation guard — `navToken` counter re-checked after lifecycle before DOM mutation
|
|
23
|
+
- Layout chain diffing — prefix-diff on layout paths to reuse shared layouts across sibling pages
|
|
24
|
+
- JSX Fragment flattening — `.flat()` on layout/leaf results because Fragments return nested arrays
|
|
25
|
+
- Live-target hydration — navigations mutate the adopted SSR root, not the detached render wrapper
|
|
26
|
+
- Server one-shot render — no shared loading flag, concurrent requests don't interfere
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
pnpm test -- --run # Run vitest once
|
|
32
|
+
pnpm lint # deno lint + tsc -noEmit
|
|
33
|
+
pnpm format # deno fmt
|
|
34
|
+
pnpm check:ts # tsc -noEmit only
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Code Style
|
|
38
|
+
|
|
39
|
+
- No comments unless explicitly requested
|
|
40
|
+
- No semicolons (deno fmt)
|
|
41
|
+
- JSDoc on public APIs only
|
|
42
|
+
- deno fmt + deno lint
|
|
43
|
+
|
|
44
|
+
## Source Layout
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
plugin/ Core Vite plugin
|
|
48
|
+
router/ Router, state, helpers, lazy, dataCache, matchRoute, unwrap
|
|
49
|
+
client/ Hydration (hydrate, elementsMatch, setAttribute)
|
|
50
|
+
server/ renderToString, data cache serialization
|
|
51
|
+
meta/ Head/Title/Meta/Link
|
|
52
|
+
setup/ Isomorphic VanJS setup (van.mjs, van-ssr.mjs, helpers.mjs)
|
|
53
|
+
jsx/ JSX runtime (jsx, Fragment, ns)
|
|
54
|
+
tests/ Vitest tests (76 tests, 100% coverage)
|
|
55
|
+
```
|
package/meta/tags.mjs
CHANGED
|
@@ -2,7 +2,10 @@ import van from "vanjs-core";
|
|
|
2
2
|
import { addMeta } from "./Head.mjs";
|
|
3
3
|
|
|
4
4
|
/** @typedef {import("./types.d.ts").SupportedTags} SupportedTags */
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* @template T
|
|
7
|
+
* @typedef {import("vanjs-core").PropsWithKnownKeys<T>} PropsWithKnownKeys
|
|
8
|
+
*/
|
|
6
9
|
/** @typedef {import("./types.d.ts").TagProps} TagProps */
|
|
7
10
|
|
|
8
11
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-vanjs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"author": "thednp",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "An async first mini meta-framework for VanJS powered by Vite",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"jsx-runtime.d.ts",
|
|
28
28
|
"jsx-dev-runtime.d.ts",
|
|
29
29
|
"README.md",
|
|
30
|
+
"AGENTS.md",
|
|
31
|
+
"llms.txt",
|
|
30
32
|
"LICENSE"
|
|
31
33
|
],
|
|
32
34
|
"keywords": [
|
|
@@ -91,7 +93,7 @@
|
|
|
91
93
|
"vanjs-ext": "^0.6.3"
|
|
92
94
|
},
|
|
93
95
|
"devDependencies": {
|
|
94
|
-
"@types/node": "^26.
|
|
96
|
+
"@types/node": "^26.6.1",
|
|
95
97
|
"@vitest/browser": "^5.0.1",
|
|
96
98
|
"@vitest/coverage-istanbul": "^5.0.1",
|
|
97
99
|
"@vitest/ui": "^5.0.1",
|
package/plugin/index.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
/** @typedef {typeof import("./types").VitePluginVanJS} VitePluginVanJS */
|
|
2
|
-
/** @typedef {import("./types").VanJSPluginOptions} VanJSPluginOptions */
|
|
3
1
|
/** @typedef {import("vite").ResolvedConfig} ResolvedConfig */
|
|
4
|
-
/** @typedef {import("./types").PageFile} PageFile */
|
|
5
|
-
/** @typedef {import("./types").RouteFile} RouteFile */
|
|
6
2
|
/** @typedef {import("vite").BuildAppHook} BuildAppHook */
|
|
7
3
|
/** @typedef {import("vite").TransformResult} TransformResult */
|
|
8
4
|
/** @typedef {import("vite").Plugin} Plugin */
|
|
9
|
-
/** @typedef {
|
|
5
|
+
/** @typedef {import("./types.d.ts").VitePluginVan} VitePluginVan */
|
|
6
|
+
/** @typedef {import("./types.d.ts").VanJSPluginOptions} VanJSPluginOptions */
|
|
7
|
+
/** @typedef {import("./types.d.ts").PageFile} PageFile */
|
|
8
|
+
/** @typedef {import("./types.d.ts").RouteFile} RouteFile */
|
|
10
9
|
|
|
11
10
|
import { fileURLToPath } from "node:url";
|
|
12
11
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -56,7 +55,7 @@ const debugModuleAliases = {
|
|
|
56
55
|
"@vanjs/setup": "../setup/index-debug",
|
|
57
56
|
};
|
|
58
57
|
|
|
59
|
-
/** @type {
|
|
58
|
+
/** @type {VitePluginVan} */
|
|
60
59
|
export default function VitePluginVanJS(options = {}) {
|
|
61
60
|
const pluginConfig = { ...pluginDefaults, ...options };
|
|
62
61
|
const { routesDir } = pluginConfig;
|
package/plugin/types.d.ts
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
// plugin/types.ts
|
|
2
|
+
import type { Plugin } from "vite";
|
|
2
3
|
export * from "../jsx/types.d.ts";
|
|
3
4
|
export * from "../setup/types.d.ts";
|
|
4
5
|
export * from "../router/types.d.ts";
|
|
5
6
|
export * from "../meta/types.d.ts";
|
|
6
7
|
export * from "../server/types.d.ts";
|
|
7
8
|
export * from "../client/types.d.ts";
|
|
8
|
-
import type { Plugin } from "vite";
|
|
9
9
|
|
|
10
10
|
export type VanJSPluginOptions = {
|
|
11
11
|
routesDir?: string;
|
|
12
12
|
extensions?: string[];
|
|
13
|
-
excludeRoutes?: string[]; //
|
|
14
|
-
excludeRoutesProd?: string[]; //
|
|
13
|
+
excludeRoutes?: string[]; // excluded in all envs
|
|
14
|
+
excludeRoutesProd?: string[]; // excluded in production only
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The plugin context members the plugin reads from `this`.
|
|
19
|
+
*/
|
|
20
|
+
export type VitePluginContext = {
|
|
21
|
+
meta?: { viteVersion?: string };
|
|
15
22
|
};
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
export type VitePluginVan = (
|
|
18
25
|
config?: VanJSPluginOptions,
|
|
19
|
-
) => Plugin
|
|
26
|
+
) => Plugin;
|
|
20
27
|
|
|
28
|
+
declare const VitePluginVanJS: VitePluginVan;
|
|
21
29
|
export default VitePluginVanJS;
|
package/router/global.d.ts
CHANGED
|
@@ -266,6 +266,7 @@ declare module "@vanjs/router" {
|
|
|
266
266
|
export type LazyComponent = Promise<{
|
|
267
267
|
default?: ComponentFn;
|
|
268
268
|
Page?: ComponentFn;
|
|
269
|
+
component?: ComponentFn;
|
|
269
270
|
route?: Pick<RouteEntry, "load" | "preload">;
|
|
270
271
|
layouts?: RouteLayout[];
|
|
271
272
|
leaf?: ComponentFn;
|
|
@@ -334,6 +335,14 @@ declare module "@vanjs/router" {
|
|
|
334
335
|
*/
|
|
335
336
|
export const matchRoute: (path: string) => RouteEntry | null;
|
|
336
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Extract the route params from a route pattern and a path
|
|
340
|
+
*/
|
|
341
|
+
export const extractParams: (
|
|
342
|
+
pattern: string,
|
|
343
|
+
path: string,
|
|
344
|
+
) => Record<string, string> | null;
|
|
345
|
+
|
|
337
346
|
/** * Convenience hook to get the current route's cached data.
|
|
338
347
|
*/
|
|
339
348
|
export const useRouteData: <T>() => T | undefined;
|
package/router/helpers.mjs
CHANGED
|
@@ -9,8 +9,7 @@ import { Head } from "../meta/index.mjs";
|
|
|
9
9
|
import * as dataCache from "./dataCache.mjs";
|
|
10
10
|
|
|
11
11
|
/** @typedef {typeof import("./types.d.ts").navigate} Navigate */
|
|
12
|
-
/** @typedef {import("./types.d.ts").
|
|
13
|
-
/** @typedef {import("./types.d.ts").Route} Route */
|
|
12
|
+
/** @typedef {import("./types.d.ts").RouteEntry} RouteEntry */
|
|
14
13
|
/** @typedef {import("./types.d.ts").VanNode} VanNode */
|
|
15
14
|
/** @typedef {import("./types.d.ts").ComponentModule} ComponentModule */
|
|
16
15
|
/** @typedef {import("./types.d.ts").ComponentFn} ComponentFn */
|
package/router/lazy.mjs
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { cacheRoute, getCachedRoute } from "./routeCache.mjs";
|
|
3
3
|
|
|
4
4
|
/** @typedef {import('./types').VanNode} VanNode */
|
|
5
|
-
/** @typedef {import('./types').DynamicModule} DynamicModule */
|
|
6
5
|
/** @typedef {import('./types').ComponentModule} ComponentModule */
|
|
7
6
|
/** @typedef {import('./types').ComponentFn} ComponentFn */
|
|
8
7
|
|
package/router/routeCache.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** @typedef {import("./types").ComponentModule} ComponentModule */
|
|
2
2
|
/** @typedef {import("./types").ImportFn} ImportFn */
|
|
3
|
-
/** @typedef {typeof import("./types").
|
|
4
|
-
/** @typedef {typeof import("./types").
|
|
3
|
+
/** @typedef {typeof import("./types").getCachedRoute} GetCachedRoute */
|
|
4
|
+
/** @typedef {typeof import("./types").cacheRoute} CacheRoute */
|
|
5
5
|
|
|
6
6
|
/** @type {Map<ImportFn, ComponentModule>} */
|
|
7
7
|
const routeCache = new Map();
|
package/router/router.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./helpers.mjs";
|
|
11
11
|
import { initializeHeadTags } from "../meta/index.mjs";
|
|
12
12
|
import { hydrate } from "../client/index.mjs";
|
|
13
|
+
import { markHydrationComplete } from "../setup/helpers.mjs";
|
|
13
14
|
import { Head } from "../meta/index.mjs";
|
|
14
15
|
import * as dataCache from "./dataCache.mjs";
|
|
15
16
|
import "virtual:@vanjs/routes";
|
|
@@ -132,9 +133,16 @@ export const Router = (initialProps = /* istanbul ignore next */ {}) => {
|
|
|
132
133
|
let initialized = false;
|
|
133
134
|
|
|
134
135
|
// Client-side: hydrate data cache from SSR output
|
|
135
|
-
// This must happen BEFORE any component renders so useRouteData() works
|
|
136
|
-
// Skip in dev mode: we manually clear dataCache on
|
|
137
|
-
|
|
136
|
+
// This must happen BEFORE any component renders so useRouteData() works.
|
|
137
|
+
// Skip in dev mode for /admin pages: we manually clear dataCache on
|
|
138
|
+
// mutations for instant updates. Public pages reuse SSR data like prod.
|
|
139
|
+
// NOTE: read via _oldVal — subscribing here would re-create the whole
|
|
140
|
+
// Router on every navigation.
|
|
141
|
+
if (
|
|
142
|
+
globalThis.__DATA_CACHE &&
|
|
143
|
+
/* istanbul ignore next -- build-time constant, isDev is false in vitest */
|
|
144
|
+
(!isDev || !routerState._oldVal.pathname.startsWith("/admin"))
|
|
145
|
+
) {
|
|
138
146
|
dataCache.hydrateFromJSON(globalThis.__DATA_CACHE);
|
|
139
147
|
}
|
|
140
148
|
|
|
@@ -243,6 +251,9 @@ export const Router = (initialProps = /* istanbul ignore next */ {}) => {
|
|
|
243
251
|
// not the detached wrapper used for the initial render.
|
|
244
252
|
liveTarget = root;
|
|
245
253
|
initialized = true;
|
|
254
|
+
// Initial hydration is done: freshly rendered client nodes are born
|
|
255
|
+
// into live DOM and need no hydration keys from here on.
|
|
256
|
+
markHydrationComplete();
|
|
246
257
|
return result;
|
|
247
258
|
};
|
|
248
259
|
}
|
package/router/types.d.ts
CHANGED
|
@@ -227,6 +227,7 @@ export type ComponentModule = {
|
|
|
227
227
|
export type LazyComponent = Promise<{
|
|
228
228
|
default?: ComponentFn;
|
|
229
229
|
Page?: ComponentFn;
|
|
230
|
+
component?: ComponentFn;
|
|
230
231
|
route?: Pick<RouteEntry, "load" | "preload">;
|
|
231
232
|
layouts?: RouteLayout[];
|
|
232
233
|
leaf?: ComponentFn;
|
package/server/types.d.ts
CHANGED
package/setup/helpers.mjs
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Once initial hydration completes, freshly rendered client nodes are born
|
|
3
|
+
* into live DOM that is never diffed again, so they need no hydration keys.
|
|
4
|
+
* Server bundles get their own module instance where this stays false.
|
|
5
|
+
*/
|
|
6
|
+
let hydrationComplete = false;
|
|
7
|
+
|
|
8
|
+
/** @type {() => void} */
|
|
9
|
+
export function markHydrationComplete() {
|
|
10
|
+
hydrationComplete = true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @type {() => void} */
|
|
14
|
+
export function resetHydrationState() {
|
|
15
|
+
hydrationComplete = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
1
18
|
/** @type {(props: Record<string, unknown>) => boolean} */
|
|
2
19
|
export function needsHydration(props) {
|
|
20
|
+
if (hydrationComplete) return false;
|
|
3
21
|
return props && (
|
|
4
22
|
Object.keys(props).some((k) => k.startsWith("on")) || // has events
|
|
5
|
-
Object.
|
|
23
|
+
Object.entries(props).some(([k, v]) =>
|
|
24
|
+
v && typeof v === "object" && (
|
|
25
|
+
"val" in v || k === "style" && Object.values(v).some((sv) =>
|
|
26
|
+
sv && typeof sv === "object" && "val" in sv
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
) // has state
|
|
6
30
|
);
|
|
7
31
|
}
|