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 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 (isTag(target, "head")) {
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
- // Handle non-style/link tags first
261
- const regularTags = newChildren.filter((child) =>
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
@@ -36,6 +36,7 @@ const getTagAttribute = (tag) => {
36
36
 
37
37
  if (value) return value;
38
38
  }
39
+ // istanbul ignore next - a value must be provided
39
40
  return "";
40
41
  };
41
42
 
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
- addMeta(link(props));
34
- return null;
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 = SupportedTags[] | TagFunc[];
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.13",
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.5.0",
91
- "@vitest/browser": "^4.1.2",
92
- "@vitest/coverage-istanbul": "^4.1.2",
93
- "@vitest/ui": "^4.1.2",
94
- "happy-dom": "^20.8.9",
95
- "typescript": "6.0.2",
96
- "vite": "^8.0.3",
97
- "vitest": "^4.1.2"
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",
@@ -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
- context = this;
79
- viteVersion = context.meta?.viteVersion[0];
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
- ? `console.log(\`🍦 @vanjs/router registered ${currentRoutes.length} routes.\`)`
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
- let isConnected = false;
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
- const root = document.querySelector("[data-root]");
51
- // istanbul ignore else - cannot test unmount
52
- if (!isConnected || !root) {
53
- initializeHeadTags();
54
- globalThis.addEventListener(
55
- "popstate",
56
- /** @param {Event & {target: globalThis}} e */
57
- // istanbul ignore next - cannot test
58
- (e) => {
59
- const location = e.target.location;
60
- const oldPath = routerState.pathname._oldVal;
61
- // istanbul ignore next - cannot test
62
- if (location.pathname !== oldPath) {
63
- setRouterState(location.pathname, location.search);
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
- // Client-side lazy component, lifeCycle is already executed on the server
70
- // or when A component has been clicked in the client
71
- if (root) {
72
- // this case is when root is server side rendered
73
- const children = () => {
74
- const module = route.component();
75
- executeLifecycle(module, route.params);
76
- // istanbul ignore next - cannot test
77
- const cp = (Array.isArray(module) || module instanceof Element)
78
- ? module
79
- : typeof module.component === "function"
80
- ? module.component()
81
- : module.component;
82
- // istanbul ignore next - cannot test
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
- // this case is when root is for SPA apps
98
- const csrRoute = van.derive(() => {
99
- const p = routerState.pathname.val;
100
- return matchRoute(p);
101
- });
102
-
103
- const children = van.derive(() => {
104
- const route = csrRoute.val;
105
- // istanbul ignore if - can only be tested in client
106
- if (!route) return [div("No Route Found")];
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
  };
package/router/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference path="global.d.ts" />
2
+
2
3
  import type {
3
4
  Element as VElement,
4
5
  TagFunc as IsoTagFunc,