vite-plugin-vanjs 0.0.10 → 0.1.0

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.
@@ -14,20 +14,30 @@ declare module "@vanjs/router" {
14
14
  } from "vanjs-core";
15
15
 
16
16
  // type VanElement = VElement | Exclude<Primitive, boolean | undefined>;
17
- type DOMElement = globalThis.Element;
17
+ // type DOMElement = globalThis.Element;
18
18
  type PrimitiveChild = Primitive | State<Primitive | undefined | null>;
19
19
 
20
- type VanElement =
20
+ type DOMElement<T extends keyof HTMLElementTagNameMap = "div"> =
21
21
  | SVGElement
22
22
  | HTMLElement
23
- | DOMElement
24
23
  | Node
24
+ | HTMLElementTagNameMap[T];
25
+
26
+ export type VanElement =
27
+ | DOMElement
25
28
  | VElement
26
29
  | PrimitiveChild;
30
+ // type VanElement =
31
+ // | SVGElement
32
+ // | HTMLElement
33
+ // | DOMElement
34
+ // | Node
35
+ // | VElement
36
+ // | PrimitiveChild;
27
37
  export type VanNode =
28
38
  | VanElement
29
- | VanNode[]
30
- | (() => VanNode)
39
+ | VanElement[]
40
+ | (() => VanElement | VanElement[])
31
41
  | null
32
42
  | undefined;
33
43
 
@@ -62,14 +72,12 @@ declare module "@vanjs/router" {
62
72
  * return Router(); // or <Router /> for JSX
63
73
  * }
64
74
  */
65
- // export const Router: JSX.Component<HTMLElement> & VanComponent<HTMLElement>;
66
- // export function Router(props?: ComponentProps<HTMLElement>): HTMLElement;
67
- // export function Router(
68
- // props?: JSX.IntrinsicElements["main"] & { children: null },
69
- // ): HTMLElement;
70
75
  export const Router:
71
76
  & VanComponent<"main">
72
77
  & JSX.Component<"main">;
78
+ export const FileSystemRouter:
79
+ & VanComponent<"main">
80
+ & JSX.Component<"main">;
73
81
 
74
82
  // a.mjs
75
83
  /**
@@ -123,8 +131,13 @@ declare module "@vanjs/router" {
123
131
  export type RouteEntry = {
124
132
  path: string;
125
133
  component: Promise<ComponentModule>;
126
- preload?: (params?: Record<string, string>) => void;
127
- load?: (params?: Record<string, string>) => void;
134
+ params?: Record<string, string>;
135
+ preload?: (
136
+ params?: Record<string, string>,
137
+ ) => boolean | void | Promise<boolean | void>;
138
+ load?: (
139
+ params?: Record<string, string>,
140
+ ) => boolean | void | Promise<boolean | void>;
128
141
  };
129
142
 
130
143
  export type RouteProps = {
@@ -134,9 +147,16 @@ declare module "@vanjs/router" {
134
147
  | HTMLElementTagNameMap[unknown]
135
148
  | HTMLElementTagNameMap[unknown][])
136
149
  | VanComponent
137
- | (() => ComponentModule);
138
- preload?: (params?: Record<string, string>) => void;
139
- load?: (params?: Record<string, string>) => void;
150
+ | ComponentFn
151
+ | (() => Promise<DynamicModule>);
152
+ // | ComponentModule["component"];
153
+ // component: ComponentFn | (() => Promise<ComponentModule>);
154
+ preload?: (
155
+ params?: Record<string, string>,
156
+ ) => boolean | void | Promise<boolean | void>;
157
+ load?: (
158
+ params?: Record<string, string>,
159
+ ) => boolean | void | Promise<boolean | void>;
140
160
  };
141
161
  export const routes: RouteEntry[];
142
162
 
@@ -185,42 +205,68 @@ declare module "@vanjs/router" {
185
205
 
186
206
  /**
187
207
  * Merge the children of an Element or an array of elements with an optional array of children
188
- * into the childen of a single HTMLFragmentElement element.
189
- * @param source
190
- * @param {...VanNode[]} children
208
+ * into the childen prperty of a simple object.
209
+ * @param source
210
+ * @param children
191
211
  */
