vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
207 lines (193 loc) • 6.23 kB
text/typescript
import { type MessagePort } from "node:worker_threads";
import type { LoadHook, ResolveHook, ModuleFormat } from "node:module";
import type {
ResolvedUserOptions,
SerializedResolvedConfig,
SerializedUserOptions,
} from "../types.js";
import { fileURLToPath } from "node:url";
import { preprocessCSS, resolveConfig } from "vite";
import { readFile } from "node:fs/promises";
import { env } from "../utils/env.js";
import type {
CssFileMessage,
InitializedCssLoaderMessage,
} from "../worker/rsc/types.js";
import { hydrateUserOptions } from "../helpers/hydrateUserOptions.js";
import { toError } from "../error/toError.js";
import { sendMessage } from "../worker/sendMessage.js";
import type { ErrorMessage } from "../worker/types.js";
/**
* Global port for communication between the main thread and the CSS loader.
* This port is used to send CSS file requests and receive responses.
*/
export let loaderPort: MessagePort | undefined;
let resolvedConfig: SerializedResolvedConfig | null;
let userOptions: ResolvedUserOptions | undefined;
/**
* Initializes the CSS loader with the necessary communication channels.
* Sets up message handlers for CSS file requests and responses.
*
* @param data - Configuration data for the CSS loader
* @param data.port - The message port for communication
*/
export async function initialize(data: {
id: string;
port: MessagePort;
resolvedConfig: SerializedResolvedConfig;
userOptions: SerializedUserOptions;
}) {
loaderPort = data.port;
resolvedConfig = data.resolvedConfig;
const resolvedUserOptions = hydrateUserOptions(data.userOptions);
if (resolvedUserOptions.type === "error") {
if (loaderPort) {
sendMessage(
{
type: "ERROR",
id: "css-loader",
error: resolvedUserOptions.error,
} satisfies ErrorMessage,
loaderPort
);
}
throw resolvedUserOptions.error;
}
// Use the hydrated user options directly (includes recreated functions)
userOptions = resolvedUserOptions.userOptions;
if (loaderPort) {
sendMessage(
{
type: "INITIALIZED_CSS_LOADER",
id: data.id,
} satisfies InitializedCssLoaderMessage,
loaderPort
);
}
}
/**
* Processes a CSS file request.
* Sends a request to the main thread and waits for the processed CSS.
*
* @param filePath - The file system path of the CSS file
* @param config - The Vite config
* @returns A promise that resolves to the processed CSS content
*/
async function processCssFile(
filePath: string,
inline: boolean
): Promise<{ format: ModuleFormat; source: string; shortCircuit: boolean }> {
try {
// Convert file URL to path if needed
const path = filePath.startsWith("file://")
? fileURLToPath(filePath)
: filePath;
// Process CSS using Vite's preprocessCSS
const source = await readFile(path, "utf-8");
let moduleID = path;
if (userOptions?.normalizer) {
const [, value] = userOptions.normalizer(path);
moduleID = userOptions.moduleID?.(value || path) || path;
}
// Try to process CSS with preprocessCSS, fall back to raw CSS if config is incomplete
let processed: { code: string; modules?: any };
try {
// Create a minimal config with environments that preprocessCSS expects
const viteConfig = await resolveConfig(
{
...resolvedConfig,
env: env,
// do-not re-resolve the config file as it would import the plugin again which we do not need.
configFile: false,
},
"serve"
);
processed = await preprocessCSS(source, path, viteConfig);
} catch (error) {
// If preprocessCSS fails, fall back to raw CSS
if (loaderPort) {
sendMessage(
{
type: "ERROR",
id: moduleID,
error: toError(error),
} satisfies ErrorMessage,
loaderPort
);
}
processed = { code: source, modules: {} };
}
// If we're processing CSS for a specific page, notify the message handler
if (loaderPort) {
sendMessage(
{
type: "CSS_FILE",
id: moduleID,
content: processed.code,
} satisfies CssFileMessage,
loaderPort
);
}
// Return a module that can be used by React components
if (inline) {
return {
format: "module",
source: processed.code,
shortCircuit: true,
};
}
return {
format: "module",
source: `export default ${JSON.stringify(processed.modules || {})};`,
shortCircuit: true,
};
} catch (error) {
const err = toError(error);
if (loaderPort) {
sendMessage(
{
type: "ERROR",
id: "css-loader",
error: err,
} satisfies ErrorMessage,
loaderPort
);
}
throw err;
}
}
/**
* Vite's load hook implementation for CSS files.
* Handles CSS file loading requests and returns a placeholder module.
* The actual CSS content is processed in the main thread.
*
* @param url - The URL of the module to load
* @param context - The load hook context
* @param defaultLoad - The default load function
* @returns A promise that resolves to the module content
*/
export const load: LoadHook = async (url, context, defaultLoad) => {
const [name, query] = url.split("?");
if (name.endsWith(".css")) {
let isInline = query?.startsWith("inline") || query?.includes("&inline");
if (isInline && query.includes('inline=')) {
// handle = true/false
const match = query.match(/inline=(1|true|0|false)/)?.[1];
isInline = match === '1' || match === 'true';
}
return processCssFile(url, isInline);
}
return defaultLoad(url, context);
};
/**
* Vite's resolve hook implementation.
* Handles module resolution during development.
*
* @param specifier - The module specifier to resolve
* @param context - The resolve hook context
* @param defaultResolve - The default resolve function
* @returns A promise that resolves to the resolved module
*/
export const resolve: ResolveHook = (specifier, context, defaultResolve) => {
return defaultResolve(specifier, context);
};