vite-plugin-vanjs 0.1.16 → 0.1.18

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.
@@ -1,22 +1,41 @@
1
1
  import isServer from "../setup/isServer.mjs";
2
2
  import { routerState, setRouterState } from "./state.mjs";
3
3
  import { matchRoute } from "./matchRoute.mjs";
4
+ import * as dataCache from "./dataCache.mjs";
4
5
 
5
6
  /** @typedef {typeof import("./types.d.ts").navigate} Navigate */
6
7
  /** @typedef {import("./types.d.ts").Route} Route */
7
8
  /** @typedef {import("./types.d.ts").VanNode} VanNode */
8
9
  /** @typedef {import("./types.d.ts").ComponentModule} ComponentModule */
9
- /** @typedef {import("./types.d.ts").DynamicModule} DynamicModule */
10
10
  /** @typedef {import("./types.d.ts").ComponentFn} ComponentFn */
11
11
  /** @typedef {import("./types.d.ts").LazyComponent} LazyComponent */
12
12
 
13
+ /**
14
+ * Returns the HREF string value
15
+ * @param {unknown} v
16
+ * @returns {string}
17
+ */
18
+ export const getValue = (v) => {
19
+ return typeof v === "function" ? v() : v.rawVal ? v.val : v;
20
+ };
21
+
13
22
  /**
14
23
  * Check if selected page is the current page;
15
24
  * @param {string} pageName
16
25
  * @returns {boolean}
17
26
  */
18
27
  export const isCurrentPage = (pageName) => {
19
- return routerState.pathname === pageName;
28
+ return routerState.pathname === getValue(pageName);
29
+ };
30
+
31
+ /**
32
+ * Check if selected page is related to the current page;
33
+ * @param {string} pageName
34
+ * @returns {boolean}
35
+ */
36
+ export const isCurrentLocation = (pageName) => {
37
+ const pathName = routerState.pathname;
38
+ return pathName !== "/" && pathName.includes(getValue(pageName));
20
39
  };
21
40
 
22
41
  /**
@@ -25,28 +44,46 @@ export const isCurrentPage = (pageName) => {
25
44
  * @returns {component is (() => LazyComponent)}
26
45
  */
27
46
  export const isLazyComponent = (component) => {
28
- // Server: Check if it's an async function
29
- if (isServer && typeof component === "function") {
30
- return component.constructor.name.includes("AsyncFunction");
31
- }
32
-
33
- // Client: Check if it's designated as lazy
47
+ if (typeof component !== "function") return false;
34
48
  // @ts-expect-error - this property is optional and on purpose
35
- return component?.isLazy === true;
49
+ return component?.isLazy === true ||
50
+ component.constructor.name.includes("AsyncFunction");
36
51
  };
37
52
 
38
53
  /**
39
54
  * Execute lifecycle methods preload and / or load
40
- * @param {ComponentModule} param0
55
+ * @param {import("./types.d.ts").RouteEntry} route
41
56
  * @param {Record<string, string> | undefined} params
42
57
  * @returns {Promise<boolean>}
43
58
  */