192
212
  export const unwrap: (
193
213
  source:
194
214
  | VanNode
195
- | IsoTagFunc
196
215
  | VanNode[]
197
- | IsoTagFunc[]
198
- | (() => IsoTagFunc | IsoTagFunc[] | VanNode | VanNode[]),
199
- ...children: VanNode[]
200
- ) => VanNode;
216
+ | VanComponent
217
+ | { children: VanNode[] }
218
+ | ComponentFn
219
+ | ReturnType<IsoTagFunc>
220
+ | ReturnType<IsoTagFunc>[]
221
+ | (() =>
222
+ | ReturnType<IsoTagFunc>
223
+ | ReturnType<IsoTagFunc>[]
224
+ | VanNode
225
+ | VanNode[]),
226
+ ...children: ChildDom[]
227
+ ) => { children: ChildDom[] };
228
+
229
+ export type JSXComponentFn = () => JSX.Element;
230
+ export type FragmentFn = () => ChildDom | ChildDom[];
231
+ export type ComponentFn = FragmentFn | VanComponent | JSXComponentFn;
201
232
 
202
233
  export type ComponentModule = {
203
- component: VanComponent | JSX.Element;
204
- route: Pick<RouteEntry, "load" | "preload">;
234
+ component: ComponentFn;
235
+ route?: Pick<RouteEntry, "load" | "preload">;
205
236
  };
206
237
 
207
238
  export type DynamicModule = {
208
- Page: VanComponent | JSX.Element;
209
- default?: VanComponent | JSX.Element;
239
+ Page: ComponentFn;
240
+ default?: ComponentFn;
210
241
  route?: Pick<RouteEntry, "load" | "preload">;
211
242
  };
212
243
 
213
244
  export type LazyComponent = Promise<DynamicModule>;
214
245
 
246
+ export type ImportFn = () => LazyComponent;
247
+
215
248
  /**
216
249
  * Registers a lazy component.
217
250
  * @param importFn
218
251
  */
219
- export const lazy: (importFn: () => LazyComponent) => () => ComponentModule;
252
+ export const lazy: (importFn: ImportFn) => () => Promise<ComponentModule>;
220
253
 
221
254
  /**
222
255
  * Fixes the URL of a route.
223
256
  * @param url
224
257
  */
225
258
  export const fixRouteUrl: (url: string) => string;
259
+
260
+ /**
261
+ * Execute lifecycle methods preload and / or load
262
+ */
263
+ export const executeLifecycle: (
264
+ { route }: ComponentModule,
265
+ params?: Record<string, string> | undefined,
266
+ ) => Promise<boolean | void>;
267
+
268
+ /**
269
+ * Find a registered route that matches the given path
270
+ */
271
+ export const matchRoute: (path: string) => RouteEntry | null;
226
272
  }
@@ -1,12 +1,16 @@
1
1
  // import van from "vanjs-core";
2
- import setup from "../setup/index.mjs";
2
+ import isServer from "../setup/isServer.mjs";
3
3
  import { routerState, setRouterState } from "./state.mjs";
4
4
  import { matchRoute } from "./routes.mjs";
5
5
 
6
+ /** @typedef {typeof import("./types.d.ts").unwrap} Unwrap */
7
+ /** @typedef {typeof import("./types.d.ts").navigate} Navigate */
6
8
  /** @typedef {import("./types.d.ts").Route} Route */
7
9
  /** @typedef {import("./types.d.ts").VanNode} VanNode */
8
10
  /** @typedef {import("./types.d.ts").ComponentModule} ComponentModule */
9
- /** @typedef {import('vanjs-core').TagFunc} TagFunc */
11
+ /** @typedef {import("./types.d.ts").DynamicModule} DynamicModule */
12
+ /** @typedef {import("./types.d.ts").ComponentFn} ComponentFn */
13
+ /** @typedef {import("./types.d.ts").LazyComponent} LazyComponent */
10
14
 
11
15
  /**
12
16
  * Check if selected page is the current page;
@@ -19,22 +23,24 @@ export const isCurrentPage = (pageName) => {
19
23
 
20
24
  /**
21
25
  * Merge the children of an Element or an array of elements with an optional array of children
22
- * into the childen of a single HTMLFragmentElement element.
23
- * @param {Element | () => Element | Element[]} source
24
- * @param {...Element[]} children
25
- * @returns {TagFunc<HTMLFragmentElement> | HTMLElement}
26
+ * into the childen prperty of a simple object.
27
+ * @type {Unwrap}
26
28
  */
