diff --git a/examples/with-rsc/src/components/Container.tsx b/examples/with-rsc/src/components/Container.tsx
new file mode 100644
index 0000000000..842adceeaf
--- /dev/null
+++ b/examples/with-rsc/src/components/Container.tsx
@@ -0,0 +1,23 @@
+'use server';
+
+import EditButton from '@/components/EditButton';
+import Counter from '@/components/Counter';
+import InnerServer from '@/components/Content';
+
+export default function Container() {
+ return (
+ <>
+
+
+
+
+ hello world
+
+
{serverPrint('serverPrint call')}
+ >
+ );
+}
+
+export function serverPrint(sentence) {
+ return sentence;
+}
\ No newline at end of file
diff --git a/examples/with-rsc/src/components/Content.tsx b/examples/with-rsc/src/components/Content.tsx
new file mode 100644
index 0000000000..8cf1e5c400
--- /dev/null
+++ b/examples/with-rsc/src/components/Content.tsx
@@ -0,0 +1,9 @@
+'use server';
+
+export default function InnerServer() {
+ return (
+
+ inner server
+
+ );
+}
\ No newline at end of file
diff --git a/examples/with-rsc/src/components/Counter.client.tsx b/examples/with-rsc/src/components/Counter.tsx
similarity index 72%
rename from examples/with-rsc/src/components/Counter.client.tsx
rename to examples/with-rsc/src/components/Counter.tsx
index c61fbaa994..3ecb96a67e 100644
--- a/examples/with-rsc/src/components/Counter.client.tsx
+++ b/examples/with-rsc/src/components/Counter.tsx
@@ -2,8 +2,9 @@
import { useState } from 'react';
import { useAppContext } from 'ice';
import styles from './index.module.css';
+import { clientPrint } from './EditButton';
-export default function Counter() {
+export default function Counter({ children }) {
const [count, setCount] = useState(0);
function updateCount() {
@@ -16,6 +17,8 @@ export default function Counter() {
return (
);
}
\ No newline at end of file
diff --git a/examples/with-rsc/src/components/EditButton.client.tsx b/examples/with-rsc/src/components/EditButton.tsx
similarity index 90%
rename from examples/with-rsc/src/components/EditButton.client.tsx
rename to examples/with-rsc/src/components/EditButton.tsx
index 4a9fd16add..559c29b2c8 100644
--- a/examples/with-rsc/src/components/EditButton.client.tsx
+++ b/examples/with-rsc/src/components/EditButton.tsx
@@ -22,3 +22,7 @@ export default function EditButton({ noteId, children }) {
);
}
+
+export function clientPrint(sentence) {
+ return sentence;
+}
\ No newline at end of file
diff --git a/examples/with-rsc/src/components/RefreshButton.client.tsx b/examples/with-rsc/src/components/RefreshButton.client.tsx
new file mode 100644
index 0000000000..643ec10bbf
--- /dev/null
+++ b/examples/with-rsc/src/components/RefreshButton.client.tsx
@@ -0,0 +1,19 @@
+'use client';
+import { useRefresh } from '@ice/runtime';
+
+export default function Button({ children }) {
+ const refresh = useRefresh();
+
+ return (
+
+ );
+}
diff --git a/examples/with-rsc/src/pages/about.tsx b/examples/with-rsc/src/pages/about.tsx
new file mode 100644
index 0000000000..b9a1859c37
--- /dev/null
+++ b/examples/with-rsc/src/pages/about.tsx
@@ -0,0 +1,26 @@
+import { useAppContext } from 'ice';
+import styles from './index.module.css';
+import RefreshButton from '@/components/RefreshButton.client';
+import Counter from '@/components/Counter.client';
+
+if (!global.requestCount) {
+ global.requestCount = 0;
+}
+
+export default function Home() {
+ console.log('Render: Index');
+
+ const appContext = useAppContext();
+ console.log(appContext);
+
+ return (
+
+
About Page
+
server request count: { global.requestCount++ }
+
+
+ Refresh Button
+
+
+ );
+}
diff --git a/examples/with-rsc/src/pages/index.tsx b/examples/with-rsc/src/pages/index.tsx
index 0305d82e97..56f3a70450 100644
--- a/examples/with-rsc/src/pages/index.tsx
+++ b/examples/with-rsc/src/pages/index.tsx
@@ -1,7 +1,6 @@
import { useAppContext } from 'ice';
import styles from './index.module.css';
-import EditButton from '@/components/EditButton.client';
-import Counter from '@/components/Counter.client';
+import Container from '@/components/Container';
export default function Home() {
console.log('Render: Index');
@@ -12,10 +11,7 @@ export default function Home() {
return (
Home Page
-
-
- hello world
-
+
);
}
diff --git a/packages/ice/package.json b/packages/ice/package.json
index b876a13868..bbedfac648 100644
--- a/packages/ice/package.json
+++ b/packages/ice/package.json
@@ -46,6 +46,9 @@
"@swc/helpers": "0.5.1",
"@types/express": "^4.17.14",
"address": "^1.1.2",
+ "acorn": "^8.10.0",
+ "acorn-jsx": "^5.3.2",
+ "recast": "^0.23.4",
"build-scripts": "^2.1.2-0",
"chalk": "^4.0.0",
"commander": "^9.0.0",
diff --git a/packages/ice/src/esbuild/rscServerRegister.ts b/packages/ice/src/esbuild/rscServerRegister.ts
index ff90045b6d..39e41c4b05 100644
--- a/packages/ice/src/esbuild/rscServerRegister.ts
+++ b/packages/ice/src/esbuild/rscServerRegister.ts
@@ -1,16 +1,128 @@
+import fs from 'fs';
import url from 'url';
import type { Plugin, PluginBuild } from 'esbuild';
+import { Parser } from 'acorn';
+import jsx from 'acorn-jsx';
+import recast from 'recast';
+import ts from 'typescript';
+
+export function rscCodeTransform({ content, path }: { content: string; path: string }): string {
+ if (content.indexOf('use client') === -1 && content.indexOf('use server') === -1) {
+ return content;
+ }
+
+ // transform tsx/ts code to es6 jsx code
+ if (path.endsWith('tsx') || path.endsWith('ts')) {
+ const result = ts.transpileModule(content, {
+ compilerOptions: {
+ target: ts.ScriptTarget.ES2022,
+ module: ts.ModuleKind.ES2022,
+ jsx: ts.JsxEmit.Preserve,
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
+ },
+ });
+ content = result.outputText;
+ }
+
+ let body;
+ try {
+ // get AST of source code.
+ body = (Parser.extend(jsx()).parse(content, {
+ ecmaVersion: 2024,
+ sourceType: 'module',
+ }) as any).body;
+ } catch (x) {
+ console.error('Error parsing %s %s %s', url, x.message, path);
+ return content;
+ }
+
+ let useClient = false;
+ let useServer = false;
+ for (let i = 0; i < body.length; i++) {
+ const node = body[i];
+ if (node.type !== 'ExpressionStatement' || !node.directive) {
+ break;
+ }
+ if (node.directive === 'use client') {
+ useClient = true;
+ }
+ if (node.directive === 'use server') {
+ useServer = true;
+ }
+ }
+
+ if (!useClient && !useServer) {
+ return content;
+ }
+
+ if (useClient && useServer) {
+ throw new Error(
+ 'Cannot have both "use client" and "use server" directives in the same file.',
+ );
+ }
+
+ let source: string = content;
+ const moduleId: string = url.pathToFileURL(path).href;
+
+ if (useClient) {
+ source = 'const Server = require(\'react-server-dom-webpack/server.node\');\n';
+ source += 'const createClientModuleProxy = Server.createClientModuleProxy;\n';
+ source += transformContent(moduleId);
+ } else if (useServer) {
+ source = 'const Server = require(\'react-server-dom-webpack/server.node\');\n';
+ source += 'const registerServerReference = Server.registerServerReference;\n';
+ for (let i = 0; i < body.length; i++) {
+ const node = body[i];
+ if (['ExpressionStatement', 'VariableDeclaration', 'AssignmentExpression', 'ForStatement', 'IfStatement'].includes(node.type)) {
+ // concat top level statements.
+ const { start, end } = node;
+ const statement = content.substring(start, end);
+ if (statement.indexOf('use client') === -1 && statement.indexOf('use server') === -1) {
+ source += `${statement}\n`;
+ }
+ } else if (node.type === 'ImportDeclaration') {
+ // concat the 'import' statements.
+ const { start, end } = node;
+ source += `${content.substring(start, end)}\n`;
+ } else if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
+ const { declaration } = node;
+ // Handling the case where the export is a function.
+ if (declaration.type === 'FunctionDeclaration') {
+ const functionName = declaration.id.name;
+ source += `${recast.print(declaration).code};\n`;
+ if (node.type === 'ExportNamedDeclaration') {
+ source += `registerServerReference(${functionName}, '${moduleId}', '${functionName}');\n`;
+ source += `module.exports.${functionName} = ${functionName};\n`;
+ } else {
+ source += `registerServerReference(${functionName}, '${moduleId}', null);\n`;
+ source += `module.exports = ${functionName};\n`;
+ }
+ } else {
+ // concat export variables
+ const exportNames = declaration.declarations.map(item => item.id.name);
+ source += `${recast.print(declaration).code}\n`;
+ for (const exportName of exportNames) {
+ source += `module.exports.${exportName} = ${exportName};\n`;
+ }
+ }
+ } else if (node.type === 'FunctionDeclaration') {
+ source += `${recast.print(node).code};\n`;
+ }
+ }
+ }
+
+ return source;
+}
const rscServerRegister = (): Plugin => {
return {
name: 'rsc-server-register',
setup: async (build: PluginBuild) => {
- build.onLoad({ filter: /\/src\/.*\.client\.(js|ts|jsx|tsx)$/ }, async (args) => { // /src\/.*\
+ build.onLoad({ filter: /\/src\/.*\.(js|ts|jsx|tsx)$/ }, async (args) => {
const { path } = args;
const loader = path.endsWith('.tsx') || path.endsWith('.ts') ? 'tsx' : 'jsx';
- const moduleId: string = url.pathToFileURL(path).href;
- let source = 'const Server: any = require(\'react-server-dom-webpack/server.node\');const createClientModuleProxy = Server.createClientModuleProxy;';
- source += transformContent(moduleId);
+ let content: string = await fs.promises.readFile(path, 'utf-8');
+ const source = rscCodeTransform({ content, path });
return { contents: source, loader };
});
},
@@ -18,9 +130,8 @@ const rscServerRegister = (): Plugin => {
};
function transformContent(moduleId: string) {
- const content = `\
- const comp = createClientModuleProxy('${moduleId}');
- module.exports = comp`;
+ let content = `const comp = createClientModuleProxy('${moduleId}');\n`;
+ content += 'module.exports = comp;\n';
return content;
}
diff --git a/packages/ice/tests/fixtures/rscTransform/client/clientInput1.tsx b/packages/ice/tests/fixtures/rscTransform/client/clientInput1.tsx
new file mode 100644
index 0000000000..86c12f3cb8
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/client/clientInput1.tsx
@@ -0,0 +1,28 @@
+'use client';
+import { useState } from 'react';
+import styles from './index.module.css';
+import { clientPrint } from './EditButton';
+
+type Name = string;
+interface Obj {
+ name: Name;
+ age: number;
+}
+
+export default function Counter({ children }) {
+ const [count, setCount] = useState(0);
+ const obj: Obj = { name: 'name', age: 100 };
+
+ function updateCount() {
+ setCount(count + 1);
+ }
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/packages/ice/tests/fixtures/rscTransform/client/clientInput2.jsx b/packages/ice/tests/fixtures/rscTransform/client/clientInput2.jsx
new file mode 100644
index 0000000000..a03fa52ef3
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/client/clientInput2.jsx
@@ -0,0 +1,20 @@
+'use client';
+import { useState } from 'react';
+import styles from './index.module.css';
+import { clientPrint } from './EditButton';
+
+export default function Counter({ children }) {
+ const [count, setCount] = useState(0);
+
+ function updateCount() {
+ setCount(count + 1);
+ }
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/packages/ice/tests/fixtures/rscTransform/client/clientInput3.js b/packages/ice/tests/fixtures/rscTransform/client/clientInput3.js
new file mode 100644
index 0000000000..a03fa52ef3
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/client/clientInput3.js
@@ -0,0 +1,20 @@
+'use client';
+import { useState } from 'react';
+import styles from './index.module.css';
+import { clientPrint } from './EditButton';
+
+export default function Counter({ children }) {
+ const [count, setCount] = useState(0);
+
+ function updateCount() {
+ setCount(count + 1);
+ }
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/packages/ice/tests/fixtures/rscTransform/client/clientOutput1.tsx b/packages/ice/tests/fixtures/rscTransform/client/clientOutput1.tsx
new file mode 100644
index 0000000000..b798d08bba
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/client/clientOutput1.tsx
@@ -0,0 +1,4 @@
+const Server = require('react-server-dom-webpack/server.node');
+const createClientModuleProxy = Server.createClientModuleProxy;
+const comp = createClientModuleProxy('file:///home/runner/work/ice/ice/packages/ice/tests/fixtures/rscTransform/client/clientInput1.tsx');
+module.exports = comp;
diff --git a/packages/ice/tests/fixtures/rscTransform/client/clientOutput2.jsx b/packages/ice/tests/fixtures/rscTransform/client/clientOutput2.jsx
new file mode 100644
index 0000000000..f8d946183d
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/client/clientOutput2.jsx
@@ -0,0 +1,4 @@
+const Server = require('react-server-dom-webpack/server.node');
+const createClientModuleProxy = Server.createClientModuleProxy;
+const comp = createClientModuleProxy('file:///home/runner/work/ice/ice/packages/ice/tests/fixtures/rscTransform/client/clientInput2.jsx');
+module.exports = comp;
diff --git a/packages/ice/tests/fixtures/rscTransform/client/clientOutput3.js b/packages/ice/tests/fixtures/rscTransform/client/clientOutput3.js
new file mode 100644
index 0000000000..f7c6c335f9
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/client/clientOutput3.js
@@ -0,0 +1,4 @@
+const Server = require('react-server-dom-webpack/server.node');
+const createClientModuleProxy = Server.createClientModuleProxy;
+const comp = createClientModuleProxy('file:///home/runner/work/ice/ice/packages/ice/tests/fixtures/rscTransform/client/clientInput3.js');
+module.exports = comp;
diff --git a/packages/ice/tests/fixtures/rscTransform/others/otherInput1.tsx b/packages/ice/tests/fixtures/rscTransform/others/otherInput1.tsx
new file mode 100644
index 0000000000..9863b9fe70
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/others/otherInput1.tsx
@@ -0,0 +1,18 @@
+import { useState } from 'react';
+
+'use client'
+export default function Counter({ children }) {
+ const [count, setCount] = useState(0);
+
+ function updateCount() {
+ setCount(count + 1);
+ }
+
+ return (
+
+ );
+}
+'use client';
\ No newline at end of file
diff --git a/packages/ice/tests/fixtures/rscTransform/others/otherInput2.tsx b/packages/ice/tests/fixtures/rscTransform/others/otherInput2.tsx
new file mode 100644
index 0000000000..2faeb5dcd7
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/others/otherInput2.tsx
@@ -0,0 +1,16 @@
+// 'use client';
+import { useState } from 'react';
+export default function Counter({ children }) {
+ const [count, setCount] = useState(0);
+
+ function updateCount() {
+ setCount(count + 1);
+ }
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/packages/ice/tests/fixtures/rscTransform/others/otherInput3.tsx b/packages/ice/tests/fixtures/rscTransform/others/otherInput3.tsx
new file mode 100644
index 0000000000..89bce3360a
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/others/otherInput3.tsx
@@ -0,0 +1,17 @@
+'use client';
+'use server';
+import { useState } from 'react';
+export default function Counter({ children }) {
+ const [count, setCount] = useState(0);
+
+ function updateCount() {
+ setCount(count + 1);
+ }
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/packages/ice/tests/fixtures/rscTransform/server/serverInput1.tsx b/packages/ice/tests/fixtures/rscTransform/server/serverInput1.tsx
new file mode 100644
index 0000000000..8cf1e5c400
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/server/serverInput1.tsx
@@ -0,0 +1,9 @@
+'use server';
+
+export default function InnerServer() {
+ return (
+
+ inner server
+
+ );
+}
\ No newline at end of file
diff --git a/packages/ice/tests/fixtures/rscTransform/server/serverInput2.tsx b/packages/ice/tests/fixtures/rscTransform/server/serverInput2.tsx
new file mode 100644
index 0000000000..d9ddc827c5
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/server/serverInput2.tsx
@@ -0,0 +1,23 @@
+'use server';
+
+import EditButton from '@/components/EditButton';
+import Counter from '@/components/Counter';
+import InnerServer from '@/components/Content';
+
+export default function Container() {
+ return (
+ <>
+
+
+
+
+ hello world
+
+ {serverPrint('serverPrint call')}
+ >
+ );
+}
+
+export function serverPrint(sentence: string): string {
+ return sentence;
+}
diff --git a/packages/ice/tests/fixtures/rscTransform/server/serverInput3.tsx b/packages/ice/tests/fixtures/rscTransform/server/serverInput3.tsx
new file mode 100644
index 0000000000..804003c5ce
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/server/serverInput3.tsx
@@ -0,0 +1,28 @@
+'use server';
+
+import EditButton from '@/components/EditButton';
+import Counter from '@/components/Counter';
+import InnerServer from '@/components/Content';
+
+export default function Container() {
+ return (
+ <>
+
+
+
+
+ hello world
+
+ {serverPrint('serverPrint call')}
+ {privateFunc()}
+ >
+ );
+}
+
+export function serverPrint(sentence: string): string {
+ return sentence;
+}
+
+function privateFunc(): string {
+ return 'privateFunc';
+}
\ No newline at end of file
diff --git a/packages/ice/tests/fixtures/rscTransform/server/serverInput4.tsx b/packages/ice/tests/fixtures/rscTransform/server/serverInput4.tsx
new file mode 100644
index 0000000000..c278b5b3cb
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/server/serverInput4.tsx
@@ -0,0 +1,27 @@
+'use server';
+
+import Counter from '@/components/Counter';
+import InnerServer from '@/components/Content';
+
+const word = 'global variable';
+let x;
+x++;
+x = 10;
+var a;
+a = {name: 'name'}
+console.log(a);
+for(let i = 0; i < 10; ++i) {
+ if(i % 2 === 0)
+ console.log(i);
+}
+
+export default function Container() {
+ return (
+ <>
+
+
+
+ {word}
+ >
+ );
+}
diff --git a/packages/ice/tests/fixtures/rscTransform/server/serverInput5.tsx b/packages/ice/tests/fixtures/rscTransform/server/serverInput5.tsx
new file mode 100644
index 0000000000..b1a228696f
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/server/serverInput5.tsx
@@ -0,0 +1,13 @@
+'use server';
+
+const word = 'global variable';
+
+export default function Container() {
+ return (
+ <>
+ {word}
+ >
+ );
+}
+
+export var tmp = 1, tmp2 = 2;
\ No newline at end of file
diff --git a/packages/ice/tests/fixtures/rscTransform/server/serverOutput1.tsx b/packages/ice/tests/fixtures/rscTransform/server/serverOutput1.tsx
new file mode 100644
index 0000000000..a716ded984
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/server/serverOutput1.tsx
@@ -0,0 +1,7 @@
+const Server = require('react-server-dom-webpack/server.node');
+const registerServerReference = Server.registerServerReference;
+function InnerServer() {
+ return inner server
;
+};
+registerServerReference(InnerServer, 'file:///home/runner/work/ice/ice/packages/ice/tests/fixtures/rscTransform/server/serverInput1.tsx', null);
+module.exports = InnerServer;
diff --git a/packages/ice/tests/fixtures/rscTransform/server/serverOutput2.tsx b/packages/ice/tests/fixtures/rscTransform/server/serverOutput2.tsx
new file mode 100644
index 0000000000..820b708549
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/server/serverOutput2.tsx
@@ -0,0 +1,21 @@
+const Server = require('react-server-dom-webpack/server.node');
+const registerServerReference = Server.registerServerReference;
+import EditButton from '@/components/EditButton';
+import Counter from '@/components/Counter';
+import InnerServer from '@/components/Content';
+function Container() {
+ return <>
+
+
+
+ hello world
+ {serverPrint("serverPrint call")}
+ >;
+};
+registerServerReference(Container, 'file:///home/runner/work/ice/ice/packages/ice/tests/fixtures/rscTransform/server/serverInput2.tsx', null);
+module.exports = Container;
+function serverPrint(sentence) {
+ return sentence;
+};
+registerServerReference(serverPrint, 'file:///home/runner/work/ice/ice/packages/ice/tests/fixtures/rscTransform/server/serverInput2.tsx', 'serverPrint');
+module.exports.serverPrint = serverPrint;
diff --git a/packages/ice/tests/fixtures/rscTransform/server/serverOutput3.tsx b/packages/ice/tests/fixtures/rscTransform/server/serverOutput3.tsx
new file mode 100644
index 0000000000..cd26878ebb
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/server/serverOutput3.tsx
@@ -0,0 +1,25 @@
+const Server = require('react-server-dom-webpack/server.node');
+const registerServerReference = Server.registerServerReference;
+import EditButton from '@/components/EditButton';
+import Counter from '@/components/Counter';
+import InnerServer from '@/components/Content';
+function Container() {
+ return <>
+
+
+
+ hello world
+ {serverPrint("serverPrint call")}
+ {privateFunc()}
+ >;
+};
+registerServerReference(Container, 'file:///Users/lzx/Documents/project/ice/packages/ice/tests/fixtures/rscTransform/server/serverInput3.tsx', null);
+module.exports = Container;
+function serverPrint(sentence) {
+ return sentence;
+};
+registerServerReference(serverPrint, 'file:///home/runner/work/ice/ice/packages/ice/tests/fixtures/rscTransform/server/serverInput3.tsx', 'serverPrint');
+module.exports.serverPrint = serverPrint;
+function privateFunc() {
+ return "privateFunc";
+};
diff --git a/packages/ice/tests/fixtures/rscTransform/server/serveroutput4.tsx b/packages/ice/tests/fixtures/rscTransform/server/serveroutput4.tsx
new file mode 100644
index 0000000000..823ea30399
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/server/serveroutput4.tsx
@@ -0,0 +1,25 @@
+const Server = require('react-server-dom-webpack/server.node');
+const registerServerReference = Server.registerServerReference;
+import Counter from '@/components/Counter';
+import InnerServer from '@/components/Content';
+const word = 'global variable';
+let x;
+x++;
+x = 10;
+var a;
+a = { name: 'name' };
+console.log(a);
+for (let i = 0; i < 10; ++i) {
+ if (i % 2 === 0)
+ console.log(i);
+}
+function Container() {
+ return <>
+
+
+
+ {word}
+ >;
+};
+registerServerReference(Container, 'file:///home/runner/work/ice/ice/packages/ice/tests/fixtures/rscTransform/server/serverInput4.tsx', null);
+module.exports = Container;
diff --git a/packages/ice/tests/fixtures/rscTransform/server/serveroutput5.tsx b/packages/ice/tests/fixtures/rscTransform/server/serveroutput5.tsx
new file mode 100644
index 0000000000..bd4dbc11e8
--- /dev/null
+++ b/packages/ice/tests/fixtures/rscTransform/server/serveroutput5.tsx
@@ -0,0 +1,13 @@
+const Server = require('react-server-dom-webpack/server.node');
+const registerServerReference = Server.registerServerReference;
+const word = 'global variable';
+function Container() {
+ return <>
+ {word}
+ >;
+};
+registerServerReference(Container, 'file:///home/runner/work/ice/ice/packages/ice/tests/fixtures/rscTransform/server/serverInput5.tsx', null);
+module.exports = Container;
+var tmp = 1, tmp2 = 2;
+module.exports.tmp = tmp;
+module.exports.tmp2 = tmp2;
diff --git a/packages/ice/tests/rscServerRegister.test.ts b/packages/ice/tests/rscServerRegister.test.ts
new file mode 100644
index 0000000000..a5d3939d98
--- /dev/null
+++ b/packages/ice/tests/rscServerRegister.test.ts
@@ -0,0 +1,95 @@
+import * as path from 'path';
+import { expect, it, describe } from 'vitest';
+import fse from 'fs-extra';
+import { rscCodeTransform } from '../src/esbuild/rscServerRegister';
+
+const rootDir = path.join(__dirname, './fixtures/rscTransform');
+
+async function getFileInfo(filePath: string) {
+ filePath = path.join(rootDir, filePath);
+ const content = await fse.readFile(filePath, 'utf-8');
+ return { content, path: filePath };
+}
+
+describe('rsc client test', () => {
+ it('basic tsx client component', async () => {
+ const fileInfo = await getFileInfo('client/clientInput1.tsx');
+ const content = rscCodeTransform(fileInfo);
+ const { content: expectContent } = await getFileInfo('client/clientOutput1.tsx');
+ expect(content).toBe(expectContent);
+ });
+
+ it('basic jsx client component', async () => {
+ const fileInfo = await getFileInfo('client/clientInput2.jsx');
+ const content = rscCodeTransform(fileInfo);
+ const { content: expectContent } = await getFileInfo('client/clientOutput2.jsx');
+ expect(content).toBe(expectContent);
+ });
+
+ it('basic js client component', async () => {
+ const fileInfo = await getFileInfo('client/clientInput3.js');
+ const content = rscCodeTransform(fileInfo);
+ const { content: expectContent } = await getFileInfo('client/clientOutput3.js');
+ expect(content).toBe(expectContent);
+ });
+});
+
+describe('rsc server test', () => {
+ it('basic server component', async () => {
+ const fileInfo = await getFileInfo('server/serverInput1.tsx');
+ const content = rscCodeTransform(fileInfo);
+ const { content: expectContent } = await getFileInfo('server/serverOutput1.tsx');
+ expect(content).toBe(expectContent);
+ });
+
+ it('server component with export function and typescript', async () => {
+ const fileInfo = await getFileInfo('server/serverInput2.tsx');
+ const content = rscCodeTransform(fileInfo);
+ const { content: expectContent } = await getFileInfo('server/serverOutput2.tsx');
+ expect(content).toBe(expectContent);
+ });
+
+ it('server component with private function and typescript', async () => {
+ const fileInfo = await getFileInfo('server/serverInput3.tsx');
+ const content = rscCodeTransform(fileInfo);
+ const { content: expectContent } = await getFileInfo('server/serverOutput3.tsx');
+ expect(content).toBe(expectContent);
+ });
+
+ it('server component with global statement', async () => {
+ const fileInfo = await getFileInfo('server/serverInput4.tsx');
+ const content = rscCodeTransform(fileInfo);
+ const { content: expectContent } = await getFileInfo('server/serverOutput4.tsx');
+ expect(content).toBe(expectContent);
+ });
+
+ it('server component with export variables', async () => {
+ const fileInfo = await getFileInfo('server/serverInput5.tsx');
+ const content = rscCodeTransform(fileInfo);
+ const { content: expectContent } = await getFileInfo('server/serverOutput5.tsx');
+ expect(content).toBe(expectContent);
+ });
+});
+
+describe('rsc boundary conditions', () => {
+ it('use client not at first line', async () => {
+ const fileInfo = await getFileInfo('others/otherInput1.tsx');
+ const content = rscCodeTransform(fileInfo);
+ expect(content.indexOf('createClientModuleProxy') === -1).toBeTruthy();
+ });
+
+ it('\'use client\' has been commented out', async () => {
+ const fileInfo = await getFileInfo('others/otherInput2.tsx');
+ const content = rscCodeTransform(fileInfo);
+ expect(content.indexOf('createClientModuleProxy') === -1).toBeTruthy();
+ });
+
+ it('with both use client and use server', async () => {
+ const fileInfo = await getFileInfo('others/otherInput3.tsx');
+ try {
+ let content = rscCodeTransform(fileInfo);
+ } catch (e) {
+ expect(e.message === 'Cannot have both "use client" and "use server" directives in the same file.').toBeTruthy();
+ }
+ });
+});
diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts
index 2a618bfa4f..9b09642ac8 100644
--- a/packages/runtime/src/index.ts
+++ b/packages/runtime/src/index.ts
@@ -26,8 +26,7 @@ import type { RunClientAppOptions, CreateRoutes } from './runClientApp.js';
import { useAppContext as useInternalAppContext, useAppData, AppContextProvider } from './AppContext.js';
import { getAppData } from './appData.js';
import { useData, useConfig } from './RouteContext.js';
-import { runRSCClientApp } from './runRSCClientApp.js';
-
+import { runRSCClientApp, useRefresh } from './runRSCClientApp.js';
import {
Meta,
Title,
@@ -146,6 +145,7 @@ export {
RouteErrorComponent,
runRSCClientApp,
+ useRefresh,
};
export type {
diff --git a/packages/runtime/src/runRSCClientApp.tsx b/packages/runtime/src/runRSCClientApp.tsx
index 14b0ece3a7..b9fe82d8e5 100644
--- a/packages/runtime/src/runRSCClientApp.tsx
+++ b/packages/runtime/src/runRSCClientApp.tsx
@@ -3,24 +3,61 @@ import * as ReactDOM from 'react-dom/client';
import pkg from 'react-server-dom-webpack/client';
// @ts-ignore
-const { Suspense, use } = React;
+const { Suspense, use, useState, createContext, useContext, startTransition } = React;
const { createFromFetch } = pkg;
export async function runRSCClientApp() {
- function App({ response }) {
- return (
- Loading...}>
- {use(response)}
-
+ const container = document.getElementById('app');
+ const root = ReactDOM.createRoot(container);
+ root.render();
+}
+
+function Root() {
+ return (
+
+ );
+}
+
+const RouterContext = createContext(null);
+const initialCache = new Map();
+
+function Router() {
+ const [cache, setCache] = useState(initialCache);
+ const [location, setLocation] = useState(window.location.href);
+
+ let content = cache.get(location);
+ if (!content) {
+ content = createFromFetch(
+ getReactTree(location),
);
+ cache.set(location, content);
+ }
+
+ function refresh() {
+ startTransition(() => {
+ const nextCache = new Map();
+ const nextContent = createFromFetch(
+ getReactTree(location),
+ );
+ nextCache.set(location, nextContent);
+ setCache(nextCache);
+ });
}
- const rscPath = location.href + (location.href.indexOf('?') ? '?rsc' : '&rsc');
- const response = createFromFetch(
- fetch(rscPath),
+ return (
+
+ Loading...}>
+ {use(content)}
+
+
);
+}
- const container = document.getElementById('app');
- const root = ReactDOM.createRoot(container);
- root.render();
+export function useRefresh() {
+ const router = useContext(RouterContext);
+ return router.refresh;
+}
+
+function getReactTree(location) {
+ return fetch(location + location.indexOf('?') ? '?rsc' : '&rsc');
}
\ No newline at end of file
diff --git a/packages/runtime/src/runServerApp.tsx b/packages/runtime/src/runServerApp.tsx
index 3c028809d1..050f5db652 100644
--- a/packages/runtime/src/runServerApp.tsx
+++ b/packages/runtime/src/runServerApp.tsx
@@ -256,6 +256,10 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio
}
}
+ if (renderOptions.clientManifest && req.url.indexOf('?') === -1) {
+ return renderDocument({ matches: [], routes, renderOptions, documentData });
+ }
+
// HashRouter loads route modules by the CSR.
if (appConfig?.router?.type === 'hash') {
return renderDocument({ matches: [], routes, renderOptions, documentData });
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 587f72c2f4..55cdc129bc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1142,6 +1142,8 @@ importers:
'@types/micromatch': ^4.0.2
'@types/multer': ^1.4.7
'@types/temp': ^0.9.1
+ acorn: ^8.10.0
+ acorn-jsx: ^5.3.2
address: ^1.1.2
build-scripts: ^2.1.2-0
chalk: ^4.0.0
@@ -1165,6 +1167,7 @@ importers:
path-to-regexp: ^6.2.0
react: ^18.2.0
react-router: 6.14.2
+ recast: ^0.23.4
regenerator-runtime: ^0.13.0
resolve.exports: ^1.1.0
sass: ^1.50.0
@@ -1184,6 +1187,8 @@ importers:
'@ice/webpack-config': link:../webpack-config
'@swc/helpers': 0.5.1
'@types/express': 4.17.17
+ acorn: 8.10.0
+ acorn-jsx: 5.3.2_acorn@8.10.0
address: 1.2.2
build-scripts: 2.1.2-0
chalk: 4.1.2
@@ -1202,6 +1207,7 @@ importers:
mrmime: 1.0.1
open: 8.4.2
path-to-regexp: 6.2.1
+ recast: 0.23.4
regenerator-runtime: 0.13.11
resolve.exports: 1.1.1
semver: 7.4.0
@@ -5565,7 +5571,7 @@ packages:
'@rollup/plugin-replace': 5.0.2_rollup@2.79.1
'@rollup/pluginutils': 4.2.1
'@swc/core': 1.3.32
- acorn: 8.8.2
+ acorn: 8.10.0
autoprefixer: 10.4.13_postcss@8.4.25
build-scripts: 2.1.0
cac: 6.7.14
@@ -8723,10 +8729,17 @@ packages:
/acorn-globals/7.0.1:
resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==}
dependencies:
- acorn: 8.8.2
+ acorn: 8.10.0
acorn-walk: 8.2.0
dev: true
+ /acorn-import-assertions/1.9.0_acorn@8.10.0:
+ resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
+ peerDependencies:
+ acorn: ^8
+ dependencies:
+ acorn: 8.10.0
+
/acorn-import-assertions/1.9.0_acorn@8.8.2:
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
peerDependencies:
@@ -8734,13 +8747,12 @@ packages:
dependencies:
acorn: 8.8.2
- /acorn-jsx/5.3.2_acorn@8.8.2:
+ /acorn-jsx/5.3.2_acorn@8.10.0:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
- acorn: 8.8.2
- dev: true
+ acorn: 8.10.0
/acorn-loose/8.3.0:
resolution: {integrity: sha512-75lAs9H19ldmW+fAbyqHdjgdCrz0pWGXKmnqFoh8PyVd1L2RIb4RzYrSjmopeqv3E1G3/Pimu6GgLlrGbrkF7w==}
@@ -8771,6 +8783,11 @@ packages:
hasBin: true
dev: true
+ /acorn/8.10.0:
+ resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
/acorn/8.8.2:
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
engines: {node: '>=0.4.0'}
@@ -9251,6 +9268,16 @@ packages:
engines: {node: '>=0.8'}
dev: false
+ /assert/2.1.0:
+ resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==}
+ dependencies:
+ call-bind: 1.0.2
+ is-nan: 1.3.2
+ object-is: 1.1.5
+ object.assign: 4.1.4
+ util: 0.12.5
+ dev: false
+
/assertion-error/1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: true
@@ -9259,6 +9286,13 @@ packages:
resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==}
dev: true
+ /ast-types/0.16.1:
+ resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
+ engines: {node: '>=4'}
+ dependencies:
+ tslib: 2.5.0
+ dev: false
+
/astral-regex/2.0.0:
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
engines: {node: '>=8'}
@@ -12595,8 +12629,8 @@ packages:
resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
- acorn: 8.8.2
- acorn-jsx: 5.3.2_acorn@8.8.2
+ acorn: 8.10.0
+ acorn-jsx: 5.3.2_acorn@8.10.0
eslint-visitor-keys: 3.3.0
dev: true
@@ -14393,6 +14427,14 @@ packages:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
dev: true
+ /is-nan/1.3.2:
+ resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.2.0
+ dev: false
+
/is-negative-zero/2.0.2:
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
engines: {node: '>= 0.4'}
@@ -16490,7 +16532,7 @@ packages:
/mlly/1.1.1:
resolution: {integrity: sha512-Jnlh4W/aI4GySPo6+DyTN17Q75KKbLTyFK8BrGhjNP4rxuUjbRWhE6gHg3bs33URWAF44FRm7gdQA348i3XxRw==}
dependencies:
- acorn: 8.8.2
+ acorn: 8.10.0
pathe: 1.1.0
pkg-types: 1.0.2
ufo: 1.1.1
@@ -16715,7 +16757,6 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.2.0
- dev: true
/object-keys/1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
@@ -20370,6 +20411,17 @@ packages:
/reading-time/1.5.0:
resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==}
+ /recast/0.23.4:
+ resolution: {integrity: sha512-qtEDqIZGVcSZCHniWwZWbRy79Dc6Wp3kT/UmDA2RJKBPg7+7k51aQBZirHmUGn5uvHf2rg8DkjizrN26k61ATw==}
+ engines: {node: '>= 4'}
+ dependencies:
+ assert: 2.1.0
+ ast-types: 0.16.1
+ esprima: 4.0.1
+ source-map: 0.6.1
+ tslib: 2.5.0
+ dev: false
+
/rechoir/0.6.2:
resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
engines: {node: '>= 0.10'}
@@ -21632,7 +21684,7 @@ packages:
/strip-literal/0.4.2:
resolution: {integrity: sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw==}
dependencies:
- acorn: 8.8.2
+ acorn: 8.10.0
dev: true
/style-equal/1.0.0:
@@ -22168,7 +22220,7 @@ packages:
hasBin: true
dependencies:
'@jridgewell/source-map': 0.3.2
- acorn: 8.8.2
+ acorn: 8.10.0
commander: 2.20.3
source-map-support: 0.5.21
@@ -22398,7 +22450,7 @@ packages:
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.3
'@types/node': 17.0.45
- acorn: 8.8.2
+ acorn: 8.10.0
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
@@ -23295,7 +23347,7 @@ packages:
hasBin: true
dependencies:
'@discoveryjs/json-ext': 0.5.7
- acorn: 8.8.2
+ acorn: 8.10.0
acorn-walk: 8.2.0
chalk: 4.1.2
commander: 7.2.0
@@ -23715,8 +23767,8 @@ packages:
'@webassemblyjs/ast': 1.11.1
'@webassemblyjs/wasm-edit': 1.11.1
'@webassemblyjs/wasm-parser': 1.11.1
- acorn: 8.8.2
- acorn-import-assertions: 1.9.0_acorn@8.8.2
+ acorn: 8.10.0
+ acorn-import-assertions: 1.9.0_acorn@8.10.0
browserslist: 4.21.5
chrome-trace-event: 1.0.3
enhanced-resolve: 5.15.0
@@ -23755,8 +23807,8 @@ packages:
'@webassemblyjs/ast': 1.11.1
'@webassemblyjs/wasm-edit': 1.11.1
'@webassemblyjs/wasm-parser': 1.11.1
- acorn: 8.8.2
- acorn-import-assertions: 1.9.0_acorn@8.8.2
+ acorn: 8.10.0
+ acorn-import-assertions: 1.9.0_acorn@8.10.0
browserslist: 4.21.5
chrome-trace-event: 1.0.3
enhanced-resolve: 5.15.0
@@ -23795,8 +23847,8 @@ packages:
'@webassemblyjs/ast': 1.11.5
'@webassemblyjs/wasm-edit': 1.11.5
'@webassemblyjs/wasm-parser': 1.11.5
- acorn: 8.8.2
- acorn-import-assertions: 1.9.0_acorn@8.8.2
+ acorn: 8.10.0
+ acorn-import-assertions: 1.9.0_acorn@8.10.0
browserslist: 4.21.5
chrome-trace-event: 1.0.3
enhanced-resolve: 5.14.1