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);