Skip to content

Commit

Permalink
feat: support stream to string & support server ender styled componen…
Browse files Browse the repository at this point in the history
…ts (#5769)
  • Loading branch information
2heal1 authored May 24, 2024
1 parent 7834d4b commit 08d9466
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 47 deletions.
6 changes: 6 additions & 0 deletions .changeset/good-zoos-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modern-js/runtime': patch
---

feat: support stream to string & support server ender styled components
feat: 支持 stream 模式转 string ,并且支持服务端渲染 styled compoents
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export async function buildShellBeforeTemplate(
beforeAppTemplate: string,
context: RuntimeContext,
pluginConfig: SSRPluginConfig,
styledComponentsStyleTags?: string,
) {
const helmetData: HelmetData = ReactHelmet.renderStatic();
const callbacks: BuildTemplateCb[] = [
Expand All @@ -48,14 +49,19 @@ export async function buildShellBeforeTemplate(
: headTemplate;
},
// @TODO: prefetch scripts of lazy component
injectCss,
template => injectCss(template, styledComponentsStyleTags),
];

return buildTemplate(beforeAppTemplate, callbacks);

async function injectCss(template: string) {
const css = await getCssChunks();

async function injectCss(
template: string,
styledComponentsStyleTags?: string,
) {
let css = await getCssChunks();
if (styledComponentsStyleTags) {
css += styledComponentsStyleTags;
}
return safeReplace(template, CHUNK_CSS_PLACEHOLDER, css);

async function getCssChunks() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Transform, Readable } from 'stream';
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
import { RenderLevel, RuntimeContext, SSRPluginConfig } from '../types';
import { ESCAPED_SHELL_STREAM_END_MARK } from '../../../common';
import { getTemplates } from './template';
Expand All @@ -17,9 +18,13 @@ function renderToPipe(
) {
let shellChunkStatus = ShellChunkStatus.START;

const forceStream2String = Boolean(process.env.MODERN_JS_STREAM_TO_STRING);
// When a crawler visit the page, we should waiting for entrie content of page
const onReady = context.ssrContext?.isSpider ? 'onAllReady' : 'onShellReady';

const onReady =
context.ssrContext?.isSpider || forceStream2String
? 'onAllReady'
: 'onShellReady';
const sheet = new ServerStyleSheet();
const { ssrContext } = context;
const chunkVec: string[] = [];
const forUserPipe = new Promise<string | Readable>(resolve => {
Expand All @@ -28,54 +33,62 @@ function renderToPipe(
// eslint-disable-next-line @typescript-eslint/no-require-imports
({ renderToPipeableStream } = require('react-dom/server'));
} catch (e) {}
const root = forceStream2String
? sheet.collectStyles(rootElement)
: rootElement;

const { pipe } = renderToPipeableStream(rootElement, {
const { pipe } = renderToPipeableStream(root, {
...options,
nonce: ssrContext?.nonce,
[onReady]() {
getTemplates(context, RenderLevel.SERVER_RENDER, pluginConfig).then(
({ shellAfter, shellBefore }) => {
options?.onShellReady?.();
const injectableTransform = new Transform({
transform(chunk, _encoding, callback) {
try {
if (shellChunkStatus !== ShellChunkStatus.FINIESH) {
chunkVec.push(chunk.toString());
const styledComponentsStyleTags = forceStream2String
? sheet.getStyleTags()
: '';
getTemplates(
context,
RenderLevel.SERVER_RENDER,
pluginConfig,
styledComponentsStyleTags,
).then(({ shellAfter, shellBefore }) => {
options?.onShellReady?.();
const injectableTransform = new Transform({
transform(chunk, _encoding, callback) {
try {
if (shellChunkStatus !== ShellChunkStatus.FINIESH) {
chunkVec.push(chunk.toString());
/**
* The shell content of App may be splitted by multiple chunks to transform,
* when any node value's size is larger than the React limitation, refer to:
* https://github.com/facebook/react/blob/v18.2.0/packages/react-server/src/ReactServerStreamConfigNode.js#L53.
* So we use the `SHELL_STREAM_END_MARK` to mark the shell content' tail.
*/
let concatedChunk = chunkVec.join('');
if (concatedChunk.endsWith(ESCAPED_SHELL_STREAM_END_MARK)) {
concatedChunk = concatedChunk.replace(
ESCAPED_SHELL_STREAM_END_MARK,
'',
);

/**
* The shell content of App may be splitted by multiple chunks to transform,
* when any node value's size is larger than the React limitation, refer to:
* https://github.com/facebook/react/blob/v18.2.0/packages/react-server/src/ReactServerStreamConfigNode.js#L53.
* So we use the `SHELL_STREAM_END_MARK` to mark the shell content' tail.
*/
let concatedChunk = chunkVec.join('');
if (concatedChunk.endsWith(ESCAPED_SHELL_STREAM_END_MARK)) {
concatedChunk = concatedChunk.replace(
ESCAPED_SHELL_STREAM_END_MARK,
'',
);

shellChunkStatus = ShellChunkStatus.FINIESH;
this.push(`${shellBefore}${concatedChunk}${shellAfter}`);
}
} else {
this.push(chunk);
}
callback();
} catch (e) {
if (e instanceof Error) {
callback(e);
} else {
callback(new Error('Received unkown error when streaming'));
shellChunkStatus = ShellChunkStatus.FINIESH;
this.push(`${shellBefore}${concatedChunk}${shellAfter}`);
}
} else {
this.push(chunk);
}
},
});
callback();
} catch (e) {
if (e instanceof Error) {
callback(e);
} else {
callback(new Error('Received unkown error when streaming'));
}
}
},
});

pipe(injectableTransform);
resolve(injectableTransform);
},
);
pipe(injectableTransform);
resolve(injectableTransform);
});
},
onShellError(error: unknown) {
// eslint-disable-next-line promise/no-promise-in-callback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const getTemplates = async (
context: RuntimeContext,
renderLevel: RenderLevel,
pluginConfig: SSRPluginConfig,
styledComponentsStyleTags?: string,
): Promise<InjectTemplate> => {
const { ssrContext } = context;
const [beforeAppTemplate = '', afterAppHtmlTemplate = ''] =
Expand All @@ -22,6 +23,7 @@ export const getTemplates = async (
beforeAppTemplate,
context,
pluginConfig,
styledComponentsStyleTags,
);
const builtAfterTemplate = await buildShellAfterTemplate(
afterAppHtmlTemplate,
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 08d9466

Please sign in to comment.