Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rsc to ssr #6682

Open
wants to merge 26 commits into
base: feat-rsc
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions examples/with-rsc/src/components/CommentsWithServerError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
async function Comments() {
const comments = import.meta.renderer === 'server' ? await getServerData() : await getClientData();

console.log('Render comments by: ', import.meta.renderer);

return (
<div>
{comments.map((comment, i) => (
<p className="comment" key={i}>
{comment}
</p>
))}
</div>
);
}

export default Comments;

const fakeData = [
"Wait, it doesn't wait for React to load?",
'How does this even work?',
'I like marshmallows',
];

async function getServerData() {
console.log('load server data');

throw new Error('server error');

await new Promise<any>((resolve) => {
setTimeout(() => resolve(null), 3000);
});

return fakeData;
Comment on lines +30 to +34
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这段代码不会执行到的了,可去掉

}


async function getClientData() {
console.log('load client data');

await new Promise<any>((resolve) => {
setTimeout(() => resolve(null), 3000);
});

return fakeData;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';
import { useState } from 'react';
import { useAppContext } from 'ice';
import styles from './counter.module.css';

export default function Counter() {
Expand All @@ -10,12 +9,9 @@ export default function Counter() {
setCount(count + 1);
}

const appContext = useAppContext();
console.log(appContext);

return (
<button className={styles.button} type="button" onClick={updateCount}>
👍🏻 {count}
</button>
);
}
}
37 changes: 37 additions & 0 deletions examples/with-rsc/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';
import { Component, lazy, Suspense } from 'react';
import type { ReactNode } from 'react';

type EProps = {
children: ReactNode;
};

type EState = {
hasError: boolean;
};

export default class ErrorBoundary extends Component<EProps, EState> {
state: EState = {
hasError: false,
};

static getDerivedStateFromError() {
return { hasError: true };
}

render() {
if (this.state.hasError) {
// @ts-ignore
const ClientComments = lazy(() => import('./CommentsWithServerError'));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClientComments -> CommentsWithServerError


return (
<Suspense fallback="loading client comments">
<h3>Client Comments</h3>
<ClientComments />
</Suspense>
);
}

return this.props.children;
}
}
16 changes: 11 additions & 5 deletions examples/with-rsc/src/pages/about.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useAppContext } from 'ice';
import { Suspense } from 'react';
import styles from './about.module.css';
import Counter from '@/components/Counter.client';
import Counter from '@/components/Counter';
import CommentsWithServerError from '@/components/CommentsWithServerError';
import ErrorBoundary from '@/components/ErrorBoundary';

if (!global.requestCount) {
global.requestCount = 0;
Expand All @@ -9,14 +11,18 @@ if (!global.requestCount) {
export default function Home() {
console.log('Render: Index');

const appContext = useAppContext();
console.log(appContext);

return (
<div className={styles.about}>
<h2>About Page</h2>
<div>server request count: { global.requestCount++ }</div>
<Counter />
<h3>Comments Wtih Server Error</h3>
<ErrorBoundary>
<Suspense fallback={<>loading server comments</>}>
{/* @ts-ignore */}
<CommentsWithServerError />
</Suspense>
</ErrorBoundary>
</div>
);
}
12 changes: 4 additions & 8 deletions examples/with-rsc/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import { Suspense } from 'react';
import { useAppContext } from 'ice';
import styles from './index.module.css';
import EditButton from '@/components/EditButton.client';
import Counter from '@/components/Counter.client';
import EditButton from '@/components/EditButton';
import Counter from '@/components/Counter';
import Comments from '@/components/Comments';

export default function Home() {
console.log('Render: Index');

const appContext = useAppContext();
console.log(appContext);

return (
<div className={styles.app}>
<h2>Home Page</h2>
<Counter />
<Suspense fallback={<>loading</>}>
<Suspense fallback="loading">
{/* @ts-ignore */}
<Comments />
</Suspense>
<EditButton noteId="editButton">
hello world
click me
</EditButton>
</div>
);
Expand Down
10 changes: 5 additions & 5 deletions packages/bundles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@ice/swc-plugin-remove-export": "0.2.0",
"@ice/swc-plugin-keep-export": "0.2.0",
"@ice/swc-plugin-node-transform": "0.2.0",
"@ice/swc-plugin-react-server-component": "0.1.1",
"@ice/swc-plugin-react-server-component": "0.1.2",
"ansi-html-community": "^0.0.8",
"html-entities": "^2.3.2",
"core-js": "3.32.0",
Expand Down Expand Up @@ -114,10 +114,10 @@
"source-map": "0.8.0-beta.0",
"find-up": "5.0.0",
"common-path-prefix": "3.0.0",
"react-builtin": "npm:[email protected]dd480ef92-20230822",
"react-dom-builtin": "npm:[email protected]dd480ef92-20230822",
"react-server-dom-webpack": "18.3.0-canary-dd480ef92-20230822",
"scheduler-builtin": "npm:[email protected]dd480ef92-20230822"
"react-builtin": "npm:[email protected]2c338b16f-20231116",
"react-dom-builtin": "npm:[email protected]2c338b16f-20231116",
"react-server-dom-webpack": "18.3.0-canary-2c338b16f-20231116",
"scheduler-builtin": "npm:[email protected]2c338b16f-20231116"
},
"publishConfig": {
"access": "public",
Expand Down
16 changes: 16 additions & 0 deletions packages/bundles/scripts/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,22 @@ handler: (source) => {
json.exports['./plugin.js'] = './plugin.js';
return JSON.stringify(json, null, 2);
} },
{
test: /react-server-dom-webpack-server.edge.(development|production).js$/,
handler: (source) => {
return source
.replace('require(\'react\')', 'require(\'@ice/bundles/compiled/react/react.shared-subset.js\')')
.replace('require(\'react-dom\')', 'require(\'@ice/bundles/compiled/react-dom/server-rendering-stub.js\')');
},
},
{
test: /react-server-dom-webpack-server.edge.production.min.js$/,
handler: (source) => {
return source
.replace('require("react")', 'require("@ice/bundles/compiled/react/react.shared-subset.js")')
.replace('require("react-dom")', 'require("@ice/bundles/compiled/react-dom/server-rendering-stub.js")');
},
},
],
},
);
Expand Down
23 changes: 20 additions & 3 deletions packages/ice/src/esbuild/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as path from 'path';
import * as mrmime from 'mrmime';
import fs from 'fs-extra';
import type { PluginBuild } from 'esbuild';
import type { AssetsManifest } from '@ice/runtime/types';
import type { AssetsManifest, ClientManifest, SSRModuleMapping } from '@ice/runtime/types';

