vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
158 lines (132 loc) • 4.49 kB
text/typescript
import type { Node } from "estree";
import type { StreamPluginOptions } from "../types.js";
import { DEFAULT_CONFIG } from "../config/defaults.js";
import { basename } from "path";
const REACT_DIRECTIVES = new Set(["use client", "use server"]);
function createSourceMap(id: string, code: string, mappings: string) {
return {
version: 3,
file: basename(id),
sources: [id],
sourcesContent: [code],
names: [],
mappings,
sourceRoot: "",
};
}
function removeRanges(code: string, ranges: Array<{ start: number; end: number }>) {
// Sort ranges in reverse order to not affect positions
ranges.sort((a, b) => b.start - a.start);
let result = code;
for (const range of ranges) {
result = result.slice(0, range.start) + result.slice(range.end);
}
return result;
}
function countLines(str: string): number {
let count = 1;
for (let i = 0; i < str.length; i++) {
if (str[i] === '\n') count++;
}
return count;
}
export function reactPreservePlugin(_options: StreamPluginOptions): import("vite").Plugin {
const meta: Record<string, Set<string>> = {};
return {
name: "vite-plugin-react-server:preserve-directives",
enforce: "post",
transform: {
order: "post", // Ensure this runs last in transform phase
handler(code: string, id: string) {
// Skip node_modules and vite files
if (id.includes("node_modules") || id.includes("vite/dist") || !id.match(DEFAULT_CONFIG.FILE_REGEX)) {
return null;
}
let ast: Node;
try {
ast = this.parse(code, {
allowReturnOutsideFunction: true,
jsx: true,
}) as Node;
} catch (e) {
console.warn(`[PreservePlugin] Failed to parse ${id}`, e);
return null;
}
if (ast.type !== "Program") {
return null;
}
const rangesToRemove: Array<{ start: number; end: number }> = [];
let hasChanged = false;
let lineCount = 1;
let mappings = "AAAA"; // Initial mapping for first line
// Only look at top-level directives
for (const node of ast.body) {
if (node.type !== "ExpressionStatement") {
break;
}
let directive: string | null = null;
if ("directive" in node) {
directive = node.directive;
} else if (
node.expression.type === "Literal" &&
typeof node.expression.value === "string" &&
REACT_DIRECTIVES.has(node.expression.value)
) {
directive = node.expression.value;
}
if (directive && "start" in node && "end" in node) {
meta[id] ||= new Set<string>();
meta[id].add(directive);
rangesToRemove.push({
start: node.start as number,
end: node.end as number
});
hasChanged = true;
// Add mapping for each line removed
const removedLines = code.slice(node.start as number, node.end as number).split('\n').length - 1;
for (let i = 0; i < removedLines; i++) {
mappings += ";AACA";
lineCount++;
}
}
}
if (!hasChanged) {
return null;
}
const newCode = removeRanges(code, rangesToRemove);
const sourceMap = createSourceMap(id, code, mappings);
return {
code: newCode,
map: sourceMap,
meta: {
directives: Array.from(meta[id] || []),
},
};
},
},
renderChunk(code, chunk) {
const chunkDirectives = new Set<string>();
// Collect directives from all modules in chunk
for (const id of chunk.moduleIds) {
if (meta[id]) {
meta[id].forEach((d) => chunkDirectives.add(d));
}
}
if (chunkDirectives.size) {
const directivesCode = Array.from(chunkDirectives)
.map((d) => `"${d}";`)
.join("\n") + "\n";
const newCode = directivesCode + code;
// Create source map for the prepended directives
const lineCount = countLines(directivesCode);
const mappings = "AAAA" + ";AACA".repeat(lineCount - 1);
const sourceMap = createSourceMap(chunk.fileName, code, mappings);
return {
code: newCode,
map: sourceMap,
};
}
return null;
},
};
}