UNPKG

vite-plugin-react-server

Version:
648 lines (645 loc) 20 kB
/** * 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