44
- export const executeLifecycle = async ({ route }, params) => {
59
+ export const executeLifecycle = async (route, params) => {
45
60
  // istanbul ignore next
46
- if (!route) return true;
47
61
  try {
48
- if (route?.preload) await route.preload(params);
49
- if (route?.load) await route.load(params);
62
+ if (!route) return true;
63
+
64
+ let data;
65
+ const preload = route.preload;
66
+ const load = route.load;
67
+
68
+ if (preload) await preload(params);
69
+ if (!data && load) data = await load(params);
70
+ if (data) {
71
+ // initialLoaded = true;
72
+ const pathname = routerState.pathname;
73
+ const cacheKey = Object.keys(params || {}).length === 0
74
+ ? ""
75
+ : JSON.stringify(params);
76
+ dataCache.set(pathname, cacheKey, {
77
+ data,
78
+ error: null,
79
+ status: "success",
80
+ timestamp: Date.now(),
81
+ });
82
+ if (!isServer) {
83
+ dataCache.touch(pathname);
84
+ }
85
+ }
86
+
50
87
  return true;
51
88
  } catch (error) {
52
89
  // istanbul ignore next
@@ -56,6 +93,16 @@ export const executeLifecycle = async ({ route }, params) => {
56
93
  }
57
94
  };
58
95
 
96
+ /**
97
+ * Convenience hook to get the current route's cached data.
98
+ * @returns {any | undefined}
99
+ */
100
+ export const useRouteData = () => {
101
+ const params = routerState.params;
102
+ const key = Object.keys(params).length === 0 ? "" : JSON.stringify(params);
103
+ return dataCache.get(routerState.pathname, key)?.data;
104
+ };
105
+
59
106
  /**
60
107
  * Client only navigation utility.
61
108
  * @type {Navigate}
package/router/index.mjs CHANGED
@@ -5,6 +5,7 @@ export * from "./a.mjs";
5
5
  export * from "./state.mjs";
6
6
  export * from "./lazy.mjs";
7
7
  export * from "./helpers.mjs";
8
- export * from "./cache.mjs";
8
+ export * from "./routeCache.mjs";
9
+ export * as dataCache from "./dataCache.mjs";
9
10
  export * from "./unwrap.mjs";
10
11
  export * from "./extractParams.mjs";
package/router/lazy.mjs CHANGED
@@ -1,7 +1,5 @@
1
1
  // router/lazy.mjs
2
- import van from "vanjs-core";
3
- import isServer from "../setup/isServer.mjs";
4
- import { cache, getCached } from "./cache.mjs";
2
+ import { cacheRoute, getCachedRoute } from "./routeCache.mjs";
5
3
 
6
4
  /** @typedef {import('./types').VanNode} VanNode */
7
5
  /** @typedef {import('./types').DynamicModule} DynamicModule */
@@ -10,63 +8,27 @@ import { cache, getCached } from "./cache.mjs";
10
8
 
11
9
  /**
12
10
  * Registers a lazy component.
11
+ * Both server and client return an async function that resolves the module.
13
12
  * @type {typeof import("./types").lazy}
14
13
  */
15
14
  export const lazy = (importFn) => {
16
- if (isServer) {
17
- return async () => {
18
- const cached = getCached(importFn);
19
- /* istanbul ignore next */
20
- if (cached) {
21
- return cached;
22
- }
23
-
24
- const module = await importFn();
25
- /** @type {ComponentFn} */
26
- const component = module?.default || module.Page;
27
- /** @type {ComponentModule} */
28
- const result = { component, route: module.route };
29
-
30
- cache(importFn, result);
31
- return result;
32
- };
33
- }
34
-
35
- let initialized = false;
36
- /** @type {import("vanjs-core").State<(ComponentModule["component"] | (() => string))>} */
37
- const component = van.state(() => "Loading..");
38
- /** @type {import("vanjs-core").State<ComponentModule["route"]>} */
39
- const route = van.state({});
40
-
41
- const load = () => {
42
- if (initialized) return;
43
-
44
- const cached = getCached(importFn);
15
+ const resolveModule = async () => {
16
+ const cached = getCachedRoute(importFn);
45
17
  /* istanbul ignore next */
46
18
  if (cached) {
47
- component.val = cached.component;
48
- route.val = cached.route;
49
- return;
19
+ return cached;
50
20
  }
51
21
 
52
- initialized = true;
53
- importFn().then(
54
- /** @param {DynamicModule} module */
55
- (module) => {
56
- /** @type {ComponentModule["component"]} */
57
- const pageComponent = module?.default || module.Page;
58
- cache(importFn, { component: pageComponent, route: module.route });
59
- component.val = pageComponent;
60
- route.val = module.route;
61
- },
62
- );
63
- };
22
+ const module = await importFn();
23
+ /** @type {ComponentFn} */
24
+ const component = module?.default || module.Page;
25
+ /** @type {ComponentModule} */
26
+ const result = { component, route: module.route };
64
27
 
65
- const lazyComponent = () => {
66
- load();
67
- return { component: component.val(), route: route.val };
28
+ cacheRoute(importFn, result);
29
+ return result;
68
30
  };
69
- lazyComponent.isLazy = true;
70
- // @ts-expect-error - typescript cannot handle this isomorphically
71
- return lazyComponent;
31
+
32
+ resolveModule.isLazy = true;
33
+ return resolveModule;
72
34
  };
package/router/route.mjs CHANGED
@@ -1,7 +1,8 @@
1
- // router/routes.mjs
1
+ // router/route.mjs
2
2
  import { isLazyComponent } from "./helpers.mjs";
3
3
  import { routes } from "./routes.mjs";
4
4
  import { lazy } from "./lazy.mjs";
5
+
5
6
  /** @typedef {import("./types.d.ts").RouteProps} RouteProps */
6
7
  /** @typedef {import("./types.d.ts").ComponentModule} ComponentModule */
7
8
 
@@ -17,7 +18,7 @@ export const Route = (routeProps) => {
17
18
  return;
18
19
  }
19
20
 
20
- // If component isn't lazy, make it lazy
21
+ // If component isn't lazy, wrap it with preload/load
21
22
  if (!isLazyComponent(component)) {
22
23
  /** @type {() => Promise<ComponentModule>} */
23
24
  const wrappedComponent = lazy(() =>
@@ -26,11 +27,10 @@ export const Route = (routeProps) => {
26
27
  route: { preload, load },
27
28
  })
28
29
  );
29
- routes.push({ ...rest, path, component: wrappedComponent });
30
+ routes.push({ ...rest, path, preload, load, component: wrappedComponent });
30
31
  return;
31
32
  }
32
33
 
33
- // Otherwise keep original component
34
- // @ts-expect-error - RouteProps and RouteEntry are now equivalent
35
- routes.push(routeProps);
34
+ // Lazy component same async function on server and client
35
+ routes.push({ ...rest, path, component, preload, load });
36
36
  };
@@ -7,9 +7,9 @@
7
7
  const routeCache = new Map();
8
8
 
9
9
  /** @type {GetCachedRoute} */
10
- export const getCached = (key) => routeCache.get(key);
10
+ export const getCachedRoute = (key) => routeCache.get(key);
11
11
 
12
12
  /** @type {CacheRoute} */
13
- export const cache = (key, value) => {
13
+ export const cacheRoute = (key, value) => {
14
14
  routeCache.set(key, value);
15
15
  };
package/router/router.mjs CHANGED
@@ -6,7 +6,7 @@ import { executeLifecycle } from "./helpers.mjs";
6
6
  import { unwrap } from "./unwrap.mjs";
7
7
  import { hydrate } from "../client/index.mjs";
8
8
  import { Head, initializeHeadTags } from "../meta/index.mjs";
9
-
9
+ import * as dataCache from "./dataCache.mjs";
10
10
  import "virtual:@vanjs/routes";
11
11
 
12
12
  /** @typedef {import("./types.d.ts").ComponentModule} ComponentModule */
@@ -34,7 +34,7 @@ const resolveChildren = (module) => {
34
34
  const updateHead = () => {
35
35
  // istanbul ignore else
36
36
  if (document.head) {
37
- van.hydrate(document.head, (head) => hydrate(head, Head()));
37
+ hydrate(document.head, Head());
38
38
  }
39
39
  };
40
40
 
@@ -64,16 +64,12 @@ const initClient = () => {
64
64
 
65
65
  export const Router = (initialProps = /* istanbul ignore next */ {}) => {
66
66
  const { div, main } = van.tags;
67
-
68
67
  const props = Object.fromEntries(
69
68
  Object.entries(initialProps).filter(([_, val]) => val !== undefined),
70
69
  );
71
- const wrapper = main({ ...props, "data-root": true });
72
-
73
- // Initialize Head BEFORE any route matching or lifecycle execution
74
- if (!isServer) initClient();
75
-
70
+ const wrapper = main({ ...props, "data-root": "" });
76
71
  const route = matchRoute(routerState.pathname);
72
+
77
73
  /* istanbul ignore else */
78
74
  if (!route) return van.add(wrapper, div("No Route Found"));
79
75
 
@@ -81,45 +77,64 @@ export const Router = (initialProps = /* istanbul ignore next */ {}) => {
81
77
 
82
78
  // Server-side rendering
83
79
  if (isServer) {
84
- return (async () => {
80
+ return async () => {
85
81
  try {
82
+ // 1. Resolve the module first (to get route lifecycle hooks)
86
83
  const module = await route.component();
87
- await executeLifecycle(module, route.params);
88
- return van.add(wrapper, ...resolveChildren(module));
84
+
85
+ // 2. Execute lifecycle
86
+ await executeLifecycle(module.route, route.params);
87
+ const children = resolveChildren(module);
88
+
89
+ // 3. Render the component (data is now in dataCache)
90
+ return van.add(wrapper, ...children);
89
91
  } catch (error) {
90
92
  /* istanbul ignore next */
91
93
  console.error("Router error:", error);
92
94
  /* istanbul ignore next */
93
95
  return van.add(wrapper, div("Error loading page"));
94
96
  }
95
- })();
97
+ };
96
98
  }
97
99
 
98
- // Client-side: check if hydrating SSR content or SPA
100
+ // Init client here
101
+ if (!isServer) initClient();
102
+
103
+ // Client-side: hydrate data cache from SSR output
104
+ // This must happen BEFORE any component renders so useRouteData() works
105
+ if (globalThis.__DATA_CACHE) {
106
+ dataCache.hydrateFromJSON(globalThis.__DATA_CACHE);
107
+ }
108
+
109
+ // Client-side: check if hydrating SSR content or pure SPA
99
110
  const root = document.querySelector("[data-root]");
100
111
 
101
112
  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));
113
+ return async () => {
114
+ const module = await route.component();
115
+
116
+ await executeLifecycle(module.route, route.params);
117
+ const children = resolveChildren(module);
118
+ updateHead();
119
+ return van.add(wrapper, ...children);
120
+ };
107
121
  }
108
122
 
109
- // SPA path - reactive routing
123
+ // Pure SPA path - reactive routing
110
124
  van.derive(() => {
111
- const r = matchRoute(routerState.pathname);
112
- if (!r) {
125
+ const matchedRoute = matchRoute(routerState.pathname);
126
+ if (!matchedRoute) {
113
127
  wrapper.replaceChildren(div("No Route Found"));
114
128
  return;
115
129
  }
116
130
 
117
- const module = r.component();
118
- executeLifecycle(module, r.params);
119
- const children = resolveChildren(module);
120
-
121
- wrapper.replaceChildren(...children);
122
- updateHead();
131
+ (async () => {
132
+ const module = await matchedRoute.component();
133
+ await executeLifecycle(module.route, matchedRoute.params);
134
+ const children = resolveChildren(module);
135
+ wrapper.replaceChildren(...children);
136
+ updateHead();
137
+ })();
123
138
  });
124
139
 
125
140
  return wrapper;
package/router/state.mjs CHANGED
@@ -53,21 +53,27 @@ const defineProxy = (key, value, target) => {
53
53
  return stateObj;
54
54
  };
55
55
 
56
+ /** @typedef */
57
+
56
58
  /**
57
- * @param {T extends Record<string, unknown>} init
59
+ * @template {Record<string, unknown>} T
60
+ * @param {T} init
58
61
  * @returns {T}
59
62
  */
60
- export function miniStore(init) {
63
+ export function microStore(init) {
64
+ /** @type {T} */
61
65
  const target = {};
62
66
  for (const [prop, value] of Object.entries(init)) {
63
- if (
64
- value && typeof value === "object" &&
65
- Object.getPrototypeOf(value) === Object
66
- ) {
67
+ const isPlainObject = value && typeof value === "object" &&
68
+ !Array.isArray(value) && Object.getPrototypeOf(value) === Object;
69
+
70
+ if (isPlainObject && Object.keys(value).length > 0) {
67
71
  for (const [sp, sv] of Object.entries(value)) {
68
72
  target[prop] = defineProxy(sp, sv, {});
69
73
  }
70
- } else if (!Array.isArray(value)) {
74
+ } else if (isPlainObject) {
75
+ target[prop] = value;
76
+ } else if (!Array.isArray(value) && value != null) {
71
77
  defineProxy(prop, value, target);
72
78
  } else {
73
79
  console.warn(typeof value + " is not supported.");
@@ -79,25 +85,25 @@ export function miniStore(init) {
79
85
  /**
80
86
  * @type {typeof import("./types.d.ts").routerState}
81
87
  */
82
- // export const routerState = {
83
- // pathname: van.state(initialPath),
84
- // searchParams: van.state(new URLSearchParams(initialSearch)),
85
- // params: van.state({}),
86
- // };
87
- export const routerState = miniStore({
88
+ const initialStatus = isServer
89
+ ? "success"
90
+ : (globalThis.__DATA_CACHE ? "success" : "idle");
91
+
92
+ export const routerState = microStore({
88
93
  pathname: initialPath,
89
94
  searchParams: new URLSearchParams(initialSearch),
90
95
  params: {},
96
+ status: initialStatus,
91
97
  });
92
98
 
93
99
  /**
94
100
  * @type {typeof import("./types.d.ts").setRouterState}
95
101
  */
96
- export const setRouterState = (path, search = undefined, params) => {
102
+ export const setRouterState = (path, search = undefined, params = {}) => {
97
103
  const [pathname, searchParams] = fixRouteUrl(path).split("?");
98
104
  routerState.pathname = pathname;
99
105
  routerState.searchParams = new URLSearchParams(
100
106
  search || searchParams || "",
101
107
  );
102
- Object.assign(routerState.params, params || {});
108
+ Object.assign(routerState.params, params);
103
109
  };