Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/cool-geckos-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@lynx-js/web-mainthread-apis": patch
"@lynx-js/web-core": patch
"@lynx-js/web-core-server": patch
---

feat: support SSR for all-on-ui
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ jobs:
env:
- ENABLE_SSR=true
- TEST_FP_ONLY=true
- name: SSR-ALL-ON-UI
env:
- ENABLE_SSR=true
with:
runs-on: lynx-ubuntu-24.04-xlarge
is-web: true
Expand Down
10 changes: 10 additions & 0 deletions packages/web-platform/web-constants/src/types/SSR.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { WebFiberElementImpl } from './Element.js';
import type { AddEventPAPI } from './MainThreadGlobalThis.js';

export type SSREventReplayInfo = [
Expand All @@ -12,6 +13,15 @@ export type SSRDumpInfo = {
events: SSREventReplayInfo[];
};

export interface SSRHydrateInfo extends SSRDumpInfo {
/** WeakRef<Element> */
lynxUniqueIdToElement: WeakRef<WebFiberElementImpl>[];
/** for cssog */
lynxUniqueIdToStyleRulesIndex: number[];
// @ts-expect-error
cardStyleElement: HTMLStyleElement | null;
}

export type SSRDehydrateHooks = {
__AddEvent: AddEventPAPI;
};
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const mainThreadInjectVars = [
'__GetTemplateParts',
'__MarkPartElement',
'__MarkTemplateElement',
'__GetPageElement',
];

const backgroundInjectVars = [
Expand Down
44 changes: 26 additions & 18 deletions packages/web-platform/web-core/src/apis/LynxView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
lynxDisposedAttribute,
lynxTagAttribute,
type Cloneable,
type SSRDumpInfo,
type I18nResourceTranslationOptions,
type InitI18nResources,
type LynxTemplate,
Expand Down Expand Up @@ -300,6 +301,7 @@ export class LynxView extends HTMLElement {
* reload the current page
*/
reload() {
this.removeAttribute('ssr');
this.#render();
}

Expand Down Expand Up @@ -403,6 +405,7 @@ export class LynxView extends HTMLElement {
this.#rendering = true;
queueMicrotask(() => {
this.#rendering = false;
const ssrData = this.getAttribute('ssr');
if (this.#instance) {
this.disconnectedCallback();
}
Expand Down Expand Up @@ -468,26 +471,31 @@ export class LynxView extends HTMLElement {
},
customTemplateLoader: this.customTemplateLoader,
},
ssr: ssrData
? JSON.parse(decodeURI(ssrData)) as SSRDumpInfo
: undefined,
});
this.#instance = lynxView;
const styleElement = document.createElement('style');
this.shadowRoot!.append(styleElement);
const styleSheet = styleElement.sheet!;
for (const rule of inShadowRootStyles) {
styleSheet.insertRule(rule);
}
for (const rule of this.injectStyleRules) {
styleSheet.insertRule(rule);
}
const injectHeadLinks =
this.getAttribute('inject-head-links') !== 'false';
if (injectHeadLinks) {
document.head.querySelectorAll('link[rel="stylesheet"]').forEach(
(linkElement) => {
const href = (linkElement as HTMLLinkElement).href;
styleSheet.insertRule(`@import url("${href}");`);
},
);
if (!ssrData) {
const styleElement = document.createElement('style');
this.shadowRoot!.append(styleElement);
const styleSheet = styleElement.sheet!;
for (const rule of inShadowRootStyles) {
styleSheet.insertRule(rule);
}
for (const rule of this.injectStyleRules) {
styleSheet.insertRule(rule);
}
const injectHeadLinks =
this.getAttribute('inject-head-links') !== 'false';
if (injectHeadLinks) {
document.head.querySelectorAll('link[rel="stylesheet"]').forEach(
(linkElement) => {
const href = (linkElement as HTMLLinkElement).href;
styleSheet.insertRule(`@import url("${href}");`);
},
);
}
}
}
});
Expand Down
4 changes: 4 additions & 0 deletions packages/web-platform/web-core/src/apis/createLynxView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import type {
Cloneable,
SSRDumpInfo,
I18nResourceTranslationOptions,
InitI18nResources,
NapiModulesMap,
Expand Down Expand Up @@ -32,6 +33,7 @@ export interface LynxViewConfigs {
lynxGroupId: number | undefined;
threadStrategy: 'all-on-ui' | 'multi-thread';
initI18nResources: InitI18nResources;
ssr?: SSRDumpInfo;
}

export interface LynxView {
Expand Down Expand Up @@ -62,6 +64,7 @@ export function createLynxView(configs: LynxViewConfigs): LynxView {
lynxGroupId,
threadStrategy = 'multi-thread',
initI18nResources,
ssr,
} = configs;
return startUIThread(
templateUrl,
Expand All @@ -82,5 +85,6 @@ export function createLynxView(configs: LynxViewConfigs): LynxView {
lynxGroupId,
threadStrategy,
callbacks,
ssr,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
I18nResources,
type InitI18nResources,
type Cloneable,
lynxUniqueIdAttribute,
type SSRDumpInfo,
} from '@lynx-js/web-constants';
import { Rpc } from '@lynx-js/web-worker-rpc';
import { dispatchLynxViewEvent } from '../utils/dispatchLynxViewEvent.js';
Expand All @@ -34,6 +36,7 @@ export function createRenderAllOnUI(
callbacks: {
onError?: (err: Error, release: string, fileName: string) => void;
},
ssrDumpInfo: SSRDumpInfo | undefined,
) {
if (!globalThis.module) {
Object.assign(globalThis, { module: {} });
Expand Down Expand Up @@ -67,8 +70,49 @@ export function createRenderAllOnUI(
);
let mtsGlobalThis!: MainThreadGlobalThis;
const start = async (configs: StartMainThreadContextConfig) => {
const mainThreadRuntime = startMainThread(configs);
mtsGlobalThis = await mainThreadRuntime;
if (ssrDumpInfo) {
const lynxUniqueIdToElement: WeakRef<HTMLElement>[] = [];
const allLynxElements = shadowRoot.querySelectorAll<HTMLElement>(
`[${lynxUniqueIdAttribute}]`,
);
const length = allLynxElements.length;
for (let ii = 0; ii < length; ii++) {
const element = allLynxElements[ii]! as HTMLElement;
const lynxUniqueId = Number(
element.getAttribute(lynxUniqueIdAttribute)!,
);
lynxUniqueIdToElement[lynxUniqueId] = new WeakRef<HTMLElement>(element);
}
const hydrateStyleElement = shadowRoot.querySelector(
`style:nth-of-type(2)`,
) as HTMLStyleElement | null;
const styleSheet = hydrateStyleElement?.sheet;
const lynxUniqueIdToStyleRulesIndex: number[] = [];
const cssRulesLength = styleSheet?.cssRules.length ?? 0;
for (let ii = 0; ii < cssRulesLength; ii++) {
const cssRule = styleSheet?.cssRules[ii];
if (cssRule?.constructor.name === 'CSSStyleRule') {
const lynxUniqueId = parseFloat(
(cssRule as CSSStyleRule).selectorText.substring(
lynxUniqueIdAttribute.length + 3, // skip `[`, `="`
),
);
if (lynxUniqueId !== undefined && !isNaN(lynxUniqueId)) {
lynxUniqueIdToStyleRulesIndex[lynxUniqueId] = ii;
}
}
}

mtsGlobalThis = await startMainThread(configs, {
// @ts-expect-error
lynxUniqueIdToElement: lynxUniqueIdToElement,
lynxUniqueIdToStyleRulesIndex,
...ssrDumpInfo,
cardStyleElement: hydrateStyleElement,
});
} else {
mtsGlobalThis = await startMainThread(configs);
}
};
const updateDataMainThread: RpcCallType<typeof updateDataEndpoint> = async (
...args
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type Cloneable,
dispatchMarkTiming,
flushMarkTiming,
type SSRDumpInfo,
} from '@lynx-js/web-constants';
import { loadTemplate } from '../utils/loadTemplate.js';
import { createUpdateData } from './crossThreadHandlers/createUpdateData.js';
Expand All @@ -35,6 +36,7 @@ export function startUIThread(
lynxGroupId: number | undefined,
threadStrategy: 'all-on-ui' | 'multi-thread',
callbacks: StartUIThreadCallbacks,
ssr?: SSRDumpInfo,
): LynxView {
const createLynxStartTiming = performance.now() + performance.timeOrigin;
const allOnUI = threadStrategy === 'all-on-ui';
Expand Down Expand Up @@ -79,6 +81,7 @@ export function startUIThread(
markTimingInternal,
flushMarkTimingInternal,
callbacks,
ssr,
)
: createRenderMultiThread(
/* main-to-ui rpc*/ mainThreadRpc,
Expand Down
2 changes: 1 addition & 1 deletion packages/web-platform/web-core/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"rootDir": "./src",
"outDir": "./dist",
"lib": ["DOM", "ESNext", "WebWorker"],
"noUnusedParameters": false,
"noUnusedParameters": true,
"noImplicitReturns": false,
},
"include": ["src"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
type MinimalRawEventObject,
type I18nResourceTranslationOptions,
lynxDisposedAttribute,
type SSRHydrateInfo,
type SSRDehydrateHooks,
} from '@lynx-js/web-constants';
import { globalMuteableVars } from '@lynx-js/web-constants';
Expand Down Expand Up @@ -150,14 +151,13 @@ export interface MainThreadRuntimeConfig {
& Pick<Element, 'append' | 'addEventListener'>
& Partial<Pick<Element, 'querySelectorAll'>>;
jsContext: LynxContextEventTarget;
ssrHydrateInfo?: SSRHydrateInfo;
ssrHooks?: SSRDehydrateHooks;
}

export function createMainThreadGlobalThis(
config: MainThreadRuntimeConfig,
): MainThreadGlobalThis {
let pageElement!: WebFiberElementImpl;
let uniqueIdInc = 1;
let timingFlags: string[] = [];
let renderPage: MainThreadGlobalThis['renderPage'];
const {
Expand All @@ -168,12 +168,19 @@ export function createMainThreadGlobalThis(
rootDom,
globalProps,
styleInfo,
ssrHydrateInfo,
ssrHooks,
} = config;
const lynxUniqueIdToElement: WeakRef<WebFiberElementImpl>[] = [];
const lynxUniqueIdToElement: WeakRef<WebFiberElementImpl>[] =
ssrHydrateInfo?.lynxUniqueIdToElement ?? [];
const lynxUniqueIdToStyleRulesIndex: number[] =
ssrHydrateInfo?.lynxUniqueIdToStyleRulesIndex ?? [];
const elementToRuntimeInfoMap: WeakMap<WebFiberElementImpl, LynxRuntimeInfo> =
new WeakMap();
const lynxUniqueIdToStyleRulesIndex: number[] = [];

let pageElement: WebFiberElementImpl | undefined = lynxUniqueIdToElement[1]
?.deref();
let uniqueIdInc = lynxUniqueIdToElement.length || 1;
/**
* for "update" the globalThis.val in the main thread
*/
Expand All @@ -197,13 +204,19 @@ export function createMainThreadGlobalThis(
const cssOGInfo: CssOGInfo = pageConfig.enableCSSSelector
? {}
: genCssOGInfo(styleInfo);
const cardStyleElement = callbacks.createElement('style');
cardStyleElement.innerHTML = genCssContent(
styleInfo,
pageConfig,
);
// @ts-expect-error
rootDom.append(cardStyleElement);
let cardStyleElement: HTMLStyleElement;
if (ssrHydrateInfo?.cardStyleElement) {
cardStyleElement = ssrHydrateInfo.cardStyleElement;
} else {
cardStyleElement = callbacks.createElement(
'style',
) as unknown as HTMLStyleElement;
cardStyleElement.innerHTML = genCssContent(
styleInfo,
pageConfig,
);
rootDom.append(cardStyleElement);
}
const cardStyleElementSheet =
(cardStyleElement as unknown as HTMLStyleElement).sheet!;
const updateCssOGStyle: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type I18nResources,
dispatchI18nResourceEndpoint,
type Cloneable,
type SSRHydrateInfo,
type SSRDehydrateHooks,
} from '@lynx-js/web-constants';
import { registerCallLepusMethodHandler } from './crossThreadHandlers/registerCallLepusMethodHandler.js';
Expand Down Expand Up @@ -69,6 +70,7 @@ export function prepareMainThreadAPIs(
markTimingInternal('lepus_execute_start');
async function startMainThread(
config: StartMainThreadContextConfig,
ssrHydrateInfo?: SSRHydrateInfo,
): Promise<MainThreadGlobalThis> {
let isFp = true;
const {
Expand Down Expand Up @@ -117,6 +119,7 @@ export function prepareMainThreadAPIs(
styleInfo,
lepusCode: lepusCodeLoaded,
rootDom,
ssrHydrateInfo,
ssrHooks,
callbacks: {
mainChunkReady: () => {
Expand Down Expand Up @@ -159,8 +162,21 @@ export function prepareMainThreadAPIs(
napiModulesMap,
browserConfig,
});
mtsGlobalThis.renderPage!(initData);
mtsGlobalThis.__FlushElementTree(undefined, {});
if (!ssrHydrateInfo) {
mtsGlobalThis.renderPage!(initData);
mtsGlobalThis.__FlushElementTree(undefined, {});
} else {
// replay the hydrate event
for (const event of ssrHydrateInfo.events) {
const uniqueId = event[0];
const element = ssrHydrateInfo.lynxUniqueIdToElement[uniqueId]
?.deref();
if (element) {
mtsGlobalThis.__AddEvent(element, event[1], event[2], event[3]);
}
}
mtsGlobalThis.ssrHydrate?.(ssrHydrateInfo.ssrEncodeData);
}
},
flushElementTree: async (
options,
Expand Down
5 changes: 4 additions & 1 deletion packages/web-platform/web-tests/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ const testMatch: string | undefined = (() => {
if (testFPOnly) {
return '**/fp-only.spec.ts';
}
if (enableMultiThread || enableSSR) {
if (enableSSR) {
return '**/react.{test,spec}.ts';
}
if (enableMultiThread) {
return '**/{react,web-core,main-thread-apis}.{test,spec}.ts';
}
return undefined;
Expand Down
Loading
Loading