vite-plugin-vanjs 0.1.15 → 0.1.17

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.val === 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
 
@@ -52,7 +52,7 @@ const initClient = () => {
52
52
  /** @param {Event & {target: globalThis}} e */
53
53
  (e) => {
54
54
  const location = e.target.location;
55
- const oldPath = routerState.pathname._oldVal;
55
+ const oldPath = routerState.pathname;
56
56
  // istanbul ignore next - cannot test
57
57
  if (location.pathname !== oldPath) {
58
58
  setRouterState(location.pathname, location.search);
@@ -64,27 +64,28 @@ 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();
70
+ const wrapper = main({ ...props, "data-root": "" });
71
+ const route = matchRoute(routerState.pathname);
75
72
 
76
- const route = matchRoute(routerState.pathname.val);
77
73
  /* istanbul ignore else */
78
74
  if (!route) return van.add(wrapper, div("No Route Found"));
79
75
 
80
- routerState.params.val = route.params || {};
76
+ Object.assign(routerState.params, route.params || {});
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);
84
+
85
+ // 2. Execute lifecycle
86
+ await executeLifecycle(module.route, route.params);
87
+
88
+ // 3. Render the component (data is now in dataCache)
88
89
  return van.add(wrapper, ...resolveChildren(module));
89
90
  } catch (error) {
90
91
  /* istanbul ignore next */
@@ -92,34 +93,46 @@ export const Router = (initialProps = /* istanbul ignore next */ {}) => {
92
93
  /* istanbul ignore next */
93
94
  return van.add(wrapper, div("Error loading page"));
94
95
  }
95
- })();
96
+ };
96
97
  }
97
98
 
98
- // Client-side: check if hydrating SSR content or SPA
99
+ // Init client here
100
+ if (!isServer) initClient();
101
+
102
+ // Client-side: hydrate data cache from SSR output
103
+ // This must happen BEFORE any component renders so useRouteData() works
104
+ if (globalThis.__DATA_CACHE) {
105
+ dataCache.hydrateFromJSON(globalThis.__DATA_CACHE);
106
+ }
107
+
108
+ // Client-side: check if hydrating SSR content or pure SPA
99
109
  const root = document.querySelector("[data-root]");
100
110
 
101
111
  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));
112
+ return async () => {
113
+ const module = await route.component();
114
+
115
+ await executeLifecycle(module.route, route.params);
116
+ updateHead();
117
+ return van.add(wrapper, ...resolveChildren(module));
118
+ };
107
119
  }
108
120
 
109
- // SPA path - reactive routing
121
+ // Pure SPA path - reactive routing
110
122
  van.derive(() => {
111
- const r = matchRoute(routerState.pathname.val);
112
- if (!r) {
123
+ const matchedRoute = matchRoute(routerState.pathname);
124
+ if (!matchedRoute) {
113
125
  wrapper.replaceChildren(div("No Route Found"));
114
126
  return;
115
127
  }
116
128
 
117
- const module = r.component();
118
- executeLifecycle(module, r.params);
119
- const children = resolveChildren(module);
120
-
121
- wrapper.replaceChildren(...children);
122
- updateHead();
129
+ (async () => {
130
+ const module = await matchedRoute.component();
131
+ await executeLifecycle(module.route, matchedRoute.params);
132
+ const children = resolveChildren(module);
133
+ wrapper.replaceChildren(...children);
134
+ updateHead();
135
+ })();
123
136
  });
124
137
 
125
138
  return wrapper;
package/router/state.mjs CHANGED
@@ -18,23 +18,92 @@ export const fixRouteUrl = (url) => {
18
18
  const initialPath = !isServer ? globalThis.location.pathname : "/";
19
19
  const initialSearch = !isServer ? globalThis.location.search : "";
20
20
 
21
+ const STATE_PROXY = "_proxy";
22
+ const proxyProps = {
23
+ value: 1,
24
+ enumerable: false,
25
+ configurable: false,
26
+ writable: false,
27
+ };
28
+
21
29
  /**
22
- * @type {typeof import("./types.d.ts").routerState}
30
+ * @param {number | Omit<keyof T, "symbol">} key
31
+ * @param {T[keyof T]} value
32
+ * @param {Record<string, string | number>} target
33
+ * @returns {T}
23
34
  */
24
- export const routerState = {
25
- pathname: van.state(initialPath),
26
- searchParams: van.state(new URLSearchParams(initialSearch)),
27
- params: van.state({}),
35
+ const defineProxy = (key, value, target) => {
36
+ const stateObj = van.state(value);
37
+
38
+ const getter = () => stateObj.val;
39
+ const setter = (newVal) => {
40
+ stateObj.val = newVal;
41
+ };
42
+ stateObj.val = value;
43
+
44
+ Object.defineProperties(target, {
45
+ [STATE_PROXY]: proxyProps,
46
+ [key]: {
47
+ get: getter,
48
+ set: setter,
49
+ enumerable: true,
50
+ },
51
+ });
52
+
53
+ return stateObj;
28
54
  };
29
55
 
56
+ /** @typedef */
57
+
58
+ /**
59
+ * @template {Record<string, unknown>} T
60
+ * @param {T} init
61
+ * @returns {T}
62
+ */
63
+ export function microStore(init) {
64
+ /** @type {T} */
65
+ const target = {};
66
+ for (const [prop, value] of Object.entries(init)) {
67
+ const isPlainObject = value && typeof value === "object" &&
68
+ !Array.isArray(value) && Object.getPrototypeOf(value) === Object;
69
+
70
+ if (isPlainObject && Object.keys(value).length > 0) {
71
+ for (const [sp, sv] of Object.entries(value)) {
72
+ target[prop] = defineProxy(sp, sv, {});
73
+ }
74
+ } else if (isPlainObject) {
75
+ target[prop] = value;
76
+ } else if (!Array.isArray(value) && value != null) {
77
+ defineProxy(prop, value, target);
78
+ } else {
79
+ console.warn(typeof value + " is not supported.");
80
+ }
81
+ }
82
+ return target;
83
+ }
84
+
85
+ /**
86
+ * @type {typeof import("./types.d.ts").routerState}
87
+ */
88
+ const initialStatus = isServer
89
+ ? "success"
90
+ : (globalThis.__DATA_CACHE ? "success" : "idle");
91
+
92
+ export const routerState = microStore({
93
+ pathname: initialPath,
94
+ searchParams: new URLSearchParams(initialSearch),
95
+ params: {},
96
+ status: initialStatus,
97
+ });
98
+
30
99
  /**
31
100
  * @type {typeof import("./types.d.ts").setRouterState}
32
101
  */
33
- export const setRouterState = (path, search = undefined, params) => {
102
+ export const setRouterState = (path, search = undefined, params = {}) => {
34
103
  const [pathname, searchParams] = fixRouteUrl(path).split("?");
35
- routerState.pathname.val = pathname;
36
- routerState.searchParams.val = new URLSearchParams(
104
+ routerState.pathname = pathname;
105
+ routerState.searchParams = new URLSearchParams(
37
106
  search || searchParams || "",
38
107
  );
39
- routerState.params.val = params || {};
108
+ Object.assign(routerState.params, params);
40
109
  };