UNPKG

vite-plugin-react-server

Version:
252 lines (240 loc) 8.36 kB
/** * # createAbsoluteUrl * * This function takes a baseURL and a public origin and returns a function that takes a path and returns the path with the baseURL attached to it. * * @example * ```ts * const absoluteURL = createAbsoluteURL("/mmc", "https://bidoof.com") * console.log(absoluteURL("/test")) // "https://bidoof.com/mmc/test" * ``` * * @example * ```ts * const absoluteURL = createAbsoluteURL("/mmc", "https://bidoof.com") * console.log(absoluteURL("/test")) // "https://bidoof.com/mmc/test" * ``` * * This can replace code like `${process.env.VITE_PUBLIC_ORIGIN}/test` with `absoluteUrl('/test')`, and you can be sure that it will work after * changing the plugin settings. */ export const createAbsoluteURL = ( withBaseURL: string, withPublicOrigin: string ) => { const baseURL = createBaseURL(withBaseURL); if (withPublicOrigin === "" || typeof withPublicOrigin !== "string") return baseURL; return (path: string) => { const pathWithBaseURL = baseURL(path); try { return new URL(pathWithBaseURL, withPublicOrigin).toString(); } catch { // Fallback: ensure proper URL construction const publicOrigin = withPublicOrigin.endsWith("/") ? withPublicOrigin.slice(0, -1) : withPublicOrigin; const pathPart = pathWithBaseURL.startsWith("/") ? pathWithBaseURL : `/${pathWithBaseURL}`; return publicOrigin + pathPart; } }; }; /** * # createBaseURL * * This function takes a baseURL and returns a function that takes a path and returns the path with the baseURL attached to it. * * @example * ```ts * const baseURL = createBaseURL("/mmc") * console.log(baseURL("/test")) // "/mmc/test" * ``` * * @example * ```ts * const baseURL = createBaseURL("/mmc/") * console.log(baseURL("/test")) // "/mmc/test" * ``` * * This can replace code like `${import.meta.env.test` with `baseURL(path)`, and you can be sure that it will work after * changing the plugin settings. * * Path handling logic: * 1. For baseURL ending with "/": * - If path starts with "/", slice off the leading slash to avoid double slashes * - If path doesn't start with "/", keep it as is * * 2. For baseURL not ending with "/": * - If path starts with "/", directly concatenate with baseURL * - If path doesn't start with "/", add a slash between baseURL and path * * baseURL "src" + path "src/test" -> should not concatenate to src/src/test * baseURL "/" + path "https://bidoof.com" -> should not concatenate to /https://bidoof.com" */ export const createBaseURL = (withBaseURL: string) => { if(withBaseURL === ''){ return (path: string) => path; } if (withBaseURL.endsWith("/")) { return (path: string) => { if (path === "") return withBaseURL; if (path.startsWith(withBaseURL) || isAbsoluteURL(path)) return path; return `${withBaseURL}${removeLeadingSlash(path)}`; }; } else { return (path: string) => { if (path === "") return withBaseURL; if (isAbsoluteURL(path)) return path; if (path.startsWith("/")) return withBaseURL + path; if (path.startsWith(withBaseURL)) return path; return `${withBaseURL}/${path}`; }; } }; /** Remove a single trailing slash from the URL if it ends with one */ export const removeTrailingSlash = (url: string) => url.endsWith("/") ? url.slice(0, -1) : url; /** Add a single trailing slash to the URL if it doesn't end with one */ export const addTrailingSlash = (url: string) => url.endsWith("/") ? url : `${url}/`; export const removeLeadingSlash = (url: string) => url.startsWith("/") ? url.slice(1) : url; export const addLeadingSlash = (url: string) => url.startsWith("/") ? url : `/${url}`; export const isBlankRegex = /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?\/\//; export const isAbsoluteURL = (url: string) => isBlankRegex.test(url) || url.startsWith("//"); export const folderName = (path: string, withBaseURL: string) => { const baseURL = createBaseURL(withBaseURL); return baseURL(path.replace(/\[index.(html?|rsc|HTML?)]$/, "")); }; /** * # createPageURL * * This function takes a baseURL, public origin and a optional normalizer function that mirrors the baseURL's format. * If baseURL ends with a slash, we continue it using the URL itself (must end with a slash) * * - `indexRSC`: The path to the index.rsc file * - `moduleBaseURL`: The baseURL to use for the module * * These can be passed in directly to the createReactFetcher and also determine the defaults when no input are provided. * * @example * ```ts * import { createFromFetch } from "react-server-dom-esm/client.browser"; * const parsedURL = pageURL(window.location.pathname ?? "/"); * const data = createFromFetch( * fetch(parsedURL.indexRSC, { * headers: { * Accept: "text/x-component", * } * }), * { * callServer: callServer, * moduleBaseURL: parsedURL.moduleBaseURL, * } * ); * ``` * * The moduleBasePath being set at the config level as "", * then we pass it to create a stream `renderToPipeableStream(elements, moduleBasePath)`, and we see * ```text * 2:I["src/components/Clickable.client-Dx9diOqr.js","ClientClickable"] * ``` * */ export const createPageURL = ( withBaseURL: string, withPublicOrigin: string, isDev = false, normalizer = !withBaseURL.endsWith("/") ? removeTrailingSlash : addTrailingSlash ) => { return (to: string, fileName: string = "index.rsc") => { try { // Ensure withBaseURL is a string const baseURLString = typeof withBaseURL === 'string' ? withBaseURL : String(withBaseURL || '/'); // Create the base URL first const folderName = addTrailingSlash( to.replace(/\[index.(html?|rsc|HTML?)]$/, "") ); const baseURL = createBaseURL(baseURLString); const rscPath = baseURL(folderName) + fileName; // Create moduleBaseURL and normalize it to match input format const moduleBaseURL = parseURL(baseURLString, withPublicOrigin); if (moduleBaseURL.type === "error") { if(isDev) console.error("Error parsing moduleBaseURL", moduleBaseURL.error); throw moduleBaseURL.error; } const indexRSC = parseURL(rscPath, withPublicOrigin); if (indexRSC.type === "error") { throw indexRSC.error; } return { indexRSC: indexRSC.url.toString(), moduleBaseURL: normalizer(moduleBaseURL.url.toString()), }; } catch (error) { if (isDev) console.error("Error parsing pageURL", error); const shouldJoin = !to.endsWith("/") && !fileName.startsWith("/"); const shouldSlice = to.endsWith("/") && fileName.startsWith("/"); return { indexRSC: to + (shouldJoin ? "/" : "") + (shouldSlice ? fileName.slice(1) : fileName), moduleBaseURL: typeof withBaseURL === 'string' ? withBaseURL : String(withBaseURL || '/'), }; } }; }; /** * # moduleBaseURL * * This function takes a baseURL, public origin and a optional normalizer function that mirrors the baseURL's format. * * @example * ```ts * const moduleBaseURL = parseURL("/mmc", "https://bidoof.com") * if(moduleBaseURL.type === "error") { * console.error(moduleBaseURL.error) * } else { * console.log(moduleBaseURL.url) * } * ``` * **/ export const parseURL = ( url: string, base: string ): | { type: "success"; url: URL; error?: never; base?: never } | { type: "error"; url: string; base: string; error: Error } => { try { // If base is empty or not a valid absolute URL, use window.location.origin as fallback (browser only) let effectiveBase = base; if (!effectiveBase || effectiveBase === "/" || !isAbsoluteURL(effectiveBase)) { if (typeof window !== "undefined" && window.location) { effectiveBase = window.location.origin; } else if (!effectiveBase) { effectiveBase = "http://localhost"; // Fallback for non-browser environments } } // If url is already absolute, use it directly if (isAbsoluteURL(url)) { return { type: "success", url: new URL(url), }; } return { type: "success", url: new URL(url, effectiveBase), }; } catch (error) { return { type: "error", url: url, base: base, error: error as Error }; } };