vite-plugin-vanjs
Version:
A mini meta-framework for VanJS powered by Vite
218 lines (196 loc) • 6.13 kB
JavaScript
// import van from "vanjs-core";
import setup from "../setup/index.mjs";
import { routerState, setRouterState } from "./state.mjs";
import { matchRoute } from "./routes.mjs";
/** @typedef {import("./types.d.ts").Route} Route */
/** @typedef {import("./types.d.ts").VanNode} VanNode */
/** @typedef {import("./types.d.ts").ComponentModule} ComponentModule */
/** @typedef {import('vanjs-core').TagFunc} TagFunc */
/**
* Check if selected page is the current page;
* @param {string} pageName
* @returns {boolean}
*/
export const isCurrentPage = (pageName) => {
return routerState.pathname.val === pageName;
};
/**
* Merge the children of an Element or an array of elements with an optional array of children
* into the childen of a single HTMLFragmentElement element.
* @param {Element | () => Element | Element[]} source
* @param {...Element[]} children
* @returns {TagFunc<HTMLFragmentElement> | HTMLElement}
*/
export const unwrap = (source, ...children) => {
const layout = () => {
const pageChildren = Array.isArray(source?.children)
? source.children
: typeof source === "function"
? [...source()?.children || source()]
: typeof HTMLElement !== "undefined" && source instanceof HTMLElement
? [...source.children]
: /* istanbul ignore next */ Array.isArray(source)
? source
: [source];
// return van.tags.fragment(
// ...(children || /* istanbul ignore next */ []),
// ...pageChildren,
// );
return {
children: [
...(children || /* istanbul ignore next */ []),
...pageChildren,
],
};
};
return layout();
};
/**
* Check if component is a lazy component
* @param {unknown} component
* @returns {component is (() => Promise<VanNode | VanNode[]>)}
*/
export const isLazyComponent = (component) => {
// Server: Check if it's an async function
if (setup.isServer) {
return component.constructor.name.includes("AsyncFunction");
}
// Client: Check if it's designated as lazy
return component?.isLazy === true;
};
/**
* Execute lifecycle methods preload and / or load
* @param {ComponentModule} param0
* @param {Record<string, string> | undefined} params
* @returns
*/
export const executeLifecycle = async ({ route }, params) => {
// istanbul ignore next
if (!route) return true;
try {
if (route?.preload) await route.preload(params);
if (route?.load) await route.load(params);
return true;
} catch (error) {
// istanbul ignore next
console.error("Lifecycle execution error:", error);
// istanbul ignore next
return false;
}
};
/**
* Client only navigation utility.
* @param {string} path - The path to navigate to
* @param {{ replace: boolean } | undefined} options - Navigation options
* @param {boolean} options.replace - Whether to replace current history entry
* @returns {void}
*/
export const navigate = (path, options = {}) => {
const { replace = false } = options;
// istanbul ignore else
if (!setup.isServer) {
// Client-side navigation
const url = new URL(path, globalThis.location.origin);
const route = matchRoute(url.pathname);
// Update history
if (replace) {
globalThis.history.replaceState({}, "", path);
} else {
globalThis.history.pushState({}, "", path);
}
// Update router state
setRouterState(url.pathname, url.search, route?.params);
} else {
// Server-side navigation - throw error
console.error("Direct navigation is not supported on server");
}
};
/**
* Extract route params
* @param {string} pattern
* @param {string} path
* @returns {Record<string, string>}
*/
export const extractParams = (pattern, path) => {
const params = {};
const patternParts = pattern.split("/");
const pathParts = path.split("/");
if (patternParts.length !== pathParts.length) return null;
for (let i = 0; i < patternParts.length; i++) {
const patternPart = patternParts[i];
const pathPart = pathParts[i];
if (patternPart.startsWith(":")) {
params[patternPart.slice(1)] = pathPart;
} else if (patternPart !== pathPart) {
return null;
}
}
return params;
};
/**
* Fix the URL of a route
* @param {string=} url
* @returns
*/
export const fixRouteUrl = (url) => {
if (!url) return "/";
if (url.startsWith("/")) {
return url;
}
return `/${url}`;
};
/**
* Client only reload utility
* WORK IN PROGRESS
* @param {boolean} forceFetch - Force fetch from server
* @returns {void}
*/
// export const reload = (forceFetch = false) => {
// if (!setup.isServer) {
// // Client-side reload
// if (forceFetch) {
// window.location.reload();
// } else {
// // Soft reload - just update router state
// const { pathname, search } = window.location;
// setRouterState(pathname, search);
// }
// } else {
// // Server-side reload - throw error
// console.error("Reload is not supported on server");
// }
// };
/**
* Isomorphic redirect utility
* WORK IN PROGRESS
* @param {string} path - The path to redirect to
* @param {object} options - Redirect options
* @param {number} options.status - HTTP status code (server-side only)
* @param {boolean} options.replace - Whether to replace current history entry (client-side only)
* @returns {void}
*/
// export const redirect = (path, options = {}) => {
// const { status = 302, replace = true } = options;
// if (!setup.isServer) {
// // Client-side redirect
// navigate(path, { replace });
// } else {
// // Server-side redirect
// const error = new Error(`Redirect to ${path}`);
// error.status = status;
// error.location = path;
// throw error;
// }
// };
// Utility to handle server-side redirects in your server entry point
// export const handleServerRedirect = (error, res) => {
// if (error.location && error.status) {
// res.writeHead(error.status, {
// Location: error.location,
// "Content-Type": "text/plain",
// });
// res.end(`Redirecting to ${error.location}...`);
// return true;
// }
// return false;
// };