-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
base: feat-rsc
Are you sure you want to change the base?
feat: rsc to ssr #6682
Changes from all commits
2fc0088
be029be
da8a791
55d62fc
954d7f6
f15fb7e
efb6f27
1f67237
f313534
dc2b3e1
72c1400
1e4bd31
0e93d5f
d93a196
4a70c70
6aea2da
90b10be
a55bd8a
690adb6
aeae8a8
310ee8a
77d3c3a
1ed427f
554c6f2
e85d65f
2701ccf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} | ||
|
||
|
||
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 |
---|---|---|
@@ -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')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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", | ||
|
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') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个 mapping 是有什么用吗? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 GitHub Actions / build (16.x, ubuntu-latest)
Check warning on line 34 in packages/ice/src/esbuild/rscLoader.ts GitHub Actions / build (16.x, windows-latest)
Check warning on line 34 in packages/ice/src/esbuild/rscLoader.ts GitHub Actions / build (18.x, ubuntu-latest)
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 GitHub Actions / build (16.x, ubuntu-latest)
Check warning on line 36 in packages/ice/src/esbuild/rscLoader.ts GitHub Actions / build (16.x, windows-latest)
Check warning on line 36 in packages/ice/src/esbuild/rscLoader.ts GitHub Actions / build (18.x, ubuntu-latest)
|
||
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}";`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
} | ||
}); | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这段代码不会执行到的了,可去掉