vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
370 lines (350 loc) • 10.9 kB
text/typescript
import type { ResolvedConfig, ViteDevServer } from "vite";
import type {
AutoDiscoveredFiles,
ResolvedUserOptions,
SerializableRecord,
ResolvedUserConfig,
SerializedUserOptions,
Serializable,
} from "../types.js";
import { cleanObject } from "./cleanObject.js";
import { DEFAULT_CONFIG } from "../config/defaults.js";
// Common non-serializable functions in Vite's resolved config
const VITE_NON_SERIALIZABLE_FUNCTIONS = new Set([
"renderChunk",
"buildStart",
"buildEnd",
"watchChange",
"resolveId",
"config",
"output[].entryFileNames",
"output[].chunkFileNames",
"output[].assetFileNames",
"transform",
"handler",
"configureServer",
"shouldTransformCachedModule",
"generateBundle",
"renderStart",
"writeBundle",
"hotUpdate",
"configResolved",
"configurePreviewServer",
"handleHotUpdate",
"load",
"augmentChunkHash",
"closeBundle",
"entryFileNames",
"assetFileNames",
"chunkFileNames",
"createEnvironment",
"sourcemapIgnoreList",
"assetsInclude",
"info",
"warn",
"warnOnce",
"error",
"clearScreen",
"hasErrorLogged",
"set",
"plugins",
"getSortedPlugins",
"getSortedPluginHooks",
"createResolver",
"fsDenyGlob",
// Nested plugin functions
"plugins[].renderChunk",
"plugins[].buildStart",
"plugins[].buildEnd",
"plugins[].watchChange",
"plugins[].resolveId",
"plugins[].config",
"plugins[].transform",
"plugins[].handler",
"plugins[].configureServer",
"plugins[].shouldTransformCachedModule",
"plugins[].generateBundle",
"plugins[].renderStart",
"plugins[].writeBundle",
"plugins[].hotUpdate",
"plugins[].configResolved",
"plugins[].configurePreviewServer",
"plugins[].handleHotUpdate",
"plugins[].load",
"plugins[].augmentChunkHash",
"plugins[].closeBundle",
"plugins[].entryFileNames",
"plugins[].assetFileNames",
"plugins[].chunkFileNames",
"plugins[].createEnvironment",
"plugins[].sourcemapIgnoreList",
"plugins[].assetsInclude",
]);
// Common non-serializable functions in our plugin's options
const PLUGIN_NON_SERIALIZABLE_FUNCTIONS = new Set([
"Page",
"props",
"normalizer",
"Root",
"Html",
"onEvent",
"onMetrics",
"autoDiscover",
]);
// Helper function to serialize RegExp objects
function serializeRegExp(regex: RegExp) {
return {
source: regex.source,
flags: regex.flags,
__isRegExp: true,
};
}
// Helper function to deserialize RegExp objects
export function deserializeRegExp<T>(obj: T): Extract<T, SerializableRecord> {
if (
obj &&
typeof obj === "object" &&
obj != null &&
"__isRegExp" in obj &&
typeof obj["__isRegExp"] === "boolean" &&
"source" in obj &&
typeof obj["source"] === "string" &&
"flags" in obj &&
typeof obj["flags"] === "string"
) {
return new RegExp(obj["source"], obj["flags"]) as unknown as Extract<
T,
SerializableRecord
>;
}
if (Array.isArray(obj)) {
// Check if this is a serialized Map (array of [key, value] pairs)
if (obj.length > 0 && Array.isArray(obj[0]) && obj[0].length === 2) {
return new Map(obj) as unknown as Extract<T, SerializableRecord>;
}
return obj.map(deserializeRegExp) as unknown as Extract<
T,
SerializableRecord
>;
}
if (obj && typeof obj === "object") {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = deserializeRegExp(value) as unknown as Extract<
T,
SerializableRecord
>[keyof T];
}
return result as unknown as Extract<T, SerializableRecord>;
}
return obj as unknown as Extract<T, SerializableRecord>;
}
// Helper function to recursively process objects for serialization
export function processForSerialization<T>(
obj: T
): Extract<T, Serializable> {
if (obj instanceof RegExp) {
return serializeRegExp(obj) as unknown as Extract<T, Serializable>;
}
if (obj instanceof Map) {
// Convert Map to array of [key, value] pairs for serialization
return Array.from(obj.entries()) as unknown as Extract<T, Serializable>;
}
if (Array.isArray(obj)) {
return obj.map(processForSerialization) as unknown as Extract<
T,
Serializable
>;
}
if (obj && typeof obj === "object") {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = processForSerialization(value) as unknown as Extract<
T,
Serializable
>[keyof T];
}
return result as unknown as Extract<T, Serializable>;
}
return obj as unknown as Extract<T, Serializable>;
}
export function serializeResolvedConfig<T extends ResolvedConfig = ResolvedConfig>(
config: T,
knownNonSerializableFunctions: Set<string> = VITE_NON_SERIALIZABLE_FUNCTIONS
) {
if (!config) {
return undefined;
}
const {
getSortedPluginHooks: _getSortedPluginHooks,
getSortedPlugins: _getSortedPlugins,
assetsInclude: _assetsInclude,
environments: _environments,
// extract known vite function properties
...handlerOptions
} = config;
// Preserve a minimal environments structure for CSS processing
const minimalEnvironments = _environments ? {
client: {
resolve: { conditions: ['browser', 'module', 'import'] },
consumer: 'client',
optimizeDeps: { include: [] },
dev: { optimizeDeps: { include: [] } },
build: { outDir: 'dist' },
},
ssr: {
resolve: { conditions: ['node', 'import'] },
consumer: 'server',
optimizeDeps: { include: [] },
dev: { optimizeDeps: { include: [] } },
build: { outDir: 'dist' },
},
} : undefined;
// Clean the object to remove non-serializable properties and process RegExp objects
const cleaned = cleanObject(handlerOptions, knownNonSerializableFunctions) as any;
// Add back the minimal environments if they existed
if (minimalEnvironments) {
cleaned.environments = minimalEnvironments;
}
return processForSerialization(cleaned);
}
export function serializeResolvedUserConfig<T extends ResolvedUserConfig>(
config: T,
knownNonSerializableFunctions: Set<string> = VITE_NON_SERIALIZABLE_FUNCTIONS
) {
const {
assetsInclude: _assetsInclude,
// extract known vite function properties
...handlerOptions
} = config;
// Clean the object to remove non-serializable properties and process RegExp objects
return processForSerialization(
cleanObject(handlerOptions, knownNonSerializableFunctions)
);
}
// For Vite's config
export const serializedDevServerConfig = <T extends ViteDevServer["config"]>(
config: T,
customNonSerializableFunctions: Set<string> = PLUGIN_NON_SERIALIZABLE_FUNCTIONS
) => {
const {
getSortedPluginHooks: _getSortedPluginHooks,
getSortedPlugins: _getSortedPlugins,
assetsInclude: _assetsInclude,
build: _build,
...handlerOptions
} = config;
return processForSerialization(
cleanObject(handlerOptions, customNonSerializableFunctions)
);
};
// For your own options (if you need custom non-serializable functions)
export const serializedOptions = <T extends ResolvedUserOptions>(
userOptions: T,
autoDiscoveredFiles: AutoDiscoveredFiles,
customNonSerializableFunctions: Set<string> = PLUGIN_NON_SERIALIZABLE_FUNCTIONS
): SerializedUserOptions => {
const {
Page: _Page,
props: _props,
normalizer: _normalizer,
Root: _Root,
Html: _Html,
onEvent: _onEvent,
onMetrics: _onMetrics,
build: _build,
loader: _loader,
autoDiscover: autoDiscover,
propsExportName: propsExportName,
pageExportName: pageExportName,
serverPipeableStreamOptions: serverPipeableStreamOptions,
clientPipeableStreamOptions: clientPipeableStreamOptions,
...handlerOptions
} = userOptions;
const {
entryFile: _entryFile,
chunkFile: _chunkFile,
assetFile: _assetFile,
pages: _pages,
...buildOptions
} = _build ?? {};
// Preserve the build properties that should be serialized
// Respect user options first, then fall back to defaults
const serializedBuild = {
...DEFAULT_CONFIG.BUILD, // Start with defaults
...buildOptions, // Override with user options
pages: autoDiscoveredFiles
? Array.from(autoDiscoveredFiles.urlMap.keys())
: [],
};
const {
isServerFunctionCode: _isServerFunctionCode,
isClientComponentCode: _isClientComponentCode,
isClientComponentByCode: _isClientComponentByCode,
isClientComponentByName: _isClientComponentByName,
getDirectiveType: _getDirectiveType,
allowedDirectives: allowedDirectives,
...loaderOptions
} = _loader ?? {};
const {
modulePattern: _modulePattern,
cssPattern: _cssPattern,
jsonPattern: _jsonPattern,
clientPattern: _clientPattern,
propsPattern: _propsPattern,
pagePattern: _pagePattern,
htmlPattern: _htmlPattern,
rscPattern: _rscPattern,
serverPattern: _serverPattern,
cssModulePattern: _cssModulePattern,
vendorPattern: _vendorPattern,
nodePattern: _nodePattern,
dotPattern: _dotPattern,
virtualPattern: _virtualPattern,
...serializedAutoDiscover
} = autoDiscover;
const result = {
...handlerOptions,
Page: typeof _Page === 'string' ? _Page : undefined,
Html: typeof _Html === 'string' ? _Html : undefined,
Root: typeof _Root === 'string' ? _Root : undefined,
normalizer: undefined,
onEvent: undefined,
onMetrics: undefined,
propsExportName: propsExportName,
pageExportName: pageExportName,
build: serializedBuild,
loader: {
directivePattern: {
config: {
validate: undefined,
...allowedDirectives,
},
},
...loaderOptions,
},
autoDiscover: {
modulePattern: serializeRegExp(_modulePattern),
serverPattern: serializeRegExp(_serverPattern),
clientPattern: serializeRegExp(_clientPattern),
pagePattern: serializeRegExp(_pagePattern),
propsPattern: serializeRegExp(_propsPattern),
cssPattern: serializeRegExp(_cssPattern),
jsonPattern: serializeRegExp(_jsonPattern),
htmlPattern: serializeRegExp(_htmlPattern),
cssModulePattern: serializeRegExp(_cssModulePattern),
vendorPattern: serializeRegExp(_vendorPattern),
nodePattern: serializeRegExp(_nodePattern),
dotPattern: serializeRegExp(_dotPattern),
virtualPattern: serializeRegExp(_virtualPattern),
rscPattern: serializeRegExp(_rscPattern),
...serializedAutoDiscover,
},
serverPipeableStreamOptions: serverPipeableStreamOptions,
clientPipeableStreamOptions: clientPipeableStreamOptions,
} as const
// Clean the object to remove non-serializable properties and process RegExp objects
const cleanedResult = cleanObject(result, customNonSerializableFunctions);
const finalResult = processForSerialization(cleanedResult);
return finalResult as never
};