Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created July 14, 2023 10:32
Show Gist options
  • Save mizchi/a60c92a080c3c228f021cef527aa10de to your computer and use it in GitHub Desktop.
Save mizchi/a60c92a080c3c228f021cef527aa10de to your computer and use it in GitHub Desktop.
Run RSC without next
{
"private": true,
"type": "module",
"scripts": {
"start": "tsm --conditions react-server run-rsc.tsx"
},
"license": "MIT",
"devDependencies": {
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"react": "0.0.0-experimental-546fe4681-20230713",
"react-dom": "0.0.0-experimental-546fe4681-20230713",
"react-server-dom-webpack": "0.0.0-experimental-546fe4681-20230713",
"tsm": "^2.3.0",
"typescript": "^5.1.6",
"webpack": "^5.86.0"
}
}
/**
* 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
*/
// from https://github.com/facebook/react/blob/main/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
import Stream from "node:stream";
import assert from 'node:assert';
import * as WebpackMock from "./webpackMock";
import React from "react";
import ReactDOMServer from 'react-dom/server.node';
import ReactServerDOMServer from 'react-server-dom-webpack/server.node';
import ReactServerDOMClient from 'react-server-dom-webpack/client.node';
const clientExports = WebpackMock.clientExports;
const webpackMap = WebpackMock.webpackMap;
const webpackModules = WebpackMock.webpackModules;
// @ts-expect-error
const use = React.use;
function ClientComponent() {
return <span>Client Component</span>;
}
// The Client build may not have the same IDs as the Server bundles for the same
// component.
const ClientComponentOnTheClient = clientExports(ClientComponent) as unknown as typeof ClientComponent;
const ClientComponentOnTheServer = clientExports(ClientComponent) as unknown as typeof ClientComponent
// In the SSR bundle this module won't exist. We simulate this by deleting it.
// @ts-expect-error;
const clientId = webpackMap[ClientComponentOnTheClient.$$id].id;
delete webpackModules[clientId];
// Instead, we have to provide a translation from the client meta data to the SSR
// meta data.
// @ts-expect-error
const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id];
const translationMap = {
[clientId]: {
'*': ssrMetadata,
},
};
function App() {
return <ClientComponentOnTheClient />;
}
const stream = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
);
const readable = new Stream.PassThrough();
const response = ReactServerDOMClient.createFromNodeStream(
readable,
translationMap,
);
stream.pipe(readable);
function ClientRoot() {
return use(response);
}
const ssrStream = await ReactDOMServer.renderToPipeableStream(
<ClientRoot />,
);
function readResult(stream: Stream.Readable) {
return new Promise((resolve, reject) => {
let buffer = '';
const writable = new Stream.PassThrough();
writable.setEncoding('utf8');
writable.on('data', chunk => {
buffer += chunk;
});
writable.on('error', error => {
reject(error);
});
writable.on('end', () => {
resolve(buffer);
});
stream.pipe(writable);
});
}
const result = await readResult(ssrStream);
assert.equal(result, '<span>Client Component</span>');
/**
* 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.
*/
// from https://github.com/facebook/react/blob/main/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js
import url from "node:url";
import Module from "node:module";
let webpackModuleIdx = 0;
const webpackServerModules = {};
const webpackClientModules = {};
const webpackErroredModules = {};
const webpackServerMap = {};
const webpackClientMap = {};
globalThis.__webpack_require__ = (id: string) => {
if (webpackErroredModules[id]) {
throw webpackErroredModules[id];
}
return webpackClientModules[id] || webpackServerModules[id];
};
// @ts-expect-error
const previousCompile = Module.prototype._compile;
import register from "react-server-dom-webpack/node-register";
register();
// @ts-expect-error
const nodeCompile = Module.prototype._compile;
if (previousCompile === nodeCompile) {
throw new Error(
'Expected the Node loader to register the _compile extension',
);
}
// @ts-expect-error
Module.prototype._compile = previousCompile;
export function clientModuleError(moduleError: any) {
const idx = '' + webpackModuleIdx++;
webpackErroredModules[idx] = moduleError;
const path = url.pathToFileURL(idx).href;
webpackClientMap[path] = {
id: idx,
chunks: [],
name: '*',
};
const mod = {exports: {}};
nodeCompile.call(mod, '"use client"', idx);
return mod.exports;
};
export function clientExports(moduleExports: any) {
const idx = '' + webpackModuleIdx++;
webpackClientModules[idx] = moduleExports;
const path = url.pathToFileURL(idx).href;
webpackClientMap[path] = {
id: idx,
chunks: [],
name: '*',
};
// We only add this if this test is testing ESM compat.
if ('__esModule' in moduleExports) {
webpackClientMap[path + '#'] = {
id: idx,
chunks: [],
name: '',
};
}
if (typeof moduleExports.then === 'function') {
moduleExports.then(
asyncModuleExports => {
for (const name in asyncModuleExports) {
webpackClientMap[path + '#' + name] = {
id: idx,
chunks: [],
name: name,
};
}
},
() => {},
);
}
if ('split' in moduleExports) {
// If we're testing module splitting, we encode this name in a separate module id.
const splitIdx = '' + webpackModuleIdx++;
webpackClientModules[splitIdx] = {
s: moduleExports.split,
};
webpackClientMap[path + '#split'] = {
id: splitIdx,
chunks: [],
name: 's',
};
}
const mod = {exports: {}};
nodeCompile.call(mod, '"use client"', idx);
return mod.exports;
};
// This tests server to server references. There's another case of client to server references.
export function serverExports(moduleExports: any) {
const idx = '' + webpackModuleIdx++;
webpackServerModules[idx] = moduleExports;
const path = url.pathToFileURL(idx).href;
webpackServerMap[path] = {
id: idx,
chunks: [],
name: '*',
};
// We only add this if this test is testing ESM compat.
if ('__esModule' in moduleExports) {
webpackServerMap[path + '#'] = {
id: idx,
chunks: [],
name: '',
};
}
if ('split' in moduleExports) {
// If we're testing module splitting, we encode this name in a separate module id.
const splitIdx = '' + webpackModuleIdx++;
webpackServerModules[splitIdx] = {
s: moduleExports.split,
};
webpackServerMap[path + '#split'] = {
id: splitIdx,
chunks: [],
name: 's',
};
}
const mod = {exports: moduleExports};
nodeCompile.call(mod, '"use server"', idx);
return mod.exports;
};
export {
webpackClientMap as webpackMap,
webpackClientModules as webpackModules,
webpackServerMap,
webpackClientMap,
webpackClientModules,
webpackServerModules
}
@mizchi
Copy link
Author

