diff --git a/package.json b/package.json index b37aa0bc0d39..492d4f52f129 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "random-seed": "^0.3.0", "react-lifecycles-compat": "^3.0.4", "rimraf": "^3.0.0", + "resolve.exports": "^2.0.0", "rollup": "^3.29.5", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-prettier": "^4.1.1", diff --git a/packages/react-server-dom-rspack/npm/client.node.unbundled.js b/packages/react-server-dom-rspack/npm/client.node.unbundled.js deleted file mode 100644 index f202f21902c2..000000000000 --- a/packages/react-server-dom-rspack/npm/client.node.unbundled.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/react-server-dom-rspack-client.node.unbundled.production.js'); -} else { - module.exports = require('./cjs/react-server-dom-rspack-client.node.unbundled.development.js'); -} diff --git a/packages/react-server-dom-rspack/npm/esm/package.json b/packages/react-server-dom-rspack/npm/esm/package.json deleted file mode 100644 index 3dbc1ca591c0..000000000000 --- a/packages/react-server-dom-rspack/npm/esm/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/packages/react-server-dom-rspack/npm/plugin.js b/packages/react-server-dom-rspack/npm/plugin.js deleted file mode 100644 index b94967ee036e..000000000000 --- a/packages/react-server-dom-rspack/npm/plugin.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -module.exports = require('./cjs/react-server-dom-rspack-plugin.js'); diff --git a/packages/react-server-dom-rspack/npm/server.node.unbundled.js b/packages/react-server-dom-rspack/npm/server.node.unbundled.js deleted file mode 100644 index e64c4ee8bf90..000000000000 --- a/packages/react-server-dom-rspack/npm/server.node.unbundled.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -var s; -if (process.env.NODE_ENV === 'production') { - s = require('./cjs/react-server-dom-rspack-server.node.unbundled.production.js'); -} else { - s = require('./cjs/react-server-dom-rspack-server.node.unbundled.development.js'); -} - -exports.renderToReadableStream = s.renderToReadableStream; -exports.renderToPipeableStream = s.renderToPipeableStream; -exports.decodeReply = s.decodeReply; -exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy; -exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; -exports.decodeAction = s.decodeAction; -exports.decodeFormState = s.decodeFormState; -exports.registerServerReference = s.registerServerReference; -exports.registerClientReference = s.registerClientReference; -exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet; - -exports.setServerActionBoundArgsEncryption = - s.setServerActionBoundArgsEncryption; -exports.encryptServerActionBoundArgs = s.encryptServerActionBoundArgs; -exports.decryptServerActionBoundArgs = s.decryptServerActionBoundArgs; -exports.loadServerAction = s.loadServerAction; -exports.createServerEntry = s.createServerEntry; -exports.ensureServerActions = s.ensureServerActions; diff --git a/packages/react-server-dom-rspack/npm/static.node.unbundled.js b/packages/react-server-dom-rspack/npm/static.node.unbundled.js deleted file mode 100644 index ae89ad7a2566..000000000000 --- a/packages/react-server-dom-rspack/npm/static.node.unbundled.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -var s; -if (process.env.NODE_ENV === 'production') { - s = require('./cjs/react-server-dom-rspack-server.node.unbundled.production.js'); -} else { - s = require('./cjs/react-server-dom-rspack-server.node.unbundled.development.js'); -} - -exports.prerenderToNodeStream = s.prerenderToNodeStream; diff --git a/packages/react-server-dom-rspack/package.json b/packages/react-server-dom-rspack/package.json index b75da8c1cdca..44f219896987 100644 --- a/packages/react-server-dom-rspack/package.json +++ b/packages/react-server-dom-rspack/package.json @@ -12,7 +12,6 @@ "LICENSE", "README.md", "index.js", - "plugin.js", "client.js", "client.browser.js", "client.edge.js", @@ -25,13 +24,10 @@ "static.browser.js", "static.edge.js", "static.node.js", - "node-register.js", - "cjs/", - "esm/" + "cjs/" ], "exports": { ".": "./index.js", - "./plugin": "./plugin.js", "./client": { "workerd": "./client.edge.js", "deno": "./client.edge.js", diff --git a/packages/react-server-dom-rspack/src/__tests__/ReactFlightDOMRspackPackage-test.js b/packages/react-server-dom-rspack/src/__tests__/ReactFlightDOMRspackPackage-test.js new file mode 100644 index 000000000000..2d244d8ec625 --- /dev/null +++ b/packages/react-server-dom-rspack/src/__tests__/ReactFlightDOMRspackPackage-test.js @@ -0,0 +1,338 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +const {mkdtempSync, mkdirSync, rmSync, symlinkSync} = require('fs'); +const {tmpdir} = require('os'); +const {join} = require('path'); +const {spawnSync} = require('child_process'); +const {exports: resolveExports} = require('resolve.exports'); + +const packageName = 'react-server-dom-rspack'; +const releaseDirectory = + process.env.RELEASE_CHANNEL === 'stable' ? 'oss-stable' : 'oss-experimental'; +const packageDirectory = join( + process.cwd(), + 'build', + releaseDirectory, + packageName, +); +let consumerDirectory; + +function getConsumerDirectory() { + if (consumerDirectory === undefined) { + consumerDirectory = mkdtempSync(join(tmpdir(), 'react-rsdr-package-')); + const nodeModulesDirectory = join(consumerDirectory, 'node_modules'); + mkdirSync(nodeModulesDirectory); + ['react', 'react-dom', packageName].forEach(dependency => { + symlinkSync( + join(packageDirectory, '..', dependency), + join(nodeModulesDirectory, dependency), + process.platform === 'win32' ? 'junction' : 'dir', + ); + }); + } + return consumerDirectory; +} + +function runNode(specifier, nodeEnv, conditions, source, expectation) { + const conditionArguments = conditions.map( + condition => `--conditions=${condition}`, + ); + const result = spawnSync( + process.execPath, + [ + '--preserve-symlinks', + ...conditionArguments, + '-e', + source, + specifier, + JSON.stringify(expectation), + ], + { + cwd: getConsumerDirectory(), + encoding: 'utf8', + env: {...process.env, NODE_ENV: nodeEnv}, + }, + ); + + if (result.status !== 0) { + throw new Error( + `Failed to load ${specifier} in ${nodeEnv}:\n${ + result.stderr || result.stdout + }`, + ); + } +} + +const requireExport = ` + global.__webpack_require__ = id => require(id); + global.__webpack_require__.u = id => String(id); + global.__webpack_chunk_load__ = () => Promise.resolve(); + global.__rspack_rsc_manifest__ = { + clientManifest: {}, + serverConsumerModuleMap: {}, + serverManifest: {}, + moduleLoading: null, + entryJsFiles: [], + entryCssFiles: {}, + }; + const exported = require(process.argv[1]); + const expectation = JSON.parse(process.argv[2]); + if (expectation.packageName !== undefined) { + if (exported.name !== expectation.packageName) { + throw new Error('Expected package metadata for ' + expectation.packageName + '.'); + } + } + for (const name of expectation.functions) { + if (typeof exported[name] !== 'function') { + throw new Error('Expected ' + process.argv[1] + ' to export function ' + name + '.'); + } + } + for (const name of expectation.missingFunctions || []) { + if (exported[name] !== undefined) { + throw new Error('Expected ' + process.argv[1] + ' not to export ' + name + '.'); + } + } + process.exit(0); +`; + +const requireUnexportedSubpath = ` + try { + require(process.argv[1]); + } catch (error) { + if (error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') { + process.exit(0); + } + throw error; + } + throw new Error('Expected the package subpath to be private.'); +`; + +const requireGuardedSubpath = ` + const expectation = JSON.parse(process.argv[2]); + try { + require(process.argv[1]); + } catch (error) { + if (error.message.includes(expectation.errorMessage)) { + process.exit(0); + } + throw error; + } + throw new Error('Expected the package subpath to require react-server.'); +`; + +function getPackedFileNames() { + const isWindows = process.platform === 'win32'; + const executable = isWindows ? process.env.ComSpec || 'cmd.exe' : 'npm'; + const args = isWindows + ? ['/d', '/s', '/c', 'npm.cmd pack --dry-run --ignore-scripts --json'] + : ['pack', '--dry-run', '--ignore-scripts', '--json']; + const result = spawnSync(executable, args, { + cwd: packageDirectory, + encoding: 'utf8', + }); + if (result.status !== 0) { + throw new Error( + `Failed to inspect the npm package:\n${result.stderr || result.stdout}`, + ); + } + return JSON.parse(result.stdout)[0].files.map(file => file.path); +} + +describe('react-server-dom-rspack npm package', () => { + afterAll(() => { + if (consumerDirectory !== undefined) { + rmSync(consumerDirectory, {recursive: true, force: true}); + } + }); + + const itBuilt = process.env.IS_BUILD ? it : it.skip; + + itBuilt( + 'loads every supported public entry point from the assembled release', + () => { + const clientEntryPoints = [ + { + specifier: `${packageName}/client`, + functions: ['createFromNodeStream', 'encodeReply'], + }, + { + specifier: `${packageName}/client.browser`, + functions: ['createFromFetch', 'encodeReply', 'setServerCallback'], + }, + { + specifier: `${packageName}/client.edge`, + functions: ['createFromFetch', 'encodeReply'], + }, + { + specifier: `${packageName}/client.node`, + functions: ['createFromNodeStream', 'encodeReply'], + }, + { + specifier: `${packageName}/package.json`, + packageName, + functions: [], + }, + ]; + const serverEntryPoints = [ + { + specifier: `${packageName}/server`, + functions: [ + 'decodeAction', + 'decodeReplyFromBusboy', + 'loadServerAction', + ], + }, + { + specifier: `${packageName}/server.browser`, + functions: ['decodeAction', 'loadServerAction'], + }, + { + specifier: `${packageName}/server.edge`, + functions: [ + 'decodeAction', + 'decodeReplyFromAsyncIterable', + 'loadServerAction', + ], + }, + { + specifier: `${packageName}/server.node`, + functions: [ + 'decodeAction', + 'decodeReplyFromBusboy', + 'loadServerAction', + ], + }, + { + specifier: `${packageName}/static`, + functions: ['prerender', 'prerenderToNodeStream'], + }, + { + specifier: `${packageName}/static.browser`, + functions: ['prerender'], + }, + { + specifier: `${packageName}/static.edge`, + functions: ['prerender'], + }, + { + specifier: `${packageName}/static.node`, + functions: ['prerender', 'prerenderToNodeStream'], + }, + ]; + + ['development', 'production'].forEach(nodeEnv => { + clientEntryPoints.forEach(entryPoint => { + runNode(entryPoint.specifier, nodeEnv, [], requireExport, entryPoint); + }); + serverEntryPoints.forEach(entryPoint => { + runNode( + entryPoint.specifier, + nodeEnv, + ['react-server'], + requireExport, + entryPoint, + ); + }); + }); + }, + ); + + itBuilt('guides package-root consumers to the client entry point', () => { + runNode(packageName, 'development', [], requireGuardedSubpath, { + errorMessage: `Use ${packageName}/client instead.`, + }); + }); + + [`${packageName}/server`, `${packageName}/static`].forEach(specifier => { + itBuilt(`guards ${specifier} outside react-server`, () => { + runNode(specifier, 'development', [], requireGuardedSubpath, { + errorMessage: 'outside a react-server environment', + }); + }); + }); + + [`${packageName}/plugin`, `${packageName}/node-register`].forEach( + specifier => { + itBuilt(`does not expose ${specifier}`, () => { + runNode( + specifier, + 'development', + ['react-server'], + requireUnexportedSubpath, + {}, + ); + }); + }, + ); + + itBuilt('excludes unsupported integration files from the npm package', () => { + const packedFileNames = getPackedFileNames(); + expect(packedFileNames).toEqual( + expect.arrayContaining([ + 'client.browser.js', + 'server.node.js', + 'static.edge.js', + 'cjs/react-server-dom-rspack-client.browser.development.js', + 'cjs/react-server-dom-rspack-server.node.production.js', + ]), + ); + [ + 'plugin.js', + 'node-register.js', + 'esm/package.json', + 'client.node.unbundled.js', + 'server.node.unbundled.js', + 'static.node.unbundled.js', + ].forEach(fileName => { + expect(packedFileNames).not.toContain(fileName); + }); + }); + + itBuilt('resolves every conditional export to its intended runtime', () => { + const packageManifest = require(join(packageDirectory, 'package.json')); + const resolve = (subpath, conditions) => + resolveExports(packageManifest, subpath, { + conditions, + unsafe: true, + }); + + expect(resolve('./client', ['workerd'])).toEqual(['./client.edge.js']); + expect(resolve('./client', ['deno'])).toEqual(['./client.edge.js']); + expect(resolve('./client', ['worker'])).toEqual(['./client.edge.js']); + expect(resolve('./client', ['node'])).toEqual(['./client.node.js']); + expect(resolve('./client', ['edge-light'])).toEqual(['./client.edge.js']); + expect(resolve('./client', ['browser'])).toEqual(['./client.browser.js']); + expect(resolve('./client', [])).toEqual(['./client.browser.js']); + + const resolveServer = (subpath, condition) => + resolve(subpath, ['react-server', condition]); + expect(resolveServer('./server', 'workerd')).toEqual(['./server.edge.js']); + expect(resolveServer('./server', 'deno')).toEqual(['./server.browser.js']); + expect(resolveServer('./server', 'node')).toEqual(['./server.node.js']); + expect(resolveServer('./server', 'edge-light')).toEqual([ + './server.edge.js', + ]); + expect(resolveServer('./server', 'browser')).toEqual([ + './server.browser.js', + ]); + + expect(resolveServer('./static', 'workerd')).toEqual(['./static.edge.js']); + expect(resolveServer('./static', 'deno')).toEqual(['./static.browser.js']); + expect(resolveServer('./static', 'node')).toEqual(['./static.node.js']); + expect(resolveServer('./static', 'edge-light')).toEqual([ + './static.edge.js', + ]); + expect(resolveServer('./static', 'browser')).toEqual([ + './static.browser.js', + ]); + }); +}); diff --git a/packages/react-server-dom-rspack/src/server/react-flight-dom-server.browser.js b/packages/react-server-dom-rspack/src/server/react-flight-dom-server.browser.js index 574827f341d6..9864e4589aaf 100644 --- a/packages/react-server-dom-rspack/src/server/react-flight-dom-server.browser.js +++ b/packages/react-server-dom-rspack/src/server/react-flight-dom-server.browser.js @@ -19,6 +19,7 @@ export { setServerActionBoundArgsEncryption, encryptServerActionBoundArgs, decryptServerActionBoundArgs, + loadServerAction, createServerEntry, ensureServerActions, } from './ReactFlightDOMServerBrowser'; diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 9efb5aec650d..096ab058b594 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -267,8 +267,6 @@ module.exports = [ 'react-server-dom-rspack/src/client/react-flight-dom-client.node', 'react-server-dom-rspack/src/server/react-flight-dom-server.node', 'react-server-dom-rspack/src/server/ReactFlightDOMServerNode.js', // react-server-dom-rspack/src/server/react-flight-dom-server.node - 'react-server-dom-rspack/node-register', - 'react-server-dom-rspack/src/ReactFlightRspackNodeRegister.js', 'react-devtools', 'react-devtools-core', 'react-devtools-shell', @@ -448,13 +446,6 @@ module.exports = [ { shortName: 'dom-browser-rspack', entryPoints: [ - 'react-dom', - 'react-dom/client', - 'react-dom/profiling', - 'react-dom/unstable_testing', - 'react-dom/src/server/react-dom-server.browser.js', - 'react-dom/static.browser', - 'react-dom/unstable_server-external-runtime', 'react-server-dom-rspack/src/client/react-flight-dom-client.browser', 'react-server-dom-rspack/src/server/react-flight-dom-server.browser', ], @@ -621,8 +612,6 @@ module.exports = [ { shortName: 'dom-edge-rspack', entryPoints: [ - 'react-dom/src/server/react-dom-server.edge.js', - 'react-dom/static.edge', 'react-server-dom-rspack/src/client/react-flight-dom-client.edge', 'react-server-dom-rspack/src/server/react-flight-dom-server.edge', ],