27
29
  export const unwrap = (source, ...children) => {
28
30
  const layout = () => {
29
- const pageChildren = Array.isArray(source?.children)
30
- ? source.children
31
- : typeof source === "function"
32
- ? [...source()?.children || source()]
33
- : typeof HTMLElement !== "undefined" && source instanceof HTMLElement
34
- ? [...source.children]
35
- : /* istanbul ignore next */ Array.isArray(source)
36
- ? source
37
- : [source];
31
+ /** @type {VanNode[]} */
32
+ const pageChildren =
33
+ source && typeof source === "object" && "children" in source &&
34
+ Array.isArray(source?.children)
35
+ ? source.children
36
+ : typeof source === "function"
37
+ // @ts-expect-error - this case is specific to VanJS components in SSR
38
+ ? [...source()?.children || source()]
39
+ : typeof HTMLElement !== "undefined" && source instanceof HTMLElement
40
+ ? [...source.children]
41
+ : /* istanbul ignore next */ Array.isArray(source)
42
+ ? source
43
+ : [source];
38
44
 
39
45
  // return van.tags.fragment(
40
46
  // ...(children || /* istanbul ignore next */ []),
@@ -52,16 +58,17 @@ export const unwrap = (source, ...children) => {
52
58
 
53
59
  /**
54
60
  * Check if component is a lazy component
55
- * @param {unknown} component
56
- * @returns {component is (() => Promise<VanNode | VanNode[]>)}
61
+ * @param {ComponentFn | (() => LazyComponent)} component
62
+ * @returns {component is (() => LazyComponent)}
57
63
  */
58
64
  export const isLazyComponent = (component) => {
59
65
  // Server: Check if it's an async function
60
- if (setup.isServer) {
66
+ if (isServer && typeof component === "function") {
61
67
  return component.constructor.name.includes("AsyncFunction");
62
68
  }
63
69
 
64
70
  // Client: Check if it's designated as lazy
71
+ // @ts-expect-error - this property is optional and on purpose
65
72
  return component?.isLazy === true;
66
73
  };
67
74
 
@@ -69,7 +76,7 @@ export const isLazyComponent = (component) => {
69
76
  * Execute lifecycle methods preload and / or load
70
77
  * @param {ComponentModule} param0
71
78
  * @param {Record<string, string> | undefined} params
72
- * @returns
79
+ * @returns {Promise<boolean>}
73
80
  */
74
81
  export const executeLifecycle = async ({ route }, params) => {
75
82
  // istanbul ignore next
@@ -88,16 +95,13 @@ export const executeLifecycle = async ({ route }, params) => {
88
95
 
89
96
  /**
90
97
  * Client only navigation utility.
91
- * @param {string} path - The path to navigate to
92
- * @param {{ replace: boolean } | undefined} options - Navigation options
93
- * @param {boolean} options.replace - Whether to replace current history entry
94
- * @returns {void}
98
+ * @type {Navigate}
95
99
  */
96
100
  export const navigate = (path, options = {}) => {
97
101
  const { replace = false } = options;
98
102
 
99
103
  // istanbul ignore else
100
- if (!setup.isServer) {
104
+ if (!isServer) {
101
105
  // Client-side navigation
102
106
  const url = new URL(path, globalThis.location.origin);
103
107
  const route = matchRoute(url.pathname);
@@ -121,9 +125,10 @@ export const navigate = (path, options = {}) => {
121
125
  * Extract route params
122
126
  * @param {string} pattern
123
127
  * @param {string} path
124
- * @returns {Record<string, string>}
128
+ * @returns {Record<string, string> | null}
125
129
  */
126
130
  export const extractParams = (pattern, path) => {
131
+ /** @type {Record<string, string>} */
127
132
  const params = {};
128
133
  const patternParts = pattern.split("/");
129
134
  const pathParts = path.split("/");
@@ -164,7 +169,7 @@ export const fixRouteUrl = (url) => {
164
169
  * @returns {void}
165
170
  */
166
171
  // export const reload = (forceFetch = false) => {
167
- // if (!setup.isServer) {
172
+ // if (!isServer) {
168
173
  // // Client-side reload
169
174
  // if (forceFetch) {
170
175
  // window.location.reload();
@@ -191,7 +196,7 @@ export const fixRouteUrl = (url) => {
191
196
  // export const redirect = (path, options = {}) => {
192
197
  // const { status = 302, replace = true } = options;
193
198
 
194
- // if (!setup.isServer) {
199
+ // if (!isServer) {
195
200
  // // Client-side redirect
196
201
  // navigate(path, { replace });
197
202
  // } else {
package/router/lazy.mjs CHANGED
@@ -1,25 +1,29 @@
1
- import setup from "../setup/index.mjs";
1
+ import isServer from "../setup/isServer.mjs";
2
2
  import van from "vanjs-core";
3
3
  import { cache, getCached } from "./cache.mjs";
4
4
 
5
5
  /** @typedef {import('./types').VanNode} VanNode */
6
+ /** @typedef {import('./types').DynamicModule} DynamicModule */
6
7
  /** @typedef {import('./types').ComponentModule} ComponentModule */
8
+ /** @typedef {import('./types').ComponentFn} ComponentFn */
7
9
 
8
10
  /**
9
11
  * Registers a lazy component.
10
- * @param {Promise<VanNode>} importFn
11
- * @returns {ComponentModule | Promise<ComponentModule>}
12
+ * @type {typeof import("./types").lazy}
12
13
  */
13
14
  export const lazy = (importFn) => {
14
- if (setup.isServer) {
15
+ if (isServer) {
15
16
  return async () => {
16
17
  const cached = getCached(importFn);
17
18
  /* istanbul ignore next */
18
19
  if (cached) {
19
20
  return cached;
20
21
  }
22
+
21
23
  const module = await importFn();
22
- const component = module.Page || module.default;
24
+ /** @type {ComponentFn} */
25
+ const component = module?.default || module.Page;
26
+ /** @type {ComponentModule} */
23
27
  const result = { component, route: module.route };
24
28
 
25
29
  cache(importFn, result);
@@ -28,7 +32,9 @@ export const lazy = (importFn) => {
28
32
  }
29
33
 
30
34
  let initialized = false;
35
+ /** @type {import("vanjs-core").State<(ComponentModule["component"] | (() => string))>} */
31
36
  const component = van.state(() => "Loading..");
37
+ /** @type {import("vanjs-core").State<ComponentModule["route"]>} */
32
38
  const route = van.state({});
33
39
 
34
40
  const load = () => {
@@ -43,12 +49,16 @@ export const lazy = (importFn) => {
43
49
  }
44
50
 
45
51
  initialized = true;
46
- importFn().then((module) => {
47
- const comp = module.Page || module.default;
48
- cache(importFn, { component: comp, route: module.route });
49
- component.val = comp;
50
- route.val = module.route;
51
- });
52
+ importFn().then(
53
+ /** @param {DynamicModule} module */
54
+ (module) => {
55
+ /** @type {ComponentModule["component"]} */
56
+ const pageComponent = module?.default || module.Page;
57
+ cache(importFn, { component: pageComponent, route: module.route });
58
+ component.val = pageComponent;
59
+ route.val = module.route;
60
+ },
61
+ );
52
62
  };
53
63
 
54
64
  const lazyComponent = () => {
@@ -56,5 +66,6 @@ export const lazy = (importFn) => {
56
66
  return { component: component.val(), route: route.val };
57
67
  };
58
68
  lazyComponent.isLazy = true;
69
+ // @ts-expect-error - typescript cannot handle this isomorphically
59
70
  return lazyComponent;
60
71
  };
package/router/router.mjs CHANGED
@@ -1,22 +1,14 @@
1
1
  import van from "vanjs-core";
2
- import setup from "../setup/index.mjs";
2
+ import isServer from "../setup/isServer.mjs";
3
3
  import { routerState, setRouterState } from "./state.mjs";
4
4
  import { matchRoute } from "./routes.mjs";
5
5
  import { executeLifecycle, unwrap } from "./helpers.mjs";
6
6
  import { hydrate } from "../client/index.mjs";
7
- import { Head, initializeHeadTags } from "@vanjs/meta";
7
+ import { Head, initializeHeadTags } from "../meta/index.mjs";
8
+
9
+ import "virtual:@vanjs/routes";
8
10
 
9
11
  let isConnected = false;
10
- /** @param {Event & {target: globalThis}} e */
11
- // istanbul ignore next - cannot test
12
- const popHandler = (e) => {
13
- const location = e.target.location;
14
- const oldPath = routerState.pathname._oldVal;
15
- // istanbul ignore next - cannot test
16
- if (location.pathname !== oldPath) {
17
- setRouterState(location.pathname, location.search);
18
- }
19
- };
20
12
 
21
13
  export const Router = (initialProps = /* istanbul ignore next */ {}) => {
22
14
  const { div, main } = van.tags;
@@ -29,11 +21,11 @@ export const Router = (initialProps = /* istanbul ignore next */ {}) => {
29
21
  const mainLayout = () => {
30
22
  const route = matchRoute(routerState.pathname.val);
31
23
  /* istanbul ignore else */
32
- if (!route) return van.add(wrapper, div("404 - Not Found"));
24
+ if (!route) return van.add(wrapper, div("No Route Found"));
33
25
 
34
26
  routerState.params.val = route.params || {};
35
27
  // Server-side or async component: use renderComponent
36
- if (setup.isServer) {
28
+ if (isServer) {
37
29
  const renderComponent = async () => {
38
30
  try {
39
31
  const module = await route.component();
@@ -56,19 +48,30 @@ export const Router = (initialProps = /* istanbul ignore next */ {}) => {
56
48
 
57
49
  const root = document.querySelector("[data-root]");
58
50
  // istanbul ignore else - cannot test unmount
59
- if (isConnected || !root) {
51
+ if (!isConnected || !root) {
60
52
  initializeHeadTags();
61
- globalThis.addEventListener("popstate", popHandler);
62
- } else {
63
- globalThis.removeEventListener("popstate", popHandler);
53
+ globalThis.addEventListener(
54
+ "popstate",
55
+ /** @param {Event & {target: globalThis}} e */
56
+ // istanbul ignore next - cannot test
57
+ (e) => {
58
+ const location = e.target.location;
59
+ const oldPath = routerState.pathname._oldVal;
60
+ // istanbul ignore next - cannot test
61
+ if (location.pathname !== oldPath) {
62
+ setRouterState(location.pathname, location.search);
63
+ }
64
+ },
65
+ );
64
66
  }
65
67
 
66
68
  // Client-side lazy component, lifeCycle is already executed on the server
67
69
  // or when A component has been clicked in the client
68
70
  if (root) {
69
71
  // this case is when root is server side rendered
70
- const module = route.component();
71
72
  const children = () => {
73
+ const module = route.component();
74
+ executeLifecycle(module, route.params);
72
75
  // istanbul ignore next - cannot test
73
76
  const cp = (Array.isArray(module) || module instanceof Element)
74
77
  ? module
@@ -79,9 +82,9 @@ export const Router = (initialProps = /* istanbul ignore next */ {}) => {
79
82
  const kids = () => cp ? Array.from(unwrap(cp).children) : [];
80
83
  const kudos = kids();
81
84
 
85
+ isConnected = true;
82
86
  // istanbul ignore else
83
87
  if (document.head) {
84
- isConnected = true;
85
88
  van.hydrate(document.head, (head) => hydrate(head, Head()));
86
89
  }
87
90
 
@@ -97,7 +100,11 @@ export const Router = (initialProps = /* istanbul ignore next */ {}) => {
97
100
  });
98
101
 
99
102
  const children = van.derive(() => {
100
- const md = csrRoute.val.component();
103
+ const route = csrRoute.val;
104
+ // istanbul ignore if - can only be tested in client
105
+ if (!route) return [div("No Route Found")];
106
+ const md = route.component();
107
+ executeLifecycle(md, route.params);
101
108
  // istanbul ignore next - cannot test all cases
102
109
  const cp = (Array.isArray(md) || md instanceof Element)
103
110
  ? md
package/router/routes.mjs CHANGED
@@ -4,6 +4,8 @@ import { lazy } from "./lazy.mjs";
4
4
 
5
5
  /** @typedef {import("./types.d.ts").RouteEntry} RouteEntry */
6
6
  /** @typedef {import("./types.d.ts").RouteProps} RouteProps */
7
+ /** @typedef {import("./types.d.ts").DynamicModule} DynamicModule */
8
+ /** @typedef {import("./types.d.ts").ComponentModule} ComponentModule */
7
9
 
8
10
  /** @type {RouteEntry[]} */
9
11
  export const routes = [];
@@ -15,15 +17,17 @@ export const Route = (routeProps) => {
15
17
  const { path, component, preload, load, ...rest } = routeProps;
16
18
 
17
19
  // istanbul ignore next - no point testing this error
18
- if (path === "/" && isLazyComponent(component)) {
19
- throw new Error("Home component must not be a lazy component");
20
+ if (routes.some((r) => r.path === path)) {
21
+ console.error(`🍦 @vanjs/router: duplicated route for "${path}".`);
22
+ return;
20
23
  }
21
24
 
22
- // If component has lifecycle methods but isn't lazy, make it lazy
23
- if (!isLazyComponent(component) && path !== "/") {
25
+ // If component isn't lazy, make it lazy
26
+ if (!isLazyComponent(component)) {
27
+ /** @type {() => Promise<ComponentModule>} */
24
28
  const wrappedComponent = lazy(() =>
25
29
  Promise.resolve({
26
- default: component,
30
+ Page: component,
27
31
  route: { preload, load },
28
32
  })
29
33
  );
@@ -32,25 +36,50 @@ export const Route = (routeProps) => {
32
36
  }
33
37
 
34
38
  // Otherwise keep original component
39
+ // @ts-expect-error - RouteProps and RouteEntry are now equivalent
35
40
  routes.push(routeProps);
36
41
  };
37
42
 
38
43
  /**
39
44
  * Find a registered route that matches the given path
40
- * @param {string} path
45
+ * @param {string} initialPath
41
46
  * @returns {RouteEntry | null}
42
47
  */
43
- export const matchRoute = (path) => {
44
- const exactMatch = routes.find((r) => r.path === path);
45
- if (exactMatch) return { ...exactMatch, params: {} };
48
+ export const matchRoute = (initialPath) => {
49
+ const path = initialPath !== "/" && initialPath.endsWith("/")
50
+ ? initialPath.slice(0, -1)
51
+ : initialPath;
52
+
53
+ // First try exact match (excluding wildcards)
54
+ let foundMatch = routes.find((r) => r.path === path && !r.path.includes("*"));
55
+
56
+ // Then try nested wildcard match if no exact match found
57
+ if (!foundMatch) {
58
+ // Build the path for potential nested wildcard, e.g. /admin/* for /admin/articles
59
+ const nestedPath = path.split("/").slice(0, -1).join("/") + "/*";
60
+ foundMatch = routes.find((r) => r.path === nestedPath);
61
+ }
46
62
 
63
+ // If we found either an exact or nested wildcard match, return it with params
64
+ if (foundMatch) {
65
+ return {
66
+ ...foundMatch,
67
+ params: extractParams(foundMatch.path, path) ?? /* istanbul ignore next */
68
+ undefined,
69
+ };
70
+ }
71
+
72
+ // Try parameterized routes (like /users/:id)
47
73
  for (const route of routes) {
74
+ // Skip the global catch-all
48
75
  if (route.path === "*") continue;
49
76
  const params = extractParams(route.path, path);
77
+
50
78
  if (params) {
51
79
  return { ...route, params };
52
80
  }
53
81
  }
54
82
 
83
+ // Finally, fallback to global catch-all route
55
84
  return routes.find((r) => r.path === "*") || null;
56
85
  };
package/router/state.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  // router/state.js
2
2
  import van from "vanjs-core";
3
- import setup from "../setup/index.mjs";
3
+ import isServer from "../setup/isServer.mjs";
4
4
  import { fixRouteUrl } from "./helpers.mjs";
5
5
 
6
- const initialPath = !setup.isServer ? globalThis.location.pathname : "/";
7
- const initialSearch = !setup.isServer ? globalThis.location.search : "";
6
+ const initialPath = !isServer ? globalThis.location.pathname : "/";
7
+ const initialSearch = !isServer ? globalThis.location.search : "";
8
8
 
9
9
  /**
10
10
  * @type {typeof import("./types.d.ts").routerState}