vite-plugin-vanjs 0.1.13 → 0.1.15
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/client/index.mjs +3 -77
- package/meta/helpers.mjs +1 -0
- package/meta/tags.mjs +6 -22
- package/meta/types.d.ts +6 -1
- package/package.json +9 -9
- package/plugin/helpers.mjs +1 -0
- package/plugin/index.mjs +4 -9
- package/router/router.mjs +103 -121
- package/router/types.d.ts +1 -0
package/client/index.mjs
CHANGED
|
@@ -133,13 +133,6 @@ export function elementsMatch(el1, el2, deep) {
|
|
|
133
133
|
: true;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/** @type {<E extends Element = Element, T extends keyof HTMLElementTagNameMap>(target: E, ...tagNames: T[]) => boolean} */
|
|
137
|
-
const isTag = (target, ...tagNames) => {
|
|
138
|
-
return tagNames.some((tag) =>
|
|
139
|
-
target.tagName.toLowerCase() === tag.toLowerCase()
|
|
140
|
-
);
|
|
141
|
-
};
|
|
142
|
-
|
|
143
136
|
function createHydrationContext() {
|
|
144
137
|
/** @type {WeakMap<Element, Element>} */
|
|
145
138
|
const parentCache = new WeakMap();
|
|
@@ -250,31 +243,15 @@ export const hydrate = (target, content) => {
|
|
|
250
243
|
const currentChildren = Array.from(target.children);
|
|
251
244
|
const newChildren = Array.from(wrapper.children);
|
|
252
245
|
|
|
253
|
-
if (
|
|
246
|
+
if (target.tagName.toLowerCase() === "head") {
|
|
254
247
|
// Keep current tags on first hydration
|
|
255
248
|
if (!target.hasAttribute("data-h")) {
|
|
256
249
|
target.setAttribute("data-h", "");
|
|
257
250
|
return target;
|
|
258
251
|
}
|
|
259
252
|
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
!isTag(child, "style", "link")
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
// Handle style/link tags separately
|
|
266
|
-
const styleTags = newChildren.filter((child) =>
|
|
267
|
-
isTag(child, "style", "link")
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
// Create maps for existing tags
|
|
271
|
-
const existingStyles = new Map(
|
|
272
|
-
currentChildren.filter((child) => isTag(child, "style", "link"))
|
|
273
|
-
.map((child) => [getTagKey(child), child]),
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
// Process regular tags normally
|
|
277
|
-
regularTags.forEach((newChild) => {
|
|
253
|
+
// Replace all tags uniformly - styles/scripts are handled via imports
|
|
254
|
+
newChildren.forEach((newChild) => {
|
|
278
255
|
const key = getTagKey(newChild);
|
|
279
256
|
const existing = currentChildren.find((child) =>
|
|
280
257
|
getTagKey(child) === key
|
|
@@ -285,57 +262,6 @@ export const hydrate = (target, content) => {
|
|
|
285
262
|
target.appendChild(newChild);
|
|
286
263
|
}
|
|
287
264
|
});
|
|
288
|
-
|
|
289
|
-
// Process style tags with special handling
|
|
290
|
-
styleTags.forEach((newChild) => {
|
|
291
|
-
const key = getTagKey(newChild);
|
|
292
|
-
const existing = existingStyles.get(key);
|
|
293
|
-
|
|
294
|
-
// Skip if tag already exists with same content+id/href
|
|
295
|
-
if (existing) {
|
|
296
|
-
// istanbul ignore next - try again later
|
|
297
|
-
if (isTag(existing, "style") && isTag(newChild, "style")) {
|
|
298
|
-
if (
|
|
299
|
-
existing.textContent === newChild.textContent &&
|
|
300
|
-
existing.id === newChild.id
|
|
301
|
-
) return;
|
|
302
|
-
}
|
|
303
|
-
// istanbul ignore next - try again later
|
|
304
|
-
if (isTag(existing, "link") && isTag(newChild, "link")) {
|
|
305
|
-
if (existing.href === newChild.href) return;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// For link tags, add with disabled state first
|
|
310
|
-
// istanbul ignore else - try again later
|
|
311
|
-
if (isTag(newChild, "link")) {
|
|
312
|
-
const temp = newChild.cloneNode();
|
|
313
|
-
temp.disabled = true;
|
|
314
|
-
|
|
315
|
-
const originalRel = temp.rel;
|
|
316
|
-
temp.rel = "preload";
|
|
317
|
-
temp.as = "style";
|
|
318
|
-
// istanbul ignore next
|
|
319
|
-
temp.onload = () => {
|
|
320
|
-
temp.rel = originalRel;
|
|
321
|
-
temp.removeAttribute("as");
|
|
322
|
-
temp.disabled = false;
|
|
323
|
-
if (existing && existing.parentNode === target) {
|
|
324
|
-
existing.remove();
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
target.appendChild(temp);
|
|
329
|
-
} // For style tags, add new one first
|
|
330
|
-
else if (isTag(newChild, "style")) {
|
|
331
|
-
target.appendChild(newChild);
|
|
332
|
-
// istanbul ignore next - try again later
|
|
333
|
-
if (existing && existing.parentNode === target) {
|
|
334
|
-
// Remove old one in next frame
|
|
335
|
-
existing.remove();
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
265
|
} else {
|
|
340
266
|
if (!target.hasAttribute("data-h")) {
|
|
341
267
|
const { diffAndHydrate } = createHydrationContext();
|
package/meta/helpers.mjs
CHANGED
package/meta/tags.mjs
CHANGED
|
@@ -25,31 +25,15 @@ export const Meta = (props) => {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Add a new `<link>` tag
|
|
28
|
+
* Add a new `<link>` tag, not to be used for stylesheets
|
|
29
29
|
* @type {(props: PropsWithKnownKeys<HTMLLinkElement>) => null}
|
|
30
30
|
*/
|
|
31
31
|
export const Link = (props) => {
|
|
32
32
|
const { link } = van.tags;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* Add a new `<script>` tag
|
|
39
|
-
* @type {(props: PropsWithKnownKeys<HTMLScriptElement>, content?: string) => null}
|
|
40
|
-
*/
|
|
41
|
-
export const Script = (props, content) => {
|
|
42
|
-
const { script } = van.tags;
|
|
43
|
-
addMeta(script(props, content));
|
|
44
|
-
return null;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Add a new `<style>` tag
|
|
49
|
-
* @type {(props: PropsWithKnownKeys<HTMLStyleElement>, content: string) => null}
|
|
50
|
-
*/
|
|
51
|
-
export const Style = (props, content) => {
|
|
52
|
-
const { style } = van.tags;
|
|
53
|
-
addMeta(style(props, content));
|
|
33
|
+
if (props.rel === "stylesheet") {
|
|
34
|
+
console.warn("Link doesn't support stylesheets.");
|
|
35
|
+
} else {
|
|
36
|
+
addMeta(link(props));
|
|
37
|
+
}
|
|
54
38
|
return null;
|
|
55
39
|
};
|
package/meta/types.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export const resetHeadTags: () => void;
|
|
|
13
13
|
export const initializeHeadTags: () => void | (() => Promise<void>);
|
|
14
14
|
|
|
15
15
|
export type SupportedTags =
|
|
16
|
+
| HTMLTitleElement
|
|
17
|
+
| HTMLLinkElement
|
|
18
|
+
| HTMLMetaElement;
|
|
19
|
+
|
|
20
|
+
export type AllHeadTags =
|
|
16
21
|
| HTMLTitleElement
|
|
17
22
|
| HTMLMetaElement
|
|
18
23
|
| HTMLScriptElement
|
|
@@ -21,7 +26,7 @@ export type SupportedTags =
|
|
|
21
26
|
|
|
22
27
|
export type TagProps = SupportedTags | PropsWithKnownKeys<SupportedTags>;
|
|
23
28
|
|
|
24
|
-
export type HeadTags =
|
|
29
|
+
export type HeadTags = AllHeadTags[] | TagFunc[];
|
|
25
30
|
|
|
26
31
|
export const addMeta = (_tag: string | TagProps) => null;
|
|
27
32
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-vanjs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"author": "thednp",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A mini meta-framework for VanJS powered by Vite",
|
|
@@ -87,14 +87,14 @@
|
|
|
87
87
|
"vanjs-ext": "^0.6.3"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
|
-
"@types/node": "^25.
|
|
91
|
-
"@vitest/browser": "^4.1.
|
|
92
|
-
"@vitest/coverage-istanbul": "^4.1.
|
|
93
|
-
"@vitest/ui": "^4.1.
|
|
94
|
-
"happy-dom": "^20.
|
|
95
|
-
"typescript": "6.0.
|
|
96
|
-
"vite": "^8.0.
|
|
97
|
-
"vitest": "^4.1.
|
|
90
|
+
"@types/node": "^25.6.0",
|
|
91
|
+
"@vitest/browser": "^4.1.5",
|
|
92
|
+
"@vitest/coverage-istanbul": "^4.1.5",
|
|
93
|
+
"@vitest/ui": "^4.1.5",
|
|
94
|
+
"happy-dom": "^20.9.0",
|
|
95
|
+
"typescript": "6.0.3",
|
|
96
|
+
"vite": "^8.0.9",
|
|
97
|
+
"vitest": "^4.1.5"
|
|
98
98
|
},
|
|
99
99
|
"engines": {
|
|
100
100
|
"node": ">=20",
|
package/plugin/helpers.mjs
CHANGED
|
@@ -48,6 +48,7 @@ export const globFiles = async (dir, extensions) => {
|
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
51
|
+
// istanbul ignore if
|
|
51
52
|
if (!entries.length) {
|
|
52
53
|
// console.warn('🍦 @vanjs/router: the "routes" folder is empty.');
|
|
53
54
|
return;
|
package/plugin/index.mjs
CHANGED
|
@@ -63,9 +63,6 @@ export default function VitePluginVanJS(options = {}) {
|
|
|
63
63
|
let config;
|
|
64
64
|
/** @type {RouteFile[] | null} */
|
|
65
65
|
let routeCache = null;
|
|
66
|
-
/** @type {PluginContext} */
|
|
67
|
-
let context;
|
|
68
|
-
let viteVersion = "8.0.0";
|
|
69
66
|
let isOxc = true;
|
|
70
67
|
|
|
71
68
|
const virtualModuleId = "virtual:@vanjs/routes";
|
|
@@ -75,9 +72,8 @@ export default function VitePluginVanJS(options = {}) {
|
|
|
75
72
|
name: "vanjs",
|
|
76
73
|
enforce: "pre",
|
|
77
74
|
buildStart() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
isOxc = Number(viteVersion) >= 8;
|
|
75
|
+
const viteVersion = this.meta?.viteVersion;
|
|
76
|
+
isOxc = Number(viteVersion[0]) >= 8;
|
|
81
77
|
},
|
|
82
78
|
// @ts-expect-error - this is temporary esbuild will be
|
|
83
79
|
config() {
|
|
@@ -239,9 +235,8 @@ routes.length = 0;
|
|
|
239
235
|
// Register routes
|
|
240
236
|
${currentRoutes.map(generateRoute).join("\n")}
|
|
241
237
|
${
|
|
242
|
-
(ops && ops.ssr && currentRoutes.length)
|
|
243
|
-
|
|
244
|
-
: /* istanbul ignore next @preserve */ ""
|
|
238
|
+
(ops && ops.ssr && currentRoutes.length) &&
|
|
239
|
+
`console.log(\`🍦 @vanjs/router registered ${currentRoutes.length} routes.\`)`
|
|
245
240
|
}
|
|
246
241
|
`;
|
|
247
242
|
|
package/router/router.mjs
CHANGED
|
@@ -9,136 +9,118 @@ import { Head, initializeHeadTags } from "../meta/index.mjs";
|
|
|
9
9
|
|
|
10
10
|
import "virtual:@vanjs/routes";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** @typedef {import("./types.d.ts").ComponentModule} ComponentModule */
|
|
13
|
+
/** @typedef {import("./types.d.ts").RouteEntry} RouteEntry */
|
|
14
|
+
/** @typedef {import("./types.d.ts").VanNode} VanNode */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve component children from a module
|
|
18
|
+
* @param {ComponentModule | Element | Element[] | any} module
|
|
19
|
+
* @returns {VanNode[]}
|
|
20
|
+
*/
|
|
21
|
+
const resolveChildren = (module) => {
|
|
22
|
+
const isElement = typeof Element !== "undefined" && module instanceof Element;
|
|
23
|
+
const cp = (Array.isArray(module) || isElement)
|
|
24
|
+
? module
|
|
25
|
+
: typeof module.component === "function"
|
|
26
|
+
? module.component()
|
|
27
|
+
: module.component;
|
|
28
|
+
return cp ? Array.from(unwrap(cp).children) : /* istanbul ignore next */ [];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update head tags
|
|
33
|
+
*/
|
|
34
|
+
const updateHead = () => {
|
|
35
|
+
// istanbul ignore else
|
|
36
|
+
if (document.head) {
|
|
37
|
+
van.hydrate(document.head, (head) => hydrate(head, Head()));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize client-side router (Head + popstate listener)
|
|
43
|
+
*/
|
|
44
|
+
let _initialized = false;
|
|
45
|
+
const initClient = () => {
|
|
46
|
+
// istanbul ignore if - already initialized
|
|
47
|
+
if (_initialized) return;
|
|
48
|
+
|
|
49
|
+
initializeHeadTags();
|
|
50
|
+
globalThis.addEventListener(
|
|
51
|
+
"popstate",
|
|
52
|
+
/** @param {Event & {target: globalThis}} e */
|
|
53
|
+
(e) => {
|
|
54
|
+
const location = e.target.location;
|
|
55
|
+
const oldPath = routerState.pathname._oldVal;
|
|
56
|
+
// istanbul ignore next - cannot test
|
|
57
|
+
if (location.pathname !== oldPath) {
|
|
58
|
+
setRouterState(location.pathname, location.search);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
_initialized = true;
|
|
63
|
+
};
|
|
13
64
|
|
|
14
65
|
export const Router = (initialProps = /* istanbul ignore next */ {}) => {
|
|
15
66
|
const { div, main } = van.tags;
|
|
16
67
|
|
|
17
|
-
/* istanbul ignore next - try again later */
|
|
18
68
|
const props = Object.fromEntries(
|
|
19
69
|
Object.entries(initialProps).filter(([_, val]) => val !== undefined),
|
|
20
70
|
);
|
|
21
71
|
const wrapper = main({ ...props, "data-root": true });
|
|
22
|
-
const mainLayout = () => {
|
|
23
|
-
const route = matchRoute(routerState.pathname.val);
|
|
24
|
-
/* istanbul ignore else */
|
|
25
|
-
if (!route) return van.add(wrapper, div("No Route Found"));
|
|
26
|
-
|
|
27
|
-
routerState.params.val = route.params || {};
|
|
28
|
-
// Server-side or async component: use renderComponent
|
|
29
|
-
if (isServer) {
|
|
30
|
-
const renderComponent = async () => {
|
|
31
|
-
try {
|
|
32
|
-
const module = await route.component();
|
|
33
|
-
const component = typeof module.component === "function"
|
|
34
|
-
? module.component()
|
|
35
|
-
: /* istanbul ignore next */ module.component;
|
|
36
|
-
|
|
37
|
-
await executeLifecycle(module, route.params);
|
|
38
|
-
return van.add(wrapper, unwrap(component).children);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
/* istanbul ignore next */
|
|
41
|
-
console.error("Router error:", error);
|
|
42
|
-
/* istanbul ignore next */
|
|
43
|
-
return van.add(wrapper, div("Error loading page"));
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
return renderComponent();
|
|
48
|
-
}
|
|
49
72
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
|
|
73
|
+
// Initialize Head BEFORE any route matching or lifecycle execution
|
|
74
|
+
if (!isServer) initClient();
|
|
75
|
+
|
|
76
|
+
const route = matchRoute(routerState.pathname.val);
|
|
77
|
+
/* istanbul ignore else */
|
|
78
|
+
if (!route) return van.add(wrapper, div("No Route Found"));
|
|
79
|
+
|
|
80
|
+
routerState.params.val = route.params || {};
|
|
81
|
+
|
|
82
|
+
// Server-side rendering
|
|
83
|
+
if (isServer) {
|
|
84
|
+
return (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const module = await route.component();
|
|
87
|
+
await executeLifecycle(module, route.params);
|
|
88
|
+
return van.add(wrapper, ...resolveChildren(module));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
/* istanbul ignore next */
|
|
91
|
+
console.error("Router error:", error);
|
|
92
|
+
/* istanbul ignore next */
|
|
93
|
+
return van.add(wrapper, div("Error loading page"));
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Client-side: check if hydrating SSR content or SPA
|
|
99
|
+
const root = document.querySelector("[data-root]");
|
|
68
100
|
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const kids = () => cp ? Array.from(unwrap(cp).children) : [];
|
|
84
|
-
const kudos = kids();
|
|
85
|
-
|
|
86
|
-
isConnected = true;
|
|
87
|
-
// istanbul ignore else
|
|
88
|
-
if (document.head) {
|
|
89
|
-
van.hydrate(document.head, (head) => hydrate(head, Head()));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return kudos;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
return van.add(wrapper, ...children());
|
|
101
|
+
if (root) {
|
|
102
|
+
// Hydration path - root exists from SSR
|
|
103
|
+
const module = route.component();
|
|
104
|
+
executeLifecycle(module, route.params);
|
|
105
|
+
updateHead();
|
|
106
|
+
return van.add(wrapper, ...resolveChildren(module));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// SPA path - reactive routing
|
|
110
|
+
van.derive(() => {
|
|
111
|
+
const r = matchRoute(routerState.pathname.val);
|
|
112
|
+
if (!r) {
|
|
113
|
+
wrapper.replaceChildren(div("No Route Found"));
|
|
114
|
+
return;
|
|
96
115
|
}
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const md = route.component();
|
|
108
|
-
executeLifecycle(md, route.params);
|
|
109
|
-
// istanbul ignore next - cannot test all cases
|
|
110
|
-
const cp = (Array.isArray(md) || md instanceof Element)
|
|
111
|
-
? md
|
|
112
|
-
: typeof md.component === "function"
|
|
113
|
-
? md.component()
|
|
114
|
-
: md.component;
|
|
115
|
-
return cp
|
|
116
|
-
? Array.from(unwrap(cp).children)
|
|
117
|
-
: /* istanbul ignore next */ [];
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const component = () => {
|
|
121
|
-
const kids = () => children.val;
|
|
122
|
-
const result = () => {
|
|
123
|
-
return van.derive(() =>
|
|
124
|
-
van.hydrate(wrapper, (el) => {
|
|
125
|
-
const kudos = kids();
|
|
126
|
-
isConnected = true;
|
|
127
|
-
// istanbul ignore else
|
|
128
|
-
if (document.head) {
|
|
129
|
-
van.hydrate(document.head, (head) => hydrate(head, Head()));
|
|
130
|
-
}
|
|
131
|
-
return hydrate(el, kudos);
|
|
132
|
-
})
|
|
133
|
-
).val;
|
|
134
|
-
};
|
|
135
|
-
return result();
|
|
136
|
-
};
|
|
137
|
-
const finalResult = component();
|
|
138
|
-
return finalResult
|
|
139
|
-
? /* istanbul ignore next*/ van.add(wrapper, finalResult)
|
|
140
|
-
: wrapper;
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
return mainLayout();
|
|
116
|
+
|
|
117
|
+
const module = r.component();
|
|
118
|
+
executeLifecycle(module, r.params);
|
|
119
|
+
const children = resolveChildren(module);
|
|
120
|
+
|
|
121
|
+
wrapper.replaceChildren(...children);
|
|
122
|
+
updateHead();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return wrapper;
|
|
144
126
|
};
|