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 21 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;
}
6 changes: 3 additions & 3 deletions examples/with-rsc/src/components/Counter.client.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useAppContext } from 'ice';
// import { useAppContext } from 'ice';
import styles from './counter.module.css';

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

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

return (
<button className={styles.button} type="button" onClick={updateCount}>
Expand Down
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;
}
}
14 changes: 10 additions & 4 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 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>
);
}
10 changes: 5 additions & 5 deletions examples/with-rsc/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Suspense } from 'react';
import { useAppContext } from 'ice';
// import { useAppContext } from 'ice';
import styles from './index.module.css';
import EditButton from '@/components/EditButton.client';
import Counter from '@/components/Counter.client';
Expand All @@ -8,19 +8,19 @@ import Comments from '@/components/Comments';
export default function Home() {
console.log('Render: Index');

const appContext = useAppContext();
console.log(appContext);
// 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 @@ -101,10 +101,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 @@ -318,6 +318,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
21 changes: 19 additions & 2 deletions packages/ice/src/esbuild/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const ASSETS_RE = new RegExp(`\\.(${ASSET_TYPES.join('|')})(\\?.*)?$`);

interface CompilationInfo {
assetsManifest?: AssetsManifest;
rscManifest?: any;
reactClientManifest?: any;
reactSSRManifest?: any;
}

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-manifest.json$/ }, (args) => {
if (args.path === 'virtual:react-ssr-manifest.json') {
return {
path: args.path,
namespace: 'react-ssr-manifest',
};
}
});
build.onLoad({ filter: /.*/, namespace: 'react-ssr-manifest' }, () => {
const manifest = typeof compilationInfo === 'function' ? compilationInfo() : compilationInfo;
return {
contents: JSON.stringify(manifest?.reactSSRManifest || ''),
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 } from '@ice/runtime/types';

interface CompilationInfo {
assetsManifest?: AssetsManifest;
reactClientManifest?: any;
reactSSRManifest?: any;
}

// Import client component modules for ssr.
const RscLoaderPlugin = (compilationInfo: CompilationInfo | (() => CompilationInfo)) => ({
name: 'esbuild-rsc-loader',
setup(build: PluginBuild) {
build.onResolve({ filter: /react-ssr-manifest.json$/ }, (args) => {
if (args.path === 'virtual-rsc-module:react-ssr-manifest.json') {
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?.reactSSRManifest || {};

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

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

Object.keys(ssrManifest).map(router => {
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) => {
const { id } = moduleMap[moduleId]['*'];
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