From 796e593fce886b2fd77fd5437f73a521d6399d51 Mon Sep 17 00:00:00 2001
From: hzy <28915578+hzy@users.noreply.github.com>
Date: Wed, 5 Mar 2025 13:59:25 +0800
Subject: [PATCH 1/2] feat(react): support lynx ssr
In this PR, we add support for Lynx SSR by adding two call-by-native api `ssrEncode` and `ssrHydrate`.
---
.changeset/small-icons-hunt.md | 5 +
packages/react/runtime/__test__/ssr.test.jsx | 474 ++++++++++++++++++
.../runtime/__test__/utils/envManager.ts | 2 +
.../react/runtime/__test__/utils/globals.js | 1 +
.../runtime/__test__/utils/nativeMethod.ts | 63 +--
.../react/runtime/src/lifecycle/render.ts | 3 +
packages/react/runtime/src/list.ts | 13 +-
.../react/runtime/src/lynx/calledByNative.ts | 51 +-
packages/react/runtime/src/opcodes.ts | 90 ++++
packages/react/runtime/src/root.ts | 2 +-
packages/react/runtime/types/types.d.ts | 2 +
.../etc/react-rsbuild-plugin.api.md | 1 +
packages/rspeedy/plugin-react/src/entry.ts | 2 +
.../plugin-react/src/pluginReactLynx.ts | 10 +
.../etc/react-webpack-plugin.api.md | 1 +
.../src/ReactWebpackPlugin.ts | 7 +
16 files changed, 676 insertions(+), 51 deletions(-)
create mode 100644 .changeset/small-icons-hunt.md
create mode 100644 packages/react/runtime/__test__/ssr.test.jsx
diff --git a/.changeset/small-icons-hunt.md b/.changeset/small-icons-hunt.md
new file mode 100644
index 0000000000..bff1b88d7d
--- /dev/null
+++ b/.changeset/small-icons-hunt.md
@@ -0,0 +1,5 @@
+---
+"@lynx-js/react": patch
+---
+
+Support Lynx SSR.
diff --git a/packages/react/runtime/__test__/ssr.test.jsx b/packages/react/runtime/__test__/ssr.test.jsx
new file mode 100644
index 0000000000..ce5a52f04a
--- /dev/null
+++ b/packages/react/runtime/__test__/ssr.test.jsx
@@ -0,0 +1,474 @@
+/** @jsxImportSource ../lepus */
+
+import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { elementTree, options } from './utils/nativeMethod';
+import { globalEnvManager } from './utils/envManager';
+import { __root } from '../src/root';
+
+const ssrIDMap = new Map();
+
+beforeAll(() => {
+ globalEnvManager.switchToMainThread();
+ globalThis.__TESTING_FORCE_RENDER_TO_OPCODE__ = true;
+
+ let ssrID = 666;
+ options.onCreateElement = element => {
+ element.ssrID = ssrID++;
+ element.toJSON = function() {
+ return {
+ ssrID: this.ssrID,
+ };
+ };
+
+ ssrIDMap.set(element.ssrID, element);
+ };
+});
+
+afterAll(() => {
+ delete options.onCreateElement;
+});
+
+beforeEach(() => {
+ globalEnvManager.resetEnv();
+ elementTree.clear();
+ globalEnvManager.switchToMainThread();
+});
+
+afterEach(() => {});
+
+describe('ssr', () => {
+ it('basic - ssrEncode', () => {
+ function Comp() {
+ return ;
+ }
+ __root.__jsx = ;
+
+ renderPage();
+ expect(JSON.parse(ssrEncode())).toMatchInlineSnapshot(`
+ {
+ "__opcodes": [
+ 0,
+ [
+ "__Card__:__snapshot_a94a8_test_1",
+ -2,
+ [
+ {
+ "ssrID": 667,
+ },
+ ],
+ ],
+ 1,
+ ],
+ }
+ `);
+ });
+
+ it('basic - ssrEncode - nested', () => {
+ function Hello() {
+ return (
+
+
+
+ );
+ }
+ function World() {
+ return (
+
+ Hello World
+
+
+ );
+ }
+ function HelloLynx() {
+ return (
+
+
+
+ );
+ }
+ function Lynx() {
+ return (
+
+ Hello Lynx
+ Hello Lynx
+
+ );
+ }
+ __root.__jsx = ;
+ renderPage();
+ expect(JSON.parse(ssrEncode())).toMatchInlineSnapshot(`
+ {
+ "__opcodes": [
+ 0,
+ [
+ "__Card__:__snapshot_a94a8_test_2",
+ -2,
+ [
+ {
+ "ssrID": 669,
+ },
+ ],
+ ],
+ 0,
+ [
+ "__Card__:__snapshot_a94a8_test_3",
+ -3,
+ [
+ {
+ "ssrID": 670,
+ },
+ {
+ "ssrID": 671,
+ },
+ {
+ "ssrID": 672,
+ },
+ {
+ "ssrID": 673,
+ },
+ ],
+ ],
+ 0,
+ [
+ "__Card__:__snapshot_a94a8_test_4",
+ -4,
+ [
+ {
+ "ssrID": 674,
+ },
+ ],
+ ],
+ 0,
+ [
+ "__Card__:__snapshot_a94a8_test_5",
+ -5,
+ [
+ {
+ "ssrID": 675,
+ },
+ {
+ "ssrID": 676,
+ },
+ {
+ "ssrID": 677,
+ },
+ {
+ "ssrID": 678,
+ },
+ {
+ "ssrID": 679,
+ },
+ ],
+ ],
+ 1,
+ 1,
+ 1,
+ 1,
+ ],
+ }
+ `);
+ });
+
+ it('basic - ssrEncode - attribute', () => {
+ const c = 'red';
+ const s = { color: 'red' };
+ function Comp() {
+ return (
+ {}}
+ ref={() => {}}
+ main-thread:bindtap={{ _lepusWorkletHash: '1' }}
+ main-thread:ref={{ _lepusWorkletHash: '2' }}
+ data-xxx={c}
+ />
+ );
+ }
+ __root.__jsx = ;
+ renderPage();
+ expect(JSON.parse(ssrEncode())).toMatchInlineSnapshot(`
+ {
+ "__opcodes": [
+ 0,
+ [
+ "__Card__:__snapshot_a94a8_test_6",
+ -2,
+ [
+ {
+ "ssrID": 681,
+ },
+ ],
+ ],
+ 2,
+ "values",
+ [
+ "red",
+ {
+ "color": "red",
+ },
+ "-2:2:",
+ "-2:3:",
+ {
+ "_lepusWorkletHash": "1",
+ "_workletType": "main-thread",
+ },
+ {
+ "_lepusWorkletHash": "2",
+ },
+ "red",
+ ],
+ 1,
+ ],
+ }
+ `);
+ });
+
+ it('basic - ssrEncode - JSXSpread', () => {
+ const c = 'red';
+ const s = { color: 'red' };
+
+ function Comp() {
+ const props = {
+ className: c,
+ style: s,
+ bindtap: () => {},
+ ref: () => {},
+ 'main-thread:bindtap': { _lepusWorkletHash: '1' },
+ 'main-thread:ref': { _lepusWorkletHash: '2' },
+ 'data-xxx': c,
+ };
+ return ;
+ }
+
+ __root.__jsx = ;
+ renderPage();
+ expect(JSON.parse(ssrEncode())).toMatchInlineSnapshot(`
+ {
+ "__opcodes": [
+ 0,
+ [
+ "__Card__:__snapshot_a94a8_test_7",
+ -2,
+ [
+ {
+ "ssrID": 683,
+ },
+ ],
+ ],
+ 2,
+ "values",
+ [
+ {
+ "bindtap": "-2:0:bindtap",
+ "className": "red",
+ "data-xxx": "red",
+ "main-thread:bindtap": {
+ "_lepusWorkletHash": "1",
+ "_workletType": "main-thread",
+ },
+ "main-thread:ref": {
+ "_lepusWorkletHash": "2",
+ },
+ "ref": "-2:0:ref",
+ "style": {
+ "color": "red",
+ },
+ },
+ ],
+ 1,
+ ],
+ }
+ `);
+ });
+
+ it('basic - ssrEncode - page', () => {
+ function Hello() {
+ return (
+
+
+
+ );
+ }
+ function World() {
+ return (
+
+ Hello World
+
+
+ );
+ }
+ function HelloLynx() {
+ return (
+
+
+
+ );
+ }
+ __root.__jsx = ;
+ renderPage();
+ expect(JSON.parse(ssrEncode())).toMatchInlineSnapshot(`
+ {
+ "__opcodes": [
+ 0,
+ [
+ "__Card__:__snapshot_a94a8_test_8",
+ -2,
+ [
+ {
+ "ssrID": 685,
+ },
+ ],
+ ],
+ 0,
+ [
+ "__Card__:__snapshot_a94a8_test_9",
+ -3,
+ [
+ {
+ "ssrID": 686,
+ },
+ {
+ "ssrID": 687,
+ },
+ {
+ "ssrID": 688,
+ },
+ {
+ "ssrID": 689,
+ },
+ ],
+ ],
+ 0,
+ [
+ "__Card__:__snapshot_a94a8_test_10",
+ -4,
+ [
+ {
+ "ssrID": 690,
+ },
+ ],
+ ],
+ 1,
+ 1,
+ 1,
+ ],
+ "__root_values": [
+ {
+ "className": "xxxx",
+ },
+ ],
+ }
+ `);
+ });
+
+ it('basic - ssrHydrate', () => {
+ function Hello() {
+ return (
+
+
+
+ );
+ }
+ function World() {
+ return (
+
+ Hello World
+
+
+ );
+ }
+ function HelloLynx() {
+ return (
+
+
+
+ );
+ }
+ __root.__jsx = ;
+ renderPage();
+
+ const __page = __root.__element_root;
+
+ const info = ssrEncode();
+
+ globalEnvManager.resetEnv();
+ globalEnvManager.switchToMainThread();
+ elementTree.clear();
+
+ const __GetPageElement = () => __page;
+ const __GetTemplateParts = () => Object.fromEntries(ssrIDMap.entries());
+ vi.stubGlobal('__GetPageElement', __GetPageElement);
+ vi.stubGlobal('__GetTemplateParts', __GetTemplateParts);
+
+ ssrHydrate(info);
+ expect(__root.__element_root).toMatchInlineSnapshot(`
+
+
+
+
+
+
+
+
+
+
+
+
+ `);
+
+ vi.unstubAllGlobals();
+ });
+
+ it('basic - ssrEncode - list', () => {
+ function Hello() {
+ return (
+
+ {[1, 2, 3].map((item, index) => {
+ return (
+
+ {item}
+
+ );
+ })}
+
+ );
+ }
+ __root.__jsx = ;
+ renderPage();
+
+ const listRef = elementTree.getElementById('ssr-list');
+ const uiSign1 = elementTree.triggerComponentAtIndex(listRef, 0);
+ const uiSign2 = elementTree.triggerComponentAtIndex(listRef, 1);
+ const uiSign3 = elementTree.triggerComponentAtIndex(listRef, 2);
+
+ const info = ssrEncode();
+
+ const __page = __root.__element_root;
+
+ globalEnvManager.resetEnv();
+ globalEnvManager.switchToMainThread();
+ delete listRef.componentAtIndex;
+ delete listRef.enqueueComponent;
+
+ const __GetPageElement = () => __page;
+ const __GetTemplateParts = () => Object.fromEntries(ssrIDMap.entries());
+ vi.stubGlobal('__GetPageElement', __GetPageElement);
+ vi.stubGlobal('__GetTemplateParts', __GetTemplateParts);
+
+ ssrHydrate(info);
+ {
+ const listRef = elementTree.getElementById('ssr-list');
+ expect(elementTree.triggerComponentAtIndex(listRef, 0)).toEqual(uiSign1);
+ expect(elementTree.triggerComponentAtIndex(listRef, 1)).toEqual(uiSign2);
+ expect(elementTree.triggerComponentAtIndex(listRef, 2)).toEqual(uiSign3);
+ }
+
+ vi.unstubAllGlobals();
+ });
+});
diff --git a/packages/react/runtime/__test__/utils/envManager.ts b/packages/react/runtime/__test__/utils/envManager.ts
index 80baa54172..21e6d6b261 100644
--- a/packages/react/runtime/__test__/utils/envManager.ts
+++ b/packages/react/runtime/__test__/utils/envManager.ts
@@ -9,6 +9,7 @@ import { BackgroundSnapshotInstance } from '../../src/backgroundSnapshot.js';
import { backgroundSnapshotInstanceManager, SnapshotInstance, snapshotInstanceManager } from '../../src/snapshot.js';
import { deinitGlobalSnapshotPatch } from '../../src/lifecycle/patch/snapshotPatch.js';
import { globalPipelineOptions, setPipeline } from '../../src/lynx/performance.js';
+import { clearListGlobal } from '../../src/list.js';
export class EnvManager {
root: typeof __root | undefined;
@@ -69,6 +70,7 @@ export class EnvManager {
backgroundSnapshotInstanceManager.nextId = 0;
snapshotInstanceManager.clear();
snapshotInstanceManager.nextId = 0;
+ clearListGlobal();
deinitGlobalSnapshotPatch();
this.switchToBackground();
this.switchToMainThread();
diff --git a/packages/react/runtime/__test__/utils/globals.js b/packages/react/runtime/__test__/utils/globals.js
index 61d15adbd8..bfd57a08e1 100644
--- a/packages/react/runtime/__test__/utils/globals.js
+++ b/packages/react/runtime/__test__/utils/globals.js
@@ -39,6 +39,7 @@ function injectGlobals() {
globalThis.__BACKGROUND__ = true;
globalThis.__MAIN_THREAD__ = true;
globalThis.__REF_FIRE_IMMEDIATELY__ = false;
+ globalThis.__ENABLE_SSR__ = true;
globalThis.__FIRST_SCREEN_SYNC_TIMING__ = 'immediately';
globalThis.__TESTING_FORCE_RENDER_TO_OPCODE__ = false;
globalThis.globDynamicComponentEntry = '__Card__';
diff --git a/packages/react/runtime/__test__/utils/nativeMethod.ts b/packages/react/runtime/__test__/utils/nativeMethod.ts
index bb839260a7..ac834702b0 100644
--- a/packages/react/runtime/__test__/utils/nativeMethod.ts
+++ b/packages/react/runtime/__test__/utils/nativeMethod.ts
@@ -13,8 +13,14 @@ interface Element {
children: any[];
}
+interface ElementOptions {
+ onCreateElement?: ((element: Element) => void) | undefined;
+}
+
export let uiSignNext = 0;
export const parentMap = new WeakMap();
+// export const elementPrototype = Object.create(null);
+export const options: ElementOptions = {};
export const elementTree = new (class {
root?: Element = undefined;
@@ -24,22 +30,11 @@ export const elementTree = new (class {
}
__CreateRawText(text: string) {
- // return text;
- const json = {
- type: 'raw-text',
- children: [],
- props: {
- text,
- },
- parentComponentUniqueId: 0,
- };
- Object.defineProperty(json, '$$typeof', {
- value: Symbol.for('react.test.json'),
- });
- Object.defineProperty(json, '$$uiSign', {
- value: uiSignNext++,
- });
- return json;
+ const r = this.__CreateElement('raw-text', 0);
+ // @ts-ignore
+ r.props.text = text;
+ this.root ??= r;
+ return r;
}
__GetElementUniqueID(e: Element): number {
@@ -65,6 +60,8 @@ export const elementTree = new (class {
value: uiSignNext++,
});
+ options.onCreateElement?.(json);
+
this.root ??= json;
return json;
}
@@ -77,37 +74,15 @@ export const elementTree = new (class {
}
__CreateText(parentComponentUniqueId: number) {
- const json = {
- type: 'text',
- children: [],
- props: {},
- parentComponentUniqueId,
- };
- Object.defineProperty(json, '$$typeof', {
- value: Symbol.for('react.test.json'),
- });
- Object.defineProperty(json, '$$uiSign', {
- value: uiSignNext++,
- });
- this.root ??= json;
- return json;
+ const r = this.__CreateElement('text', parentComponentUniqueId);
+ this.root ??= r;
+ return r;
}
__CreateImage(parentComponentUniqueId: number) {
- const json = {
- type: 'image',
- children: [],
- props: {},
- parentComponentUniqueId,
- };
- Object.defineProperty(json, '$$typeof', {
- value: Symbol.for('react.test.json'),
- });
- Object.defineProperty(json, '$$uiSign', {
- value: uiSignNext++,
- });
- this.root ??= json;
- return json;
+ const r = this.__CreateElement('image', parentComponentUniqueId);
+ this.root ??= r;
+ return r;
}
__CreateWrapperElement(parentComponentUniqueId: number) {
diff --git a/packages/react/runtime/src/lifecycle/render.ts b/packages/react/runtime/src/lifecycle/render.ts
index ef3450d043..1c67d0e547 100644
--- a/packages/react/runtime/src/lifecycle/render.ts
+++ b/packages/react/runtime/src/lifecycle/render.ts
@@ -37,6 +37,9 @@ function renderMainThread(): void {
console.profile('renderOpcodesInto');
}
renderOpcodesInto(opcodes, __root as any);
+ if (__ENABLE_SSR__) {
+ __root.__opcodes = opcodes;
+ }
if (__PROFILE__) {
console.profileEnd();
}
diff --git a/packages/react/runtime/src/list.ts b/packages/react/runtime/src/list.ts
index 842dea9d02..02a171c260 100644
--- a/packages/react/runtime/src/list.ts
+++ b/packages/react/runtime/src/list.ts
@@ -223,8 +223,17 @@ export const __pendingListUpdates = {
},
};
-const gSignMap: Record> = {};
-const gRecycleMap: Record>> = {};
+export const gSignMap: Record> = {};
+export const gRecycleMap: Record>> = {};
+
+export function clearListGlobal(): void {
+ for (const key in gSignMap) {
+ delete gSignMap[key];
+ }
+ for (const key in gRecycleMap) {
+ delete gRecycleMap[key];
+ }
+}
export function componentAtIndexFactory(ctx: SnapshotInstance[]): ComponentAtIndexCallback {
const componentAtIndex = (
diff --git a/packages/react/runtime/src/lynx/calledByNative.ts b/packages/react/runtime/src/lynx/calledByNative.ts
index 906fc28c5b..feef3ad696 100644
--- a/packages/react/runtime/src/lynx/calledByNative.ts
+++ b/packages/react/runtime/src/lynx/calledByNative.ts
@@ -13,8 +13,51 @@ import { renderMainThread } from '../lifecycle/render.js';
import { hydrate } from '../hydrate.js';
import { markTiming, PerformanceTimingKeys, setPipeline } from './performance.js';
import { __pendingListUpdates } from '../list.js';
+import { ssrHydrateByOpcodes } from '../opcodes.js';
+
+function ssrEncode() {
+ const { __opcodes } = __root;
+ delete __root.__opcodes;
+
+ const oldToJSON = SnapshotInstance.prototype.toJSON;
+ SnapshotInstance.prototype.toJSON = function(this: SnapshotInstance): any {
+ return [
+ this.type,
+ this.__id,
+ this.__elements,
+ ];
+ };
+
+ try {
+ return JSON.stringify({ __opcodes, __root_values: __root.__values });
+ } finally {
+ SnapshotInstance.prototype.toJSON = oldToJSON;
+ }
+}
+
+function ssrHydrate(info: string) {
+ const nativePage = __GetPageElement();
+ if (!nativePage) {
+ throw 'SSR Hydration Failed! Please check if the SSR content loaded successfully!';
+ }
+
+ const refsMap = __GetTemplateParts(nativePage);
+
+ const { __opcodes, __root_values } = JSON.parse(info);
+ __root_values && __root.setAttribute('values', __root_values);
+ ssrHydrateByOpcodes(__opcodes, __root as SnapshotInstance, refsMap);
+
+ (__root as SnapshotInstance).__elements = [nativePage];
+ (__root as SnapshotInstance).__element_root = nativePage;
+}
function injectCalledByNative(): void {
+ if (process.env['NODE_ENV'] !== 'test') {
+ if (__FIRST_SCREEN_SYNC_TIMING__ !== 'jsReady' && __ENABLE_SSR__) {
+ throw new Error('`firstScreenSyncTiming` must be `jsReady` when SSR is enabled');
+ }
+ }
+
const calledByNative: LynxCallByNative = {
renderPage,
updatePage,
@@ -23,9 +66,13 @@ function injectCalledByNative(): void {
return null;
},
removeComponents: function(): void {},
+ ...(__ENABLE_SSR__ ? { ssrEncode, ssrHydrate } : {}),
};
Object.assign(globalThis, calledByNative);
+ Object.assign(globalThis, {
+ [LifecycleConstant.jsReady]: jsReady,
+ });
}
function renderPage(data: any): void {
@@ -46,10 +93,6 @@ function renderPage(data: any): void {
if (__FIRST_SCREEN_SYNC_TIMING__ === 'immediately') {
jsReady();
- } else {
- Object.assign(globalThis, {
- [LifecycleConstant.jsReady]: jsReady,
- });
}
}
diff --git a/packages/react/runtime/src/opcodes.ts b/packages/react/runtime/src/opcodes.ts
index fee2df660c..7d32f8130a 100644
--- a/packages/react/runtime/src/opcodes.ts
+++ b/packages/react/runtime/src/opcodes.ts
@@ -1,6 +1,7 @@
// Copyright 2024 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
+import { componentAtIndexFactory, enqueueComponentFactory, gRecycleMap, gSignMap } from './list.js';
import { CHILDREN } from './renderToOpcodes/constants.js';
import { SnapshotInstance } from './snapshot.js';
@@ -11,6 +12,90 @@ const enum Opcode {
Text,
}
+interface SSRFiberElement {
+ ssrID: string;
+}
+export type SSRSnapshotInstance = [string, number, SSRFiberElement[]];
+
+export function ssrHydrateByOpcodes(
+ opcodes: any[],
+ into: SnapshotInstance,
+ refMap?: Record,
+): void {
+ let top: SnapshotInstance & { __pendingElements?: SSRFiberElement[] } = into;
+ const stack: SnapshotInstance[] = [into];
+ for (let i = 0; i < opcodes.length;) {
+ const opcode = opcodes[i];
+ switch (opcode) {
+ case Opcode.Begin: {
+ const p = top;
+ const [type, __id, elements] = opcodes[i + 1] as SSRSnapshotInstance;
+ top = new SnapshotInstance(type, __id);
+ top.__pendingElements = elements;
+ p.insertBefore(top);
+ stack.push(top);
+
+ i += 2;
+ break;
+ }
+ case Opcode.End: {
+ // @ts-ignore
+ top[CHILDREN] = undefined;
+
+ top.__elements = top.__pendingElements!.map(({ ssrID }) => refMap![ssrID]!);
+ top.__element_root = top.__elements[0];
+ delete top.__pendingElements;
+
+ if (top.__snapshot_def.isListHolder) {
+ const listElement = top.__element_root!;
+ const listElementUniqueID = __GetElementUniqueID(listElement);
+ const signMap = gSignMap[listElementUniqueID] = new Map();
+ gRecycleMap[listElementUniqueID] = new Map();
+ const enqueueFunc = enqueueComponentFactory();
+ const componentAtIndex = componentAtIndexFactory(top.childNodes);
+ for (const child of top.childNodes) {
+ if (child.__element_root) {
+ const childElementUniqueID = __GetElementUniqueID(child.__element_root);
+ signMap.set(childElementUniqueID, child);
+ enqueueFunc(
+ listElement,
+ listElementUniqueID,
+ childElementUniqueID,
+ );
+ }
+ }
+ __UpdateListCallbacks(listElement, componentAtIndex, enqueueFunc);
+ }
+
+ stack.pop();
+ const p = stack[stack.length - 1];
+ top = p!;
+
+ i += 1;
+ break;
+ }
+ case Opcode.Attr: {
+ const key = opcodes[i + 1];
+ const value = opcodes[i + 2];
+ top.setAttribute(key, value);
+
+ i += 3;
+ break;
+ }
+ case Opcode.Text: {
+ const [[type, __id, elements], text] = opcodes[i + 1] as [SSRSnapshotInstance, string];
+ const s = new SnapshotInstance(type, __id);
+ s.setAttribute(0, text);
+ top.insertBefore(s);
+ s.__elements = elements.map(({ ssrID }) => refMap![ssrID]!);
+ s.__element_root = s.__elements[0];
+ i += 2;
+ break;
+ }
+ }
+ }
+}
+
export function renderOpcodesInto(opcodes: any[], into: SnapshotInstance): void {
let top: SnapshotInstance = into;
const stack: SnapshotInstance[] = [into];
@@ -24,6 +109,7 @@ export function renderOpcodesInto(opcodes: any[], into: SnapshotInstance): void
if (top.__parent) {
// already inserted
top = new SnapshotInstance(top.type);
+ opcodes[i + 1] = top;
}
p.insertBefore(top);
stack.push(top);
@@ -53,6 +139,10 @@ export function renderOpcodesInto(opcodes: any[], into: SnapshotInstance): void
case Opcode.Text: {
const text = opcodes[i + 1];
const s = new SnapshotInstance(null as unknown as string);
+ if (__ENABLE_SSR__) {
+ // We need store the just created SnapshotInstance, or it will be lost when we leave the function
+ opcodes[i + 1] = [s, text];
+ }
s.setAttribute(0, text);
top.insertBefore(s);
diff --git a/packages/react/runtime/src/root.ts b/packages/react/runtime/src/root.ts
index b0402c13ff..f4bb58f7d1 100644
--- a/packages/react/runtime/src/root.ts
+++ b/packages/react/runtime/src/root.ts
@@ -4,7 +4,7 @@
import { BackgroundSnapshotInstance } from './backgroundSnapshot.js';
import { SnapshotInstance } from './snapshot.js';
-let __root: (SnapshotInstance | BackgroundSnapshotInstance) & { __jsx?: React.ReactNode };
+let __root: (SnapshotInstance | BackgroundSnapshotInstance) & { __jsx?: React.ReactNode; __opcodes?: any[] };
function setRoot(root: typeof __root): void {
__root = root;
diff --git a/packages/react/runtime/types/types.d.ts b/packages/react/runtime/types/types.d.ts
index 4e00dae0cf..b7af9e7159 100644
--- a/packages/react/runtime/types/types.d.ts
+++ b/packages/react/runtime/types/types.d.ts
@@ -16,6 +16,7 @@ declare global {
declare const __BACKGROUND__: boolean;
declare const __MAIN_THREAD__: boolean;
declare const __PROFILE__: boolean;
+ declare const __ENABLE_SSR__: boolean;
declare function __CreatePage(componentId: string, cssId: number): FiberElement;
declare function __CreateElement(
@@ -56,6 +57,7 @@ declare global {
declare function __LastElement(parent: FiberElement): FiberElement;
declare function __NextElement(parent: FiberElement): FiberElement;
declare function __GetPageElement(): FiberElement | undefined;
+ declare function __GetTemplateParts(e: FiberElement): Record;
declare function __AddDataset(node: FiberElement, key: string, value: any): void;
declare function __SetDataset(
node: FiberElement,
diff --git a/packages/rspeedy/plugin-react/etc/react-rsbuild-plugin.api.md b/packages/rspeedy/plugin-react/etc/react-rsbuild-plugin.api.md
index ac89c19d5a..f4b96a7851 100644
--- a/packages/rspeedy/plugin-react/etc/react-rsbuild-plugin.api.md
+++ b/packages/rspeedy/plugin-react/etc/react-rsbuild-plugin.api.md
@@ -33,6 +33,7 @@ export interface PluginReactLynxOptions {
enableNewGesture?: boolean;
enableParallelElement?: boolean;
enableRemoveCSSScope?: boolean | undefined;
+ enableSSR?: boolean;
engineVersion?: string;
// @alpha
experimental_isLazyBundle?: boolean;
diff --git a/packages/rspeedy/plugin-react/src/entry.ts b/packages/rspeedy/plugin-react/src/entry.ts
index 0daa2c10f6..c4d00c3c87 100644
--- a/packages/rspeedy/plugin-react/src/entry.ts
+++ b/packages/rspeedy/plugin-react/src/entry.ts
@@ -50,6 +50,7 @@ export function applyEntry(
enableParallelElement,
enableRemoveCSSScope,
firstScreenSyncTiming,
+ enableSSR,
pipelineSchedulerConfig,
removeDescendantSelectorScope,
targetSdkVersion,
@@ -220,6 +221,7 @@ export function applyEntry(
disableCreateSelectorQueryIncompatibleWarning: compat
?.disableCreateSelectorQueryIncompatibleWarning ?? false,
firstScreenSyncTiming,
+ enableSSR,
mainThreadChunks,
experimental_isLazyBundle,
}])
diff --git a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts
index 31cef32a61..030f142c9a 100644
--- a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts
+++ b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts
@@ -226,6 +226,15 @@ export interface PluginReactLynxOptions {
*/
firstScreenSyncTiming?: 'immediately' | 'jsReady'
+ /**
+ * `enableSSR` enable Lynx SSR feature for this build.
+ *
+ * @defaultValue `false`
+ *
+ * @public
+ */
+ enableSSR?: boolean
+
/**
* The `jsx` option controls how JSX is transformed.
*/
@@ -330,6 +339,7 @@ export function pluginReactLynx(
enableParallelElement: true,
enableRemoveCSSScope: true,
firstScreenSyncTiming: 'immediately',
+ enableSSR: false,
jsx: undefined,
pipelineSchedulerConfig: 0x00010000,
removeDescendantSelectorScope: true,
diff --git a/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md b/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md
index 9aa989dd41..16d2c4d211 100644
--- a/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md
+++ b/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md
@@ -40,6 +40,7 @@ export class ReactWebpackPlugin {
// @public
export interface ReactWebpackPluginOptions {
disableCreateSelectorQueryIncompatibleWarning?: boolean | undefined;
+ enableSSR?: boolean;
// @alpha
experimental_isLazyBundle?: boolean;
firstScreenSyncTiming?: 'immediately' | 'jsReady';
diff --git a/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts b/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts
index d85329ded2..19493ce772 100644
--- a/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts
+++ b/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts
@@ -32,6 +32,11 @@ interface ReactWebpackPluginOptions {
*/
firstScreenSyncTiming?: 'immediately' | 'jsReady';
+ /**
+ * {@inheritdoc @lynx-dev/react-rsbuild-plugin#PluginReactLynxOptions.enableSSR}
+ */
+ enableSSR?: boolean;
+
/**
* The chunk names to be considered as main thread chunks.
*/
@@ -111,6 +116,7 @@ class ReactWebpackPlugin {
.freeze>({
disableCreateSelectorQueryIncompatibleWarning: false,
firstScreenSyncTiming: 'immediately',
+ enableSSR: false,
mainThreadChunks: [],
experimental_isLazyBundle: false,
});
@@ -156,6 +162,7 @@ class ReactWebpackPlugin {
__FIRST_SCREEN_SYNC_TIMING__: JSON.stringify(
options.firstScreenSyncTiming,
),
+ __ENABLE_SSR__: JSON.stringify(options.enableSSR),
__DISABLE_CREATE_SELECTOR_QUERY_INCOMPATIBLE_WARNING__: JSON.stringify(
options.disableCreateSelectorQueryIncompatibleWarning,
),
From 19761f99694e93b5058ebbc489377732c3176037 Mon Sep 17 00:00:00 2001
From: Zhiyuan Hong <28915578+hzy@users.noreply.github.com>
Date: Thu, 20 Mar 2025 23:50:28 +0800
Subject: [PATCH 2/2] Update calledByNative.ts
Co-authored-by: Qingyu Wang <40660121+colinaaa@users.noreply.github.com>
Signed-off-by: Zhiyuan Hong <28915578+hzy@users.noreply.github.com>
---
packages/react/runtime/src/lynx/calledByNative.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/react/runtime/src/lynx/calledByNative.ts b/packages/react/runtime/src/lynx/calledByNative.ts
index feef3ad696..d5c02da961 100644
--- a/packages/react/runtime/src/lynx/calledByNative.ts
+++ b/packages/react/runtime/src/lynx/calledByNative.ts
@@ -38,7 +38,7 @@ function ssrEncode() {
function ssrHydrate(info: string) {
const nativePage = __GetPageElement();
if (!nativePage) {
- throw 'SSR Hydration Failed! Please check if the SSR content loaded successfully!';
+ throw new Error('SSR Hydration Failed! Please check if the SSR content loaded successfully!');
}
const refsMap = __GetTemplateParts(nativePage);