export const ASSET_TYPES = [
// images
Expand Down Expand Up @@ -40,7 +40,8 @@ const ASSETS_RE = new RegExp(`\\.(${ASSET_TYPES.join('|')})(\\?.*)?$`);

interface CompilationInfo {
assetsManifest?: AssetsManifest;
rscManifest?: any;
reactClientManifest?: ClientManifest;
reactSSRModuleMapping?: SSRModuleMapping;
}

const createAssetsPlugin = (compilationInfo: CompilationInfo | (() => CompilationInfo), rootDir: string) => ({
Expand Down Expand Up @@ -72,10 +73,26 @@ const createAssetsPlugin = (compilationInfo: CompilationInfo | (() => Compilatio
build.onLoad({ filter: /.*/, namespace: 'react-client-manifest' }, () => {
const manifest = typeof compilationInfo === 'function' ? compilationInfo() : compilationInfo;
return {
contents: JSON.stringify(manifest?.rscManifest || ''),
contents: JSON.stringify(manifest?.reactClientManifest || ''),
loader: 'json',
};
});
build.onResolve({ filter: /react-ssr-module-mapping.json$/ }, (args) => {
if (args.path === 'virtual:react-ssr-module-mapping.json') {
return {
path: args.path,
namespace: 'react-ssr-module-mapping',
};
}
});
build.onLoad({ filter: /.*/, namespace: 'react-ssr-module-mapping' }, () => {
const manifest = typeof compilationInfo === 'function' ? compilationInfo() : compilationInfo;
return {
contents: JSON.stringify(manifest?.reactSSRModuleMapping || ''),
loader: 'json',
};
});

build.onLoad({ filter: ASSETS_RE }, async (args) => {
const manifest = typeof compilationInfo === 'function' ? compilationInfo() : compilationInfo;
if (args.suffix == '?raw') {
Expand Down
64 changes: 64 additions & 0 deletions packages/ice/src/esbuild/rscLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { PluginBuild } from 'esbuild';
import type { AssetsManifest, ClientManifest, SSRModuleMapping } from '@ice/runtime/types';

interface CompilationInfo {
assetsManifest?: AssetsManifest;
reactClientManifest?: ClientManifest;
reactSSRModuleMapping?: SSRModuleMapping;
}

// Import client component modules for ssr.
const RscLoaderPlugin = (compilationInfo: CompilationInfo | (() => CompilationInfo)) => ({
name: 'esbuild-rsc-loader',
setup(build: PluginBuild) {
build.onResolve({ filter: /react-ssr-module-mapping.json$/ }, (args) => {
if (args.path === 'virtual-rsc-module:react-ssr-module-mapping.json') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 mapping 是有什么用吗?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个是 Server 端渲染 RSC 结果到 SSR 时,需要用到 Client Components 的 mapping。会根据这份产物加载对应的 Client Component 的 Server 端 Bundle,然后渲染

return {
path: args.path,
namespace: 'virtual-rsc-module',
};
}
});

build.onLoad({ filter: /.*/, namespace: 'virtual-rsc-module' }, () => {
const manifest = typeof compilationInfo === 'function' ? compilationInfo() : compilationInfo;
const ssrManifest = manifest?.reactSSRModuleMapping || {};

const imports: string[] = [];
const maps: string[] = [];
const modules = {};
let index = 0;

const CSSRegex = /\.(css|sass|scss)$/;

Object.keys(ssrManifest).map(router => {

Check warning on line 34 in packages/ice/src/esbuild/rscLoader.ts

View workflow job for this annotation

GitHub Actions / build (16.x, ubuntu-latest)

Array.prototype.map() expects a return value from arrow function

Check warning on line 34 in packages/ice/src/esbuild/rscLoader.ts

View workflow job for this annotation

GitHub Actions / build (16.x, windows-latest)

Array.prototype.map() expects a return value from arrow function

Check warning on line 34 in packages/ice/src/esbuild/rscLoader.ts

View workflow job for this annotation

GitHub Actions / build (18.x, ubuntu-latest)

Array.prototype.map() expects a return value from arrow function

Check warning on line 34 in packages/ice/src/esbuild/rscLoader.ts

View workflow job for this annotation

GitHub Actions / build (18.x, windows-latest)

Array.prototype.map() expects a return value from arrow function
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有一些 lint 的问题

const moduleMap = ssrManifest[router];
Object.keys(moduleMap).map((moduleId) => {

Check warning on line 36 in packages/ice/src/esbuild/rscLoader.ts

View workflow job for this annotation

GitHub Actions / build (16.x, ubuntu-latest)

Array.prototype.map() expects a value to be returned at the end of arrow function

Check warning on line 36 in packages/ice/src/esbuild/rscLoader.ts

View workflow job for this annotation

GitHub Actions / build (16.x, windows-latest)

Array.prototype.map() expects a value to be returned at the end of arrow function

Check warning on line 36 in packages/ice/src/esbuild/rscLoader.ts

View workflow job for this annotation

GitHub Actions / build (18.x, ubuntu-latest)

Array.prototype.map() expects a value to be returned at the end of arrow function

Check warning on line 36 in packages/ice/src/esbuild/rscLoader.ts

View workflow job for this annotation

GitHub Actions / build (18.x, windows-latest)

Array.prototype.map() expects a value to be returned at the end of arrow function
const id = moduleMap[moduleId]['*'].id as string;
if (modules[id] || CSSRegex.test(id)) return;
modules[id] = true;
index++;
imports.push(`import * as component_${index} from "(rsc)${id}";`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(rsc) 感觉可以换成 rsc:,然后注释下后面会替换掉

maps.push(`"${id}": component_${index}`);
});
});

const contents = `
${imports.join('\n')};

const clientModules = {
${maps.join(',\n')}
}

export default clientModules;
`;

return {
contents: contents,
loader: 'tsx',
};
});
},
});

export default RscLoaderPlugin;
11 changes: 8 additions & 3 deletions packages/ice/src/esbuild/transformPipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,25 @@ const transformPipe = (options: PluginOptions = {}): Plugin => {
});
if (pluginResolveIds.length > 0) {
build.onResolve({ filter }, async (args) => {
let redirected = false;
const isEntry = args.kind === 'entry-point';
const res = await pluginResolveIds.reduce(async (resolveData, resolveId) => {
const { path, external } = await resolveData;
if (!external) {
const result = await resolveId(path, isEntry ? undefined : args.importer, { isEntry });
if (typeof result === 'string') {
redirected = true;
return { path: result };
} else if (typeof result === 'object' && result !== null) {
return { path: result.id, external: result.external };
redirected = true;
return { path: result.id, external: result.external, namespace: result.namespace };
}
}
return resolveData;
}, Promise.resolve({ path: args.path }));
if (path.isAbsolute(res.path) || res.external) {

// For path not changed, should return null, otherwise it will breack other path resolution.
if (redirected && (path.isAbsolute(res.path) || res.external)) {
return res;
}
});
Expand All @@ -112,7 +117,7 @@ const transformPipe = (options: PluginOptions = {}): Plugin => {
let sourceMap = null;

if (plugin.load && (!loadInclude || loadInclude?.(id))) {
const result = await plugin.load.call(pluginContext, id);
const result = await plugin.load.call(pluginContext, id, args.namespace);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

希望在插件的 load 中只处理 resolved 的 path,所以透传了 namespace 进行标记,看看是否有更好的方式。

if (typeof result === 'string') {
sourceCode = result;
} else if (typeof result === 'object' && result !== null) {
Expand Down
29 changes: 29 additions & 0 deletions packages/ice/src/esbuild/transfromRSCDirective.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import fse from 'fs-extra';

// Remove `use client` directive for Client Component
// to rendering rsc result by ssr.
const transformRscDirective = () => {
return {
name: 'transform-rsc-directive',
resolveId(id) {
if (id.indexOf('(rsc)') > -1) {
const newId = id.replace('(rsc)', '');
return {
id: newId,
namespace: 'rsc',
};
}
},
async load(id, namespace) {
if (namespace === 'rsc') {
let source = await fse.readFile(id, 'utf-8');
if (source.indexOf("'use client';") === 0) {
const code = source.replace("'use client';", '');
return code;
}
}
},
};
};

export default transformRscDirective;
Loading
Loading