Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,38 @@ export type Options = {
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
// For the Node.js client we only support a single-direction debug channel.
debugChannel?: Readable,
};

function startReadingFromStream(
response: Response,
stream: Readable,
isSecondaryStream: boolean,
): void {
const streamState = createStreamState();

stream.on('data', chunk => {
if (typeof chunk === 'string') {
processStringChunk(response, streamState, chunk);
} else {
processBinaryChunk(response, streamState, chunk);
}
});

stream.on('error', error => {
reportGlobalError(response, error);
});

stream.on('end', () => {
// If we're the secondary stream, then we don't close the response until the
// debug channel closes.
if (!isSecondaryStream) {
close(response);
}
});
}

function createFromNodeStream<T>(
stream: Readable,
moduleRootPath: string,
Expand All @@ -80,18 +110,14 @@ function createFromNodeStream<T>(
? options.environmentName
: undefined,
);
const streamState = createStreamState();
stream.on('data', chunk => {
if (typeof chunk === 'string') {
processStringChunk(response, streamState, chunk);
} else {
processBinaryChunk(response, streamState, chunk);
}
});
stream.on('error', error => {
reportGlobalError(response, error);
});
stream.on('end', () => close(response));

if (__DEV__ && options && options.debugChannel) {
startReadingFromStream(response, options.debugChannel, false);
startReadingFromStream(response, stream, true);
} else {
startReadingFromStream(response, stream, false);
}

return getRoot(response);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export type Options = {
temporaryReferences?: TemporaryReferenceSet,
replayConsoleLogs?: boolean,
environmentName?: string,
// For the Edge client we only support a single-direction debug channel.
debugChannel?: {readable?: ReadableStream, ...},
};

function createResponseFromOptions(options?: Options) {
Expand All @@ -100,6 +102,7 @@ function createResponseFromOptions(options?: Options) {
function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
isSecondaryStream: boolean,
): void {
const streamState = createStreamState();
const reader = stream.getReader();
Expand All @@ -112,7 +115,11 @@ function startReadingFromStream(
...
}): void | Promise<void> {
if (done) {
close(response);
// If we're the secondary stream, then we don't close the response until
// the debug channel closes.
if (!isSecondaryStream) {
close(response);
}
return;
}
const buffer: Uint8Array = (value: any);
Expand All @@ -130,7 +137,19 @@ export function createFromReadableStream<T>(
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponseFromOptions(options);
startReadingFromStream(response, stream);

if (
__DEV__ &&
options &&
options.debugChannel &&
options.debugChannel.readable
) {
startReadingFromStream(response, options.debugChannel.readable, false);
startReadingFromStream(response, stream, true);
} else {
startReadingFromStream(response, stream, false);
}

return getRoot(response);
}

Expand All @@ -141,7 +160,17 @@ export function createFromFetch<T>(
const response: FlightResponse = createResponseFromOptions(options);
promiseForResponse.then(
function (r) {
startReadingFromStream(response, (r.body: any));
if (
__DEV__ &&
options &&
options.debugChannel &&
options.debugChannel.readable
) {
startReadingFromStream(response, options.debugChannel.readable, false);
startReadingFromStream(response, (r.body: any), true);
} else {
startReadingFromStream(response, (r.body: any), false);
}
},
function (e) {
reportGlobalError(response, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,38 @@ export type Options = {
encodeFormAction?: EncodeFormActionCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
// For the Node.js client we only support a single-direction debug channel.
debugChannel?: Readable,
};

function startReadingFromStream(
response: Response,
stream: Readable,
isSecondaryStream: boolean,
): void {
const streamState = createStreamState();

stream.on('data', chunk => {
if (typeof chunk === 'string') {
processStringChunk(response, streamState, chunk);
} else {
processBinaryChunk(response, streamState, chunk);
}
});

stream.on('error', error => {
reportGlobalError(response, error);
});

stream.on('end', () => {
// If we're the secondary stream, then we don't close the response until the
// debug channel closes.
if (!isSecondaryStream) {
close(response);
}
});
}

export function createFromNodeStream<T>(
stream: Readable,
options?: Options,
Expand All @@ -72,17 +102,13 @@ export function createFromNodeStream<T>(
? options.environmentName
: undefined,
);
const streamState = createStreamState();
stream.on('data', chunk => {
if (typeof chunk === 'string') {
processStringChunk(response, streamState, chunk);
} else {
processBinaryChunk(response, streamState, chunk);
}
});
stream.on('error', error => {
reportGlobalError(response, error);
});
stream.on('end', () => close(response));

if (__DEV__ && options && options.debugChannel) {
startReadingFromStream(response, options.debugChannel, false);
startReadingFromStream(response, stream, true);
} else {
startReadingFromStream(response, stream, false);
}

return getRoot(response);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,28 @@
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.WritableStream =
require('web-streams-polyfill/ponyfill/es6').WritableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;

// Don't wait before processing work on the server.
// TODO: we can replace this with FlightServer.act().
global.setTimeout = cb => cb();

let clientExports;
let turbopackMap;
let turbopackModules;
let React;
let ReactServer;
let ReactDOMServer;
let ReactServerDOMServer;
let ReactServerDOMClient;
let use;
let serverAct;

describe('ReactFlightTurbopackDOMEdge', () => {
beforeEach(() => {
jest.resetModules();

serverAct = require('internal-test-utils').serverAct;

// Simulate the condition resolution
jest.mock('react', () => require('react/react.react-server'));
jest.mock('react-server-dom-turbopack/server', () =>
Expand All @@ -43,6 +45,7 @@ describe('ReactFlightTurbopackDOMEdge', () => {
turbopackMap = TurbopackMock.turbopackMap;
turbopackModules = TurbopackMock.turbopackModules;

ReactServer = require('react');
ReactServerDOMServer = require('react-server-dom-turbopack/server.edge');

jest.resetModules();
Expand All @@ -66,6 +69,15 @@ describe('ReactFlightTurbopackDOMEdge', () => {
}
}

function normalizeCodeLocInfo(str) {
return (
str &&
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
})
);
}

it('should allow an alternative module mapping to be used for SSR', async () => {
function ClientComponent() {
return <span>Client Component</span>;
Expand All @@ -92,9 +104,8 @@ describe('ReactFlightTurbopackDOMEdge', () => {
return <ClientComponentOnTheClient />;
}

const stream = ReactServerDOMServer.renderToReadableStream(
<App />,
turbopackMap,
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<App />, turbopackMap),
);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
serverConsumerManifest: {
Expand All @@ -107,10 +118,98 @@ describe('ReactFlightTurbopackDOMEdge', () => {
return use(response);
}

const ssrStream = await ReactDOMServer.renderToReadableStream(
<ClientRoot />,
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(<ClientRoot />),
);
const result = await readResult(ssrStream);
expect(result).toEqual('<span>Client Component</span>');
});

// @gate __DEV__
it('can transport debug info through a separate debug channel', async () => {
function Thrower() {
throw new Error('ssr-throw');
}

const ClientComponentOnTheClient = clientExports(
Thrower,
123,
'path/to/chunk.js',
);

const ClientComponentOnTheServer = clientExports(Thrower);

function App() {
return ReactServer.createElement(
ReactServer.Suspense,
null,
ReactServer.createElement(ClientComponentOnTheClient, null),
);
}

let debugReadableStreamController;

const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});

const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
ReactServer.createElement(App, null),
turbopackMap,
{
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
}),
},
},
),
);

function ClientRoot({response}) {
return use(response);
}

const serverConsumerManifest = {
moduleMap: {
[turbopackMap[ClientComponentOnTheClient.$$id].id]: {
'*': turbopackMap[ClientComponentOnTheServer.$$id],
},
},
moduleLoading: null,
};

const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
serverConsumerManifest,
debugChannel: {readable: debugReadableStream},
});

let ownerStack;

const ssrStream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(
<ClientRoot response={response} />,
{
onError(err, errorInfo) {
ownerStack = React.captureOwnerStack
? React.captureOwnerStack()
: null;
},
},
),
);

const result = await readResult(ssrStream);

expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');

expect(result).toContain(
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
);
});
});
Loading
Loading