vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
648 lines (645 loc) • 20 kB
JavaScript
/**
* vite-plugin-react-server
* Copyright (c) Nico Brinkkemper
* MIT License
*/
import * as acorn from 'acorn-loose';
import { basename } from 'path';
import { setSourceMapsSupport, SourceMap } from 'node:module';
let stashedGetSource = null;
setSourceMapsSupport(true, {
nodeModules: true,
// Enable for node_modules files
generatedCode: true
// Enable for generated code
});
let loaderPort;
async function getSource(url, context, defaultGetSource) {
stashedGetSource = defaultGetSource;
return defaultGetSource(url, context, defaultGetSource);
}
function addExportedEntry(exportedEntries, localNames, localName, exportedName, type, loc) {
if (localNames.has(localName)) {
return;
}
exportedEntries.push({
localName,
exportedName,
type,
loc,
originalLine: -1,
originalColumn: -1,
originalSource: -1,
nameIndex: -1
});
}
function addLocalExportedNames(exportedEntries, localNames, node) {
switch (node.type) {
case "Identifier":
addExportedEntry(
exportedEntries,
localNames,
node.name,
node.name,
null,
node.loc
);
return;
case "ObjectPattern":
for (let i = 0; i < node.properties.length; i++)
addLocalExportedNames(exportedEntries, localNames, node.properties[i]);
return;
case "ArrayPattern":
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
if (element)
addLocalExportedNames(exportedEntries, localNames, element);
}
return;
case "Property":
addLocalExportedNames(exportedEntries, localNames, node.value);
return;
case "AssignmentPattern":
addLocalExportedNames(exportedEntries, localNames, node.left);
return;
case "RestElement":
addLocalExportedNames(exportedEntries, localNames, node.argument);
return;
case "ParenthesizedExpression":
addLocalExportedNames(exportedEntries, localNames, node.expression);
return;
}
}
function transformServerModule(source, program, url, sourceMap, _loader, port) {
const body = program.body;
const exportedEntries = [];
const localNames = /* @__PURE__ */ new Set();
for (let i = 0; i < body.length; i++) {
const node = body[i];
switch (node.type) {
case "ExportAllDeclaration":
break;
case "ExportDefaultDeclaration":
if (node.declaration.type === "Identifier") {
addExportedEntry(
exportedEntries,
localNames,
node.declaration.name,
"default",
null,
node.declaration.loc
);
} else if (node.declaration.type === "FunctionDeclaration") {
if (node.declaration.id) {
addExportedEntry(
exportedEntries,
localNames,
node.declaration.id.name,
"default",
"function",
node.declaration.id.loc
);
}
}
continue;
case "ExportNamedDeclaration":
if (node.declaration) {
if (node.declaration.type === "VariableDeclaration") {
const declarations = node.declaration.declarations;
for (let j = 0; j < declarations.length; j++) {
addLocalExportedNames(
exportedEntries,
localNames,
declarations[j].id
);
}
} else {
const name = node.declaration.id.name;
addExportedEntry(
exportedEntries,
localNames,
name,
name,
node.declaration.type === "FunctionDeclaration" ? "function" : null,
node.declaration.id.loc
);
}
}
if (node.specifiers) {
const specifiers = node.specifiers;
for (let j = 0; j < specifiers.length; j++) {
const specifier = specifiers[j];
addExportedEntry(
exportedEntries,
localNames,
specifier.local.name,
specifier.exported.name,
null,
specifier.local.loc
);
}
}
continue;
}
}
let mappings = sourceMap && typeof sourceMap.mappings === "string" ? sourceMap.mappings : "";
let newSrc = source;
if (exportedEntries.length > 0) {
let lastSourceIndex = 0;
let lastOriginalLine = 0;
let lastOriginalColumn = 0;
let lastNameIndex = 0;
let sourceLineCount = 0;
let lastMappedLine = 0;
if (sourceMap) {
let nextEntryIdx = 0;
let nextEntryLine = exportedEntries[nextEntryIdx].loc.start.line;
let nextEntryColumn = exportedEntries[nextEntryIdx].loc.start.column;
readMappings(
mappings,
(generatedLine2, generatedColumn, sourceIndex, originalLine, originalColumn, nameIndex) => {
if (generatedLine2 > nextEntryLine || generatedLine2 === nextEntryLine && generatedColumn > nextEntryColumn) {
if (lastMappedLine === nextEntryLine) {
exportedEntries[nextEntryIdx].originalLine = lastOriginalLine;
exportedEntries[nextEntryIdx].originalColumn = lastOriginalColumn;
exportedEntries[nextEntryIdx].originalSource = lastSourceIndex;
exportedEntries[nextEntryIdx].nameIndex = lastNameIndex;
}
nextEntryIdx++;
if (nextEntryIdx < exportedEntries.length) {
nextEntryLine = exportedEntries[nextEntryIdx].loc.start.line;
nextEntryColumn = exportedEntries[nextEntryIdx].loc.start.column;
} else {
nextEntryLine = -1;
nextEntryColumn = -1;
}
}
lastMappedLine = generatedLine2;
if (sourceIndex > -1) {
lastSourceIndex = sourceIndex;
}
if (originalLine > -1) {
lastOriginalLine = originalLine;
}
if (originalColumn > -1) {
lastOriginalColumn = originalColumn;
}
if (nameIndex > -1) {
lastNameIndex = nameIndex;
}
}
);
if (nextEntryIdx < exportedEntries.length) {
if (lastMappedLine === nextEntryLine) {
exportedEntries[nextEntryIdx].originalLine = lastOriginalLine;
exportedEntries[nextEntryIdx].originalColumn = lastOriginalColumn;
exportedEntries[nextEntryIdx].originalSource = lastSourceIndex;
exportedEntries[nextEntryIdx].nameIndex = lastNameIndex;
}
}
for (let lastIdx = mappings.length - 1; lastIdx >= 0 && mappings[lastIdx] === ";"; lastIdx--) {
lastMappedLine++;
}
sourceLineCount = program.loc.end.line;
if (sourceLineCount < lastMappedLine) {
throw new Error(
"The source map has more mappings than there are lines."
);
}
for (let extraLines = sourceLineCount - lastMappedLine; extraLines > 0; extraLines--) {
mappings += ";";
}
} else {
sourceLineCount = 1;
let idx = -1;
while ((idx = source.indexOf("\n", idx + 1)) !== -1) {
sourceLineCount++;
}
mappings = "AAAA" + ";AACA".repeat(sourceLineCount - 1);
sourceMap = new SourceMap({
version: 3,
file: basename(url),
sources: [url],
sourcesContent: [source],
names: [],
mappings,
sourceRoot: ""
});
lastSourceIndex = 0;
lastOriginalLine = sourceLineCount;
lastOriginalColumn = 0;
lastNameIndex = -1;
lastMappedLine = sourceLineCount;
for (let i = 0; i < exportedEntries.length; i++) {
const entry = exportedEntries[i];
entry.originalSource = 0;
entry.originalLine = entry.loc.start.line;
entry.originalColumn = 0;
}
}
newSrc += "\n\n;";
newSrc += 'import {registerServerReference} from "react-server-dom-esm/server";\n';
if (mappings) {
mappings += ";;";
}
const createMapping = createMappingsSerializer();
let generatedLine = 1;
createMapping(
generatedLine,
0,
lastSourceIndex,
lastOriginalLine,
lastOriginalColumn,
lastNameIndex
);
for (let i = 0; i < exportedEntries.length; i++) {
const entry = exportedEntries[i];
generatedLine++;
if (entry.type !== "function") {
newSrc += "if (typeof " + entry.localName + ' === "function") ';
}
newSrc += "registerServerReference(" + entry.localName + ",";
newSrc += JSON.stringify(url) + ",";
newSrc += JSON.stringify(entry.exportedName) + ");\n";
mappings += createMapping(
generatedLine,
0,
entry.originalSource,
entry.originalLine,
entry.originalColumn,
entry.nameIndex
);
}
}
if (sourceMap) {
sourceMap.mappings = mappings;
newSrc += "//# sourceMappingURL=data:application/json;charset=utf-8;base64," + Buffer.from(JSON.stringify(sourceMap)).toString("base64");
}
if (port) {
port.postMessage({
type: "SERVER_MODULE",
url,
source: newSrc
});
}
return newSrc;
}
function addExportNames(names, node) {
switch (node.type) {
case "Identifier":
names.push(node.name);
return;
case "ObjectPattern":
for (let i = 0; i < node.properties.length; i++)
addExportNames(names, node.properties[i]);
return;
case "ArrayPattern":
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
if (element) addExportNames(names, element);
}
return;
case "Property":
addExportNames(names, node.value);
return;
case "AssignmentPattern":
addExportNames(names, node.left);
return;
case "RestElement":
addExportNames(names, node.argument);
return;
case "ParenthesizedExpression":
addExportNames(names, node.expression);
return;
}
}
function resolveClientImport(specifier, parentURL) {
{
throw new Error(
"Expected resolve to have been called before transformSource"
);
}
}
async function parseExportNamesInto(body, names, parentURL, loader) {
for (let i = 0; i < body.length; i++) {
const node = body[i];
switch (node.type) {
case "ExportAllDeclaration":
if (node.exported) {
addExportNames(names, node.exported);
continue;
} else {
const _await$resolveClientI = await resolveClientImport(
node.source.value), url = _await$resolveClientI.url;
const _await$loader = await loader(
url,
{
format: "module",
conditions: [],
importAttributes: {}
},
loader
), source = _await$loader.source;
if (typeof source !== "string") {
throw new Error("Expected the transformed source to be a string.");
}
let childBody;
try {
childBody = acorn.parse(source, {
ecmaVersion: "2024",
sourceType: "module"
}).body;
} catch (x) {
console.error("Error parsing %s %s", url, x?.message);
continue;
}
await parseExportNamesInto(childBody, names, url, loader);
continue;
}
case "ExportDefaultDeclaration":
names.push("default");
continue;
case "ExportNamedDeclaration":
if (node.declaration) {
if (node.declaration.type === "VariableDeclaration") {
const declarations = node.declaration.declarations;
for (let j = 0; j < declarations.length; j++) {
addExportNames(names, declarations[j].id);
}
} else {
addExportNames(names, node.declaration.id);
}
}
if (node.specifiers) {
const specifiers = node.specifiers;
for (let j = 0; j < specifiers.length; j++) {
addExportNames(names, specifiers[j].exported);
}
}
continue;
}
}
}
async function transformClientModule(program, url, sourceMap, loader) {
const body = program.body;
const names = [];
await parseExportNamesInto(body, names, url, loader);
if (names.length === 0) {
console.log("[react-loader] No exports found in:", url);
return "";
}
let newSrc = 'import {registerClientReference} from "react-server-dom-esm/server";\n';
for (let i = 0; i < names.length; i++) {
const name = names[i];
const errorMessage = name === "default" ? `Attempted to call the default export of ${url} from the server but it's on the client` : `Attempted to call ${name}() from the server but ${name} is on the client`;
const fullError = `${errorMessage}. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.`;
const browserUrl = url.replace("file://", "").replace(process.cwd(), "");
if (name === "default") {
newSrc += `export default registerClientReference(function() { throw new Error(${JSON.stringify(
fullError
)}); }, ${JSON.stringify(browserUrl)}, ${JSON.stringify(name)});
`;
} else {
newSrc += `export const ${name} = registerClientReference(function() { throw new Error(${JSON.stringify(
fullError
)}); }, ${JSON.stringify(browserUrl)}, ${JSON.stringify(name)});
`;
}
}
return newSrc;
}
async function loadClientImport(url, defaultTransformSource) {
if (stashedGetSource === null) {
throw new Error(
"Expected getSource to have been called before transformSource"
);
}
const _await$stashedGetSour = await stashedGetSource(
url,
{
format: "module"
},
stashedGetSource
), source = _await$stashedGetSour.source;
const result = await defaultTransformSource(
source,
{
format: "module",
url
},
defaultTransformSource
);
return {
format: "module",
source: result.source
};
}
async function transformModuleIfNeeded(source, url, loader, port) {
if (source.indexOf("use client") === -1 && source.indexOf("use server") === -1) {
return source;
}
let program;
try {
program = acorn.parse(source, {
ecmaVersion: "2024",
sourceType: "module",
locations: true
});
} catch (x) {
console.error(
"[react-loader] Error parsing %s: %s",
url,
x?.message
);
return source;
}
let useClient = false;
let useServer = false;
for (const node of program.body) {
if (node.type !== "ExpressionStatement" || !node.directive) continue;
if (node.directive === "use client") {
useClient = true;
if (port) {
port.postMessage({
type: "CLIENT_COMPONENT",
url,
source
});
}
break;
}
if (node.directive === "use server") {
useServer = true;
break;
}
}
if (useClient) {
return transformClientModule(program, url, undefined, loader);
} else if (useServer) {
return transformServerModule(source, program, url, undefined, loader, port);
}
return source;
}
function readMappings(mappings, callback) {
let line = 1;
let column = 0;
let sourceIndex = 0;
let originalLine = 0;
let originalColumn = 0;
let nameIndex = 0;
let index = 0;
while (index < mappings.length) {
if (mappings[index] === ";") {
line++;
column = 0;
index++;
continue;
}
if (mappings[index] === ",") {
index++;
continue;
}
let [
generatedColumnDelta = 0,
sourceIndexDelta = 0,
originalLineDelta = 0,
originalColumnDelta = 0,
nameIndexDelta = 0
] = decodeVLQ(mappings.slice(index));
column += generatedColumnDelta;
sourceIndex += sourceIndexDelta;
originalLine += originalLineDelta;
originalColumn += originalColumnDelta;
nameIndex += nameIndexDelta;
while (index < mappings.length && !/[,;]/.test(mappings[index])) {
index++;
}
callback(
line,
column,
sourceIndex,
originalLine,
originalColumn,
nameIndex
);
}
}
function createMappingsSerializer() {
let previousGeneratedLine = 1;
let previousGeneratedColumn = 0;
let previousOriginalFile = 0;
let previousOriginalLine = 0;
let previousOriginalColumn = 0;
let previousNameIndex = 0;
return function(generatedLine, generatedColumn, originalFile, originalLine, originalColumn, nameIndex) {
if (generatedLine > previousGeneratedLine) {
previousGeneratedColumn = 0;
let lines = "";
for (let i = previousGeneratedLine; i < generatedLine; i++) {
lines += ";";
}
previousGeneratedLine = generatedLine;
if (lines) return lines;
}
const segment = [
generatedColumn - previousGeneratedColumn,
originalFile - previousOriginalFile,
originalLine - previousOriginalLine,
originalColumn - previousOriginalColumn
];
if (nameIndex >= 0) {
segment.push(nameIndex - previousNameIndex);
}
previousGeneratedColumn = generatedColumn;
previousOriginalFile = originalFile;
previousOriginalLine = originalLine;
previousOriginalColumn = originalColumn;
previousNameIndex = nameIndex;
return encodeVLQ(segment) + ",";
};
}
const VLQ_SHIFT = 5;
const VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT;
const VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1;
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function encodeVLQ(numbers) {
return numbers.map((num) => {
const vlq = num < 0 ? -num << 1 | 1 : num << 1;
let result = "";
let value = vlq;
do {
let digit = value & VLQ_VALUE_MASK;
value >>>= VLQ_SHIFT;
if (value > 0) {
digit |= VLQ_CONTINUATION_BIT;
}
result += BASE64_CHARS[digit];
} while (value > 0);
return result;
}).join("");
}
function decodeVLQ(str) {
const numbers = [];
let value = 0;
let shift = 0;
let index = 0;
while (index < str.length && !/[,;]/.test(str[index])) {
const digit = BASE64_CHARS.indexOf(str[index]);
if (digit === -1) break;
value += (digit & VLQ_VALUE_MASK) << shift;
if ((digit & VLQ_CONTINUATION_BIT) === 0) {
const negate = value & 1;
value >>>= 1;
numbers.push(negate ? -value : value);
value = shift = 0;
} else {
shift += VLQ_SHIFT;
}
index++;
}
return numbers;
}
async function initialize(data) {
loaderPort = data.port;
data.port.postMessage({ type: "INITIALIZED" });
data.port.unref();
}
async function resolve(specifier, context, nextResolve) {
return nextResolve(specifier, context);
}
async function load(url, context, nextLoad) {
const result = await nextLoad(url, context);
if (result.format === "module") {
const newSrc = await transformModuleIfNeeded(
result.source,
url,
nextLoad,
loaderPort ?? undefined
);
return { ...result, source: newSrc };
}
return result;
}
async function transformSource(source, context, defaultTransformSource) {
const transformed = await defaultTransformSource(
source,
context,
defaultTransformSource
);
if (context.format === "module") {
const transformedSource = transformed.source;
if (typeof transformedSource !== "string") {
throw new Error("Expected source to have been transformed to a string.");
}
const newSrc = await transformModuleIfNeeded(
transformedSource,
context.url,
(url) => loadClientImport(url, defaultTransformSource),
context.data?.port
);
return { source: newSrc };
}
return transformed;
}
export { getSource, initialize, load, resolve, transformModuleIfNeeded, transformSource };
//# sourceMappingURL=react-loader.js.map