UNPKG

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
/** * @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 };