mizchi commented Jul 14, 2023

No webpackMock version

/**
 * 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
 */
// from https://github.com/facebook/react/blob/main/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js

import Stream from "node:stream";
import assert from 'node:assert';
import React from "react";
import ReactDOMServer from 'react-dom/server.node';
import ReactServerDOMServer from 'react-server-dom-webpack/server.node';
import ReactServerDOMClient from 'react-server-dom-webpack/client.node';

// WIP
type ReactServerDOM$Response = unknown;
// React Internal Types
type ReactUse = (response: Promise<ReactServerDOM$Response>) => React.ReactNode;
type ModuleChunkMap = {
  [key: string]: {
    id: string;
    chunks: Array<string>;
    name: string;
  };
}

type ReactServerDom$renderToPipeableStream = (
  node: React.ReactNode,
  moduleChunkMap: ModuleChunkMap,
) => Stream.Readable;


type ReactServerDOM$createFromNodeStream = (
  stream: Stream.Readable,
  moduleChunkMap: ModuleChunkMap,
) => Promise<ReactServerDOM$Response>;

// test helper
function readResult(stream: Stream.Readable): Promise<string> {
  return new Promise((resolve, reject) => {
    let buffer = '';
    const writable = new Stream.PassThrough();
    writable.setEncoding('utf8');
    writable.on('data', chunk => {
      buffer += chunk;
    });
    writable.on('error', error => {
      reject(error);
    });
    writable.on('end', () => {
      resolve(buffer);
    });
    stream.pipe(writable);
  });
}

// defs

function ClientComponent() {
  return <span>Client Component</span>;
}

function App() {
  return <ClientComponent />;
}

const chunks: ModuleChunkMap = {
  'file:///xxx/rsc-without-nextjs/0': { id: '0', chunks: [], name: '*' },
  'file:///xxx/rsc-without-nextjs/1': { id: '1', chunks: [], name: '*' }
};

const clientChunks: ModuleChunkMap = {
  'file:///xxx/rsc-without-nextjs/1': { id: '1', chunks: [], name: '*' }  
}

const stream = (ReactServerDOMServer.renderToPipeableStream as ReactServerDom$renderToPipeableStream)(
  <App />,
  chunks,
);
const readable = new Stream.PassThrough();
stream.pipe(readable);

const response = (ReactServerDOMClient.createFromNodeStream as ReactServerDOM$createFromNodeStream)(
  readable,
  clientChunks,
);

const use: ReactUse = (React as any).use;
function ClientRoot() {
  return use(response);
}

const ssrStream = await ReactDOMServer.renderToPipeableStream(
  <ClientRoot />,
);

const result = await readResult(ssrStream);
assert.equal(result, '<span>Client Component</span>');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment