react-server-dom-webpack
Version:
React Server Components bindings for DOM using Webpack. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.
425 lines (337 loc) • 12.5 kB
JavaScript
/**
* @license React
* react-server-dom-webpack-node-loader.production.min.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as acorn from 'acorn-loose';
const assign = Object.assign;
let warnedAboutConditionsFlag = false;
let stashedGetSource = null;
let stashedResolve = null;
async function resolve(specifier, context, defaultResolve) {
// We stash this in case we end up needing to resolve export * statements later.
stashedResolve = defaultResolve;
if (!context.conditions.includes('react-server')) {
context = assign({}, context, {
conditions: [].concat(context.conditions, ['react-server'])
});
if (!warnedAboutConditionsFlag) {
warnedAboutConditionsFlag = true; // eslint-disable-next-line react-internal/no-production-logging
console.warn('You did not run Node.js with the `--conditions react-server` flag. ' + 'Any "react-server" override will only work with ESM imports.');
}
}
return await defaultResolve(specifier, context, defaultResolve);
}
async function getSource(url, context, defaultGetSource) {
// We stash this in case we end up needing to resolve export * statements later.
stashedGetSource = defaultGetSource;
return defaultGetSource(url, context, defaultGetSource);
}
function addLocalExportedNames(names, node) {
switch (node.type) {
case 'Identifier':
names.set(node.name, node.name);
return;
case 'ObjectPattern':
for (let i = 0; i < node.properties.length; i++) addLocalExportedNames(names, node.properties[i]);
return;
case 'ArrayPattern':
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
if (element) addLocalExportedNames(names, element);
}
return;
case 'Property':
addLocalExportedNames(names, node.value);
return;
case 'AssignmentPattern':
addLocalExportedNames(names, node.left);
return;
case 'RestElement':
addLocalExportedNames(names, node.argument);
return;
case 'ParenthesizedExpression':
addLocalExportedNames(names, node.expression);
return;
}
}
function transformServerModule(source, body, url, loader) {
// If the same local name is exported more than once, we only need one of the names.
const localNames = new Map();
const localTypes = new Map();
for (let i = 0; i < body.length; i++) {
const node = body[i];
switch (node.type) {
case 'ExportAllDeclaration':
// If export * is used, the other file needs to explicitly opt into "use server" too.
break;
case 'ExportDefaultDeclaration':
if (node.declaration.type === 'Identifier') {
localNames.set(node.declaration.name, 'default');
} else if (node.declaration.type === 'FunctionDeclaration') {
if (node.declaration.id) {
localNames.set(node.declaration.id.name, 'default');
localTypes.set(node.declaration.id.name, 'function');
}
}
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(localNames, declarations[j].id);
}
} else {
const name = node.declaration.id.name;
localNames.set(name, name);
if (node.declaration.type === 'FunctionDeclaration') {
localTypes.set(name, 'function');
}
}
}
if (node.specifiers) {
const specifiers = node.specifiers;
for (let j = 0; j < specifiers.length; j++) {
const specifier = specifiers[j];
localNames.set(specifier.local.name, specifier.exported.name);
}
}
continue;
}
}
if (localNames.size === 0) {
return source;
}
let newSrc = source + '\n\n;';
newSrc += 'import {registerServerReference} from "react-server-dom-webpack/server";\n';
localNames.forEach(function (exported, local) {
if (localTypes.get(local) !== 'function') {
// We first check if the export is a function and if so annotate it.
newSrc += 'if (typeof ' + local + ' === "function") ';
}
newSrc += 'registerServerReference(' + local + ',';
newSrc += JSON.stringify(url) + ',';
newSrc += JSON.stringify(exported) + ');\n';
});
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) {
// Resolve an import specifier as if it was loaded by the client. This doesn't use
// the overrides that this loader does but instead reverts to the default.
// This resolution algorithm will not necessarily have the same configuration
// as the actual client loader. It should mostly work and if it doesn't you can
// always convert to explicit exported names instead.
const conditions = ['node', 'import'];
if (stashedResolve === null) {
throw new Error('Expected resolve to have been called before transformSource');
}
return stashedResolve(specifier, {
conditions,
parentURL
}, stashedResolve);
}
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, parentURL),
url = _await$resolveClientI.url;
const _await$loader = await loader(url, {
format: 'module',
conditions: [],
importAssertions: {}
}, 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) {
// eslint-disable-next-line react-internal/no-production-logging
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(body, url, loader) {
const names = [];
await parseExportNamesInto(body, names, url, loader);
if (names.length === 0) {
return '';
}
let newSrc = 'import {registerClientReference} from "react-server-dom-webpack/server";\n';
for (let i = 0; i < names.length; i++) {
const name = names[i];
if (name === 'default') {
newSrc += 'export default ';
newSrc += 'registerClientReference(function() {';
newSrc += 'throw new Error(' + JSON.stringify("Attempted to call the default export of " + url + " from the server" + "but it's on the client. 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.") + ');';
} else {
newSrc += 'export const ' + name + ' = ';
newSrc += 'registerClientReference(function() {';
newSrc += 'throw new Error(' + JSON.stringify("Attempted to call " + name + "() from the server but " + name + " is on the client. " + "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.") + ');';
}
newSrc += '},';
newSrc += JSON.stringify(url) + ',';
newSrc += JSON.stringify(name) + ');\n';
}
return newSrc;
}
async function loadClientImport(url, defaultTransformSource) {
if (stashedGetSource === null) {
throw new Error('Expected getSource to have been called before transformSource');
} // TODO: Validate that this is another module by calling getFormat.
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) {
// Do a quick check for the exact string. If it doesn't exist, don't
// bother parsing.
if (source.indexOf('use client') === -1 && source.indexOf('use server') === -1) {
return source;
}
let body;
try {
body = acorn.parse(source, {
ecmaVersion: '2024',
sourceType: 'module'
}).body;
} catch (x) {
// eslint-disable-next-line react-internal/no-production-logging
console.error('Error parsing %s %s', url, x.message);
return source;
}
let useClient = false;
let useServer = false;
for (let i = 0; i < body.length; i++) {
const node = body[i];
if (node.type !== 'ExpressionStatement' || !node.directive) {
break;
}
if (node.directive === 'use client') {
useClient = true;
}
if (node.directive === 'use server') {
useServer = true;
}
}
if (!useClient && !useServer) {
return source;
}
if (useClient && useServer) {
throw new Error('Cannot have both "use client" and "use server" directives in the same file.');
}
if (useClient) {
return transformClientModule(body, url, loader);
}
return transformServerModule(source, body, url);
}
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, ctx, defaultLoad) => {
return loadClientImport(url, defaultTransformSource);
});
return {
source: newSrc
};
}
return transformed;
}
async function load(url, context, defaultLoad) {
const result = await defaultLoad(url, context, defaultLoad);
if (result.format === 'module') {
if (typeof result.source !== 'string') {
throw new Error('Expected source to have been loaded into a string.');
}
const newSrc = await transformModuleIfNeeded(result.source, url, defaultLoad);
return {
format: 'module',
source: newSrc
};
}
return result;
}
export { getSource, load, resolve, transformSource };