vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
157 lines (140 loc) • 5.02 kB
text/typescript
// vprs-side adapter for the React-RSC Node ESM loader. The actual
// load/resolve hooks live in react-server-loader/loader — this file
// threads vprs's worker context (parent MessagePort, serialized user
// options, the `createDefaultModuleID` policy) into rsl's factory and
// publishes the resulting hooks to the worker bootstrap path.
import type {
ResolvedUserOptions,
SerializedResolvedConfig,
SerializedUserOptions,
} from "../types.js";
import type { ModuleInfo } from "rollup";
import { parentPort } from "node:worker_threads";
import type { MessagePort } from "node:worker_threads";
import type {
InitializedReactLoaderMessage,
ServerModuleMessage,
} from "../worker/rsc/types.js";
import { join } from "node:path";
import { hydrateUserOptions } from "../helpers/hydrateUserOptions.js";
import type { LoadHook, ResolveHook } from "node:module";
import { createReactLoader } from "react-server-loader/loader";
import { createLogger, type Logger } from "vite";
import { createDefaultModuleID } from "../config/createModuleID.js";
export type LoaderOptions = {
id: string;
resolveDependencies?: boolean;
format?: string;
conditions?: string[];
importAssertions?: Record<string, unknown>;
importAttributes?: Record<string, unknown>;
source: string;
};
export type LoaderFunction = (options: LoaderOptions) => Promise<ModuleInfo>;
let initialized = false;
let userOptions: ResolvedUserOptions;
let loaderPort: MessagePort | null;
let resolvedConfig: SerializedResolvedConfig;
let logger: Logger;
let verbose = false;
let load: LoadHook = async (url, context, nextLoad) => nextLoad(url, context);
let resolveHook: ResolveHook = async (specifier, context, nextResolve) =>
nextResolve(specifier, context);
export function initialize(data: {
id: string;
port: MessagePort;
userOptions: SerializedUserOptions;
resolvedConfig: SerializedResolvedConfig;
}) {
const {
id,
port,
userOptions: serializedUserOptions,
resolvedConfig: serializedResolvedConfig,
} = data;
verbose = serializedUserOptions?.verbose ?? false;
logger = createLogger(serializedResolvedConfig?.logLevel ?? "info", {
prefix: id,
});
resolvedConfig = serializedResolvedConfig;
if (verbose) {
logger.info(`Initializing with options: ${id}`);
}
loaderPort = port;
const resolvedUserOptions = hydrateUserOptions(serializedUserOptions);
if (resolvedUserOptions.type === "error") {
throw resolvedUserOptions.error;
}
userOptions = resolvedUserOptions.userOptions;
// Materialise vprs's default moduleID policy if the user didn't supply
// one. Done here at init time so rsl's loader can reach for a stable
// reference per call without recreating it.
if (typeof userOptions.moduleID !== "function") {
const buildConfigEnv =
resolvedConfig?.configEnv ?? { command: "build", mode: "production" };
userOptions.moduleID = createDefaultModuleID(
{
moduleBase: userOptions.moduleBase,
moduleBasePath: userOptions.moduleBasePath,
autoDiscover: userOptions.autoDiscover,
build: userOptions.build,
dev: userOptions.dev,
moduleBaseURL: userOptions.moduleBaseURL,
projectRoot: userOptions.projectRoot,
},
buildConfigEnv
);
}
const { load: rslLoad, resolve: rslResolve } = createReactLoader({
loader: userOptions.loader,
verbose,
logger,
moduleID: (filePath) => {
let moduleID = filePath;
let finalID = filePath;
if (userOptions?.normalizer) {
const [, value] = userOptions.normalizer(filePath);
moduleID = join(userOptions.moduleBasePath, value);
finalID = userOptions.moduleID?.(moduleID) || moduleID;
}
return finalID;
},
onTransform: ({ filePath, transformedId, source }) => {
if (loaderPort) {
loaderPort.postMessage({
type: "SERVER_MODULE",
id: transformedId,
url: filePath,
source,
} satisfies ServerModuleMessage);
}
},
});
load = rslLoad;
resolveHook = rslResolve;
if (!initialized && loaderPort) {
loaderPort.postMessage({
type: "INITIALIZED_REACT_LOADER",
id,
} satisfies InitializedReactLoaderMessage);
}
initialized = true;
}
// Module-level load/resolve hooks for the worker bootstrap path. They
// forward to whatever rsl's factory produced at init time; if init was
// skipped (unexpected) they fall back to a pass-through that lets the
// underlying loader chain handle the URL untouched.
export const loadHook: LoadHook = async (url, context, nextLoad) => {
if (!initialized) {
initialize({
id: "react-loader",
port: parentPort!,
userOptions: {} as SerializedUserOptions,
resolvedConfig: {} as SerializedResolvedConfig,
});
}
return load(url, context, nextLoad);
};
export const resolve: ResolveHook = async (specifier, context, nextResolve) =>
resolveHook(specifier, context, nextResolve);
export { loadHook as load };