vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
252 lines (240 loc) • 8.36 kB
text/typescript
/**
* # 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 };
}
};