diff --git a/.changeset/evil-lights-say.md b/.changeset/evil-lights-say.md new file mode 100644 index 0000000000..f9c686f231 --- /dev/null +++ b/.changeset/evil-lights-say.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/web-core-server": patch +--- + +feat: dump dehydrate string with shadow root template diff --git a/.changeset/stale-zebras-add.md b/.changeset/stale-zebras-add.md new file mode 100644 index 0000000000..20f935e48b --- /dev/null +++ b/.changeset/stale-zebras-add.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/offscreen-document": patch +--- + +revert get() innerHTML diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e894165244..d82b996a37 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -100,6 +100,23 @@ jobs: export ALL_ON_UI=true pnpm --filter @lynx-js/web-tests run test --reporter='github,dot,junit,html' pnpm --filter @lynx-js/web-tests run coverage:ci + playwright-linux-fp-only: + needs: build + uses: ./.github/workflows/workflow-test.yml + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + runs-on: lynx-ubuntu-24.04-xlarge + is-web: true + codecov-flags: "e2e,fp-only" + run: | + export NODE_OPTIONS="--max-old-space-size=32768" + export PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-report.junit.xml + export ENABLE_SSR=true + export TEST_FP_ONLY=true + pnpm --filter @lynx-js/web-tests run build:cases:ssr + pnpm --filter @lynx-js/web-tests run test --reporter='github,dot,junit,html' + pnpm --filter @lynx-js/web-tests run coverage:ci test-api: needs: build uses: ./.github/workflows/workflow-test.yml diff --git a/packages/web-platform/offscreen-document/src/webworker/OffscreenCSSStyleDeclaration.ts b/packages/web-platform/offscreen-document/src/webworker/OffscreenCSSStyleDeclaration.ts index f7f9219c9c..37ab72d661 100644 --- a/packages/web-platform/offscreen-document/src/webworker/OffscreenCSSStyleDeclaration.ts +++ b/packages/web-platform/offscreen-document/src/webworker/OffscreenCSSStyleDeclaration.ts @@ -6,16 +6,28 @@ import { operations } from './OffscreenDocument.js'; import { OperationType } from '../types/ElementOperation.js'; import { uniqueId } from './OffscreenNode.js'; +export const styleMapSymbol = Symbol('styleMapSymbol'); export class OffscreenCSSStyleDeclaration { /** * @private */ private readonly _parent: OffscreenElement; + readonly [styleMapSymbol]: Map = new Map(); constructor(parent: OffscreenElement) { this._parent = parent; } + set cssText(value: string) { + this[styleMapSymbol].clear(); + this._parent[ancestorDocument][operations].push({ + type: OperationType['SetAttribute'], + uid: this._parent[uniqueId], + key: 'style', + value: value, + }); + } + setProperty( property: string, value: string, @@ -28,6 +40,10 @@ export class OffscreenCSSStyleDeclaration { value: value, priority: priority, }); + this[styleMapSymbol].set( + property, + priority ? `${value} !important` : value, + ); } removeProperty(property: string): void { @@ -36,5 +52,6 @@ export class OffscreenCSSStyleDeclaration { uid: this._parent[uniqueId], property, }); + this[styleMapSymbol].delete(property); } } diff --git a/packages/web-platform/offscreen-document/src/webworker/OffscreenDocument.ts b/packages/web-platform/offscreen-document/src/webworker/OffscreenDocument.ts index 813c456b5e..a6834c83c8 100644 --- a/packages/web-platform/offscreen-document/src/webworker/OffscreenDocument.ts +++ b/packages/web-platform/offscreen-document/src/webworker/OffscreenDocument.ts @@ -5,6 +5,7 @@ import { OperationType, type ElementOperation, } from '../types/ElementOperation.js'; +import { styleMapSymbol } from './OffscreenCSSStyleDeclaration.js'; import { _attributes, OffscreenElement } from './OffscreenElement.js'; import { eventPhase, @@ -131,18 +132,24 @@ export class OffscreenDocument extends OffscreenNode { } } }; - - get innerHTML(): string { - const buffer: string[] = []; - for (const child of this.children) { - getInnerHTMLImpl(buffer, child as OffscreenElement); - } - return buffer.join(''); - } } -function getInnerHTMLImpl(buffer: string[], element: OffscreenElement): void { - const tagName = element.tagName.toLowerCase(); +type ShadowrootTemplates = + | (( + attributes: Record, + ) => string) + | string; + +function getInnerHTMLImpl( + buffer: string[], + element: OffscreenElement, + shadowrootTemplates: Record, + tagTransformMap: Record = {}, +): void { + let tagName = element.tagName.toLowerCase(); + if (tagTransformMap[tagName]) { + tagName = tagTransformMap[tagName]!; + } buffer.push('<'); buffer.push(tagName); for (const [key, value] of Object.entries(element[_attributes])) { @@ -153,11 +160,46 @@ function getInnerHTMLImpl(buffer: string[], element: OffscreenElement): void { buffer.push('"'); } + const cssText = Array.from(element.style[styleMapSymbol].entries()) + .map(([key, value]) => `${key}: ${value};`).join(';'); + if (cssText) { + buffer.push(' style="', cssText, '"'); + } + buffer.push('>'); + const templateImpl = shadowrootTemplates[tagName]; + if (templateImpl) { + const template = typeof templateImpl === 'function' + ? templateImpl(element[_attributes]) + : templateImpl; + buffer.push(''); + } for (const child of element.children) { - getInnerHTMLImpl(buffer, child as OffscreenElement); + getInnerHTMLImpl( + buffer, + child as OffscreenElement, + shadowrootTemplates, + tagTransformMap, + ); } buffer.push(''); } + +export function dumpHTMLString( + element: OffscreenDocument, + shadowrootTemplates: Record, + tagTransformMap: Record, +): string { + const buffer: string[] = []; + for (const child of element.children) { + getInnerHTMLImpl( + buffer, + child as OffscreenElement, + shadowrootTemplates, + tagTransformMap, + ); + } + return buffer.join(''); +} diff --git a/packages/web-platform/offscreen-document/src/webworker/index.ts b/packages/web-platform/offscreen-document/src/webworker/index.ts index 909e102dca..8933d3188b 100644 --- a/packages/web-platform/offscreen-document/src/webworker/index.ts +++ b/packages/web-platform/offscreen-document/src/webworker/index.ts @@ -1,7 +1,11 @@ // Copyright 2023 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. -export { OffscreenDocument, _onEvent } from './OffscreenDocument.js'; +export { + OffscreenDocument, + _onEvent, + dumpHTMLString, +} from './OffscreenDocument.js'; export type * from './OffscreenEvent.js'; export type * from './OffscreenElement.js'; export type * from './OffscreenCSSStyleDeclaration.js'; diff --git a/packages/web-platform/web-core-server/package.json b/packages/web-platform/web-core-server/package.json index 2cbcd617c6..5650d35c01 100644 --- a/packages/web-platform/web-core-server/package.json +++ b/packages/web-platform/web-core-server/package.json @@ -22,7 +22,9 @@ "README.md" ], "devDependencies": { + "@lynx-js/offscreen-document": "workspace:*", "@lynx-js/web-constants": "workspace:*", + "@lynx-js/web-elements-template": "workspace:*", "@lynx-js/web-worker-rpc": "workspace:*", "@lynx-js/web-worker-runtime": "workspace:*" } diff --git a/packages/web-platform/web-core-server/src/createLynxView.ts b/packages/web-platform/web-core-server/src/createLynxView.ts index fcbd11cb9f..98455364f7 100644 --- a/packages/web-platform/web-core-server/src/createLynxView.ts +++ b/packages/web-platform/web-core-server/src/createLynxView.ts @@ -6,6 +6,22 @@ import { import { Rpc } from '@lynx-js/web-worker-rpc'; import { startMainThread } from '@lynx-js/web-worker-runtime'; import { loadTemplate } from './utils/loadTemplate.js'; +import { dumpHTMLString } from '@lynx-js/offscreen-document/webworker'; +import { + templateScrollView, + templateXAudioTT, + templateXImage, + templateFilterImage, + templateXInput, + templateXList, + templateXOverlayNg, + templateXRefreshView, + templateXSwiper, + templateXText, + templateInlineImage, + templateXTextarea, + templateXViewpageNg, +} from '@lynx-js/web-elements-template'; interface LynxViewConfig extends Pick< @@ -14,8 +30,40 @@ interface LynxViewConfig extends > { templateName?: string; + hydrateUrl: string; + injectStyles: string; + overrideElemenTemplates?: Record< + string, + ((attributes: Record) => string) | string + >; + overrideTagTransformMap?: Record; + autoSize?: boolean; } +const builtinElementTemplates = { + 'scroll-view': templateScrollView, + 'x-audio-tt': templateXAudioTT, + 'x-image': templateXImage, + 'filter-image': templateFilterImage, + 'x-input': templateXInput, + 'x-list': templateXList, + 'x-overlay-ng': templateXOverlayNg, + 'x-refresh-view': templateXRefreshView, + 'x-swiper': templateXSwiper, + 'x-text': templateXText, + 'inline-image': templateInlineImage, + 'x-textarea': templateXTextarea, + 'x-viewpage-ng': templateXViewpageNg, +}; +const builtinTagTransformMap = { + 'page': 'div', + 'view': 'x-view', + 'text': 'x-text', + 'image': 'x-image', + 'list': 'x-list', + 'svg': 'x-svg', +}; + export async function createLynxView( config: LynxViewConfig, ) { @@ -25,6 +73,11 @@ export async function createLynxView( tagMap, initData, globalProps, + overrideElemenTemplates = {}, + overrideTagTransformMap = {}, + hydrateUrl, + autoSize, + injectStyles, } = config; const mainToUIChannel = new MessageChannel(); @@ -55,9 +108,31 @@ export async function createLynxView( }, ); + const elementTemplates = { + ...builtinElementTemplates, + ...overrideElemenTemplates, + }; + const tagTransformMap = { + ...builtinTagTransformMap, + ...overrideTagTransformMap, + }; + async function renderToString(): Promise { await firstPaintReadyPromise; - return offscreenDocument.innerHTML; + const innerShadowRootHTML = dumpHTMLString( + offscreenDocument, + elementTemplates, + tagTransformMap, + ); + return ` + + + `; } return { renderToString, diff --git a/packages/web-platform/web-mainthread-apis/src/elementAPI/style/styleFunctions.ts b/packages/web-platform/web-mainthread-apis/src/elementAPI/style/styleFunctions.ts index cf1810e06d..1dcb482f73 100644 --- a/packages/web-platform/web-mainthread-apis/src/elementAPI/style/styleFunctions.ts +++ b/packages/web-platform/web-mainthread-apis/src/elementAPI/style/styleFunctions.ts @@ -105,10 +105,15 @@ export function createStyleFunctions( value, ]), ); - const transformedStyleStr = transformedStyle.map(( - [property, value], - ) => `${property}:${value};`).join(''); - element.setAttribute('style', transformedStyleStr); + const style = element.style; + style.cssText = ''; + for (const [property, value] of transformedStyle) { + const important = value.includes('!important'); + const cleanValue = important + ? value.replace('!important', '').trim() + : value; + style.setProperty(property, cleanValue, important ? 'important' : ''); + } } function __SetCSSId( diff --git a/packages/web-platform/web-tests/package.json b/packages/web-platform/web-tests/package.json index b09b4bc6b1..d5f030a791 100644 --- a/packages/web-platform/web-tests/package.json +++ b/packages/web-platform/web-tests/package.json @@ -13,6 +13,7 @@ "bench": "vitest bench", "build": "pnpm dlx is-ci && pnpm build:cases || echo 'Skipping build:cases in non-CI environment'", "build:cases": "rm -rf dist && node ./scripts/generate-build-command.js", + "build:cases:ssr": "rm -rf dist && SSR=1 node ./scripts/generate-build-command.js", "coverage": "nyc report --cwd=$(realpath ../)", "coverage:ci": "nyc report --cwd=$(realpath ../) --reporter=lcov", "lh": "pnpm dlx @lhci/cli autorun", diff --git a/packages/web-platform/web-tests/playwright.config.ts b/packages/web-platform/web-tests/playwright.config.ts index 702a98b86c..2fd97f23ea 100644 --- a/packages/web-platform/web-tests/playwright.config.ts +++ b/packages/web-platform/web-tests/playwright.config.ts @@ -8,11 +8,31 @@ process.env['LIBGL_ALWAYS_SOFTWARE'] = 'true'; // https://github.com/microsoft/p process.env['GALLIUM_HUD_SCALE'] = '1'; const isCI = !!process.env.CI; const ALL_ON_UI = !!process.env.ALL_ON_UI; +const enableSSR = !!process.env.ENABLE_SSR; +const testFPOnly = !!process.env.TEST_FP_ONLY; const port = process.env.PORT ?? 3080; const workerLimit = process.env['cpu_limit'] ? Math.floor(parseFloat(process.env['cpu_limit']) / 2) : undefined; +const testMatch: string | undefined = (() => { + if (testFPOnly) { + return '**/fp-only.spec.ts'; + } + if (ALL_ON_UI || enableSSR) { + return '**/{react,web-core}.{test,spec}.ts'; + } + return undefined; +})(); + +const testIgnore: string[] = (() => { + const ignore = ['**vitest**']; + if (isCI && !testFPOnly) { + ignore.push('**/fp-only.spec.ts'); // fp-only tests has its own test steps + } + return ignore; +})(); + /** * Read environment variables from file. * https://github.com/motdotla/dotenv @@ -26,8 +46,8 @@ export default defineConfig({ /** global timeout https://playwright.dev/docs/test-timeouts#global-timeout */ globalTimeout: 20 * 60 * 1000, testDir: './tests', - testMatch: ALL_ON_UI ? '**/{react,web-core}.{test,spec}.ts' : undefined, - testIgnore: '**vitest**', + testMatch, + testIgnore, /* Run tests in files in parallel */ fullyParallel: true, workers: isCI ? workerLimit : undefined, diff --git a/packages/web-platform/web-tests/rspack.config.js b/packages/web-platform/web-tests/rspack.config.js index 5630b319f5..eaee8b5986 100644 --- a/packages/web-platform/web-tests/rspack.config.js +++ b/packages/web-platform/web-tests/rspack.config.js @@ -2,8 +2,10 @@ // 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 path from 'node:path'; +import { readFile } from 'node:fs/promises'; import rspack from '@rspack/core'; import { fileURLToPath } from 'node:url'; +import { genHtml } from './server.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -17,6 +19,7 @@ const config = { 'main-thread-test': './shell-project/mainthread-test.ts', 'rpc-test': './shell-project/rpc-test/index.ts', 'web-core': './shell-project/web-core.ts', + 'fp-only': './shell-project/fp-only.ts', }, output: { filename: '[name].js', @@ -75,7 +78,6 @@ const config = { scriptLoading: 'module', filename: 'rpc-test.html', }), - new rspack.HtmlRspackPlugin({ title: 'lynx-for-web-core-test', meta: { @@ -91,6 +93,21 @@ const config = { scriptLoading: 'module', filename: 'web-core.html', }), + new rspack.HtmlRspackPlugin({ + title: 'fp-only-test', + meta: { + viewport: + 'width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no', + 'mobile-web-app-capable': 'yes', + 'apple-mobile-web-app-status-bar-style': 'default', + 'screen-orientation': 'portrait', + 'format-detection': 'telephone=no', + 'x5-orientation': 'portrait', + }, + chunks: ['fp-only'], + scriptLoading: 'module', + filename: 'fp-only.html', + }), ], mode: 'development', devServer: { @@ -104,6 +121,36 @@ const config = { devMiddleware: { writeToDisk: true, }, + setupMiddlewares: (middlewares) => { + middlewares.push({ + name: 'fp-only', + path: '/fp-only', + middleware: async (req, res, next) => { + try { + const html = await readFile( + path.join(__dirname, 'www', 'fp-only.html'), + 'utf-8', + ); + const casename = req.query.casename; + if (!casename) { + res.statusCode = 400; + res.send('casename is required'); + next(); + return; + } + + res.send(await genHtml(html, casename, 'fp-only')); + next(); + } catch (e) { + res.statusCode = 500; + console.error(e); + res.send(e.toString() + '\n' + e.stack?.toString()); + next(); + } + }, + }); + return middlewares; + }, watchFiles: ['./dist/**/*', './node_modules/@lynx-js/**/*'], static: [ { @@ -122,10 +169,6 @@ const config = { directory: path.join(__dirname, 'dist'), publicPath: '/dist', }, - { - directory: path.join(__dirname, 'tests', 'web-elements-plus'), - publicPath: '/web-element-plus-tests', - }, { directory: path.join(__dirname, 'tests', 'common.css'), publicPath: '/common.css', diff --git a/packages/web-platform/web-tests/server.js b/packages/web-platform/web-tests/server.js new file mode 100644 index 0000000000..7f1d52a647 --- /dev/null +++ b/packages/web-platform/web-tests/server.js @@ -0,0 +1,49 @@ +import { createLynxView } from '@lynx-js/web-core-server'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +export async function loadTemplate(caseName) { + return JSON.parse( + await readFile( + path.join(__dirname, 'dist', caseName, 'index.web.json'), + 'utf-8', + ), + ); +} +export async function SSR(rawTemplate, caseName, projectName = 'fp-only') { + const lynxView = await createLynxView({ + browserConfig: { + pixelRatio: 1, + pixelHeight: 800, + pixelWidth: 375, + }, + tagMap: {}, + initData: {}, + globalProps: {}, + template: rawTemplate, + templateName: caseName, + hydrateUrl: `/dist/${caseName}/index.web.json`, + injectStyles: `@import url("/${projectName}.css");`, + autoSize: true, + }); + const ssrHtml = await lynxView.renderToString(); + return ssrHtml; +} +export async function genTemplate(caseName, projectName = 'fp-only') { + const ssrHtml = await SSR( + await loadTemplate(caseName), + caseName, + projectName, + ); + return ssrHtml; +} +export async function genHtml(originalHTML, caseName, projectName) { + const ssrHtml = await genTemplate(caseName, projectName); + + return originalHTML.replace( + '', + '' + ssrHtml, + ); +} diff --git a/packages/web-platform/web-tests/shell-project/fp-only.ts b/packages/web-platform/web-tests/shell-project/fp-only.ts new file mode 100644 index 0000000000..58e054a336 --- /dev/null +++ b/packages/web-platform/web-tests/shell-project/fp-only.ts @@ -0,0 +1,3 @@ +import '@lynx-js/web-elements/index.css'; +import '@lynx-js/web-core/index.css'; +import './index.css'; diff --git a/packages/web-platform/web-tests/tests/__snapshots__/server.vitest.spec.ts.snap b/packages/web-platform/web-tests/tests/__snapshots__/server.vitest.spec.ts.snap index 34e03067c0..da7d41b816 100644 --- a/packages/web-platform/web-tests/tests/__snapshots__/server.vitest.spec.ts.snap +++ b/packages/web-platform/web-tests/tests/__snapshots__/server.vitest.spec.ts.snap @@ -1,11 +1,51 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`server-tests > basic-performance-div-10 1`] = `"
"`; +exports[`server-tests > basic-performance-div-10 1`] = ` +" + + + " +`; -exports[`server-tests > basic-performance-div-100 1`] = `"
"`; +exports[`server-tests > basic-performance-div-100 1`] = ` +" + + + " +`; -exports[`server-tests > basic-performance-div-1000 1`] = `"
"`; +exports[`server-tests > basic-performance-div-1000 1`] = ` +" + + + " +`; -exports[`server-tests > basic-performance-div-10000 1`] = `"
"`; +exports[`server-tests > basic-performance-div-10000 1`] = ` +" + + + " +`; -exports[`server-tests > basic-performance-nest-level-100 1`] = `"
"`; +exports[`server-tests > basic-performance-nest-level-100 1`] = ` +" + + + " +`; diff --git a/packages/web-platform/web-tests/tests/fp-only.spec.ts b/packages/web-platform/web-tests/tests/fp-only.spec.ts new file mode 100644 index 0000000000..9dbecbebfa --- /dev/null +++ b/packages/web-platform/web-tests/tests/fp-only.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from './coverage-fixture.js'; +import type { Page } from '@playwright/test'; + +const wait = async (ms: number) => { + await new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}; + +const diffScreenShot = async ( + page: Page, + caseName: string, + subcaseName: string, + label: string = 'index', + screenshotOptions?: Parameters< + ReturnType>['toHaveScreenshot'] + >[0], +) => { + await expect(page).toHaveScreenshot([ + `${caseName}`, + `${subcaseName}`, + `${label}.png`, + ], { + maxDiffPixelRatio: 0, + fullPage: true, + animations: 'allow', + ...screenshotOptions, + }); +}; + +const goto = async ( + page: Page, + testname: string, + hasDir?: boolean, +) => { + let url = `/fp-only?casename=${testname}`; + if (hasDir) { + url += '&hasdir=true'; + } + await page.goto(url, { + waitUntil: 'load', + }); + await page.evaluate(() => document.fonts.ready); +}; + +test.describe('SSR no Javascript tests', () => { + test('basic-pink-rect', async ({ page }, { title }) => { + await goto(page, title); + await wait(100); + const target = await page.locator('#target'); + await expect(target).toHaveCSS('height', '100px'); + await expect(target).toHaveCSS('width', '100px'); + await expect(target).toHaveCSS('background-color', 'rgb(255, 192, 203)'); + }); +}); diff --git a/packages/web-platform/web-tests/tests/main-thread-apis.test.ts b/packages/web-platform/web-tests/tests/main-thread-apis.test.ts index 7f2b70e37c..0597d478cc 100644 --- a/packages/web-platform/web-tests/tests/main-thread-apis.test.ts +++ b/packages/web-platform/web-tests/tests/main-thread-apis.test.ts @@ -505,23 +505,25 @@ test.describe('main thread api tests', () => { }); test('__SetInlineStyles', async ({ page }, { title }) => { - const ret = await page.evaluate(() => { - let root = globalThis.__CreateView(0); - globalThis.__SetInlineStyles(root, undefined); - globalThis.__SetInlineStyles(root, { + await page.evaluate(() => { + const root = globalThis.__CreatePage('page', 0); + let target = globalThis.__CreateView(0); + globalThis.__SetID(target, 'target'); + globalThis.__SetInlineStyles(target, undefined); + globalThis.__SetInlineStyles(target, { 'margin': '10px', 'marginTop': '20px', 'marginLeft': '30px', 'marginRight': '20px', 'marginBottom': '10px', }); - return { - inlineStyle: root.getAttribute('style'), - }; + globalThis.__AppendElement(root, target); + globalThis.__FlushElementTree(); }); - expect(ret.inlineStyle).toContain('20px'); - expect(ret.inlineStyle).toContain('30px'); - expect(ret.inlineStyle).toContain('10px'); + const targetStyle = await page.locator(`#target`).getAttribute('style'); + expect(targetStyle).toContain('20px'); + expect(targetStyle).toContain('30px'); + expect(targetStyle).toContain('10px'); }); test('__GetConfig__AddConfig', async ({ page }, { title }) => { diff --git a/packages/web-platform/web-tests/tests/react/commonConfig.ts b/packages/web-platform/web-tests/tests/react/commonConfig.ts index ec51765386..074bbae208 100644 --- a/packages/web-platform/web-tests/tests/react/commonConfig.ts +++ b/packages/web-platform/web-tests/tests/react/commonConfig.ts @@ -3,14 +3,20 @@ // LICENSE file in the root directory of this source tree. import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin'; import { type defineConfig } from '@lynx-js/rspeedy'; -const port = process.env.PORT ?? 3080; + +const port = process.env['PORT'] ?? 3080; +const enableSSR = !!process.env['ENABLE_SSR']; export function commonConfig( reactlynxConfigs?: Parameters[0], ): Parameters[0] { return { plugins: [ - pluginReactLynx(reactlynxConfigs), + pluginReactLynx({ + enableSSR, + firstScreenSyncTiming: enableSSR ? 'jsReady' : 'immediately', + ...reactlynxConfigs, + }), ], output: { filename: '[name]/index.[platform].json', diff --git a/packages/web-platform/web-tests/tests/server.bench.vitest.spec.ts b/packages/web-platform/web-tests/tests/server.bench.vitest.spec.ts index a6662fc6af..96e1e51631 100644 --- a/packages/web-platform/web-tests/tests/server.bench.vitest.spec.ts +++ b/packages/web-platform/web-tests/tests/server.bench.vitest.spec.ts @@ -1,48 +1,24 @@ -import { test, bench, expect, describe } from 'vitest'; -import { createLynxView } from '@lynx-js/web-core-server'; -import path from 'node:path'; -import { readFile } from 'fs/promises'; -import { fileURLToPath } from 'node:url'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +import { bench, expect, describe } from 'vitest'; +// @ts-expect-error +import { SSR, loadTemplate } from '../server.js'; -async function readTemplate(name: string) { - const file = await readFile( - path.join(__dirname, `../dist/${name}/index.web.json`), - 'utf-8', - ); - const rawTemplate = JSON.parse(file); - return rawTemplate as any; -} const cases = { - 'basic-performance-div-10000': await readTemplate( + 'basic-performance-div-10000': await loadTemplate( 'basic-performance-div-10000', ), - 'basic-performance-div-1000': await readTemplate( + 'basic-performance-div-1000': await loadTemplate( 'basic-performance-div-1000', ), - 'basic-performance-div-100': await readTemplate('basic-performance-div-100'), - 'basic-performance-div-10': await readTemplate('basic-performance-div-10'), - 'basic-performance-nest-level-100': await readTemplate( + 'basic-performance-div-100': await loadTemplate('basic-performance-div-100'), + 'basic-performance-div-10': await loadTemplate('basic-performance-div-10'), + 'basic-performance-nest-level-100': await loadTemplate( 'basic-performance-nest-level-100', ), }; -describe('server-tests', () => { - Object.entries(cases).forEach(([name, template]) => { - bench(name, async () => { - const lynxView = await createLynxView({ - browserConfig: { - pixelRatio: 1, - pixelWidth: 600, - pixelHeight: 800, - }, - tagMap: {}, - initData: {}, - globalProps: {}, - template, - templateName: name, - }); - await lynxView.renderToString(); +describe('server-tests', async () => { + for (const [testName, rawTemplate] of Object.entries(cases)) { + bench(testName, async () => { + const html = await SSR(rawTemplate, testName); }); - }); + } }); diff --git a/packages/web-platform/web-tests/tests/server.vitest.spec.ts b/packages/web-platform/web-tests/tests/server.vitest.spec.ts index feb43d9087..95a12d4b68 100644 --- a/packages/web-platform/web-tests/tests/server.vitest.spec.ts +++ b/packages/web-platform/web-tests/tests/server.vitest.spec.ts @@ -1,115 +1,20 @@ import { test, expect, describe } from 'vitest'; -import { createLynxView } from '@lynx-js/web-core-server'; -import path from 'node:path'; -import { readFile } from 'fs/promises'; -import { fileURLToPath } from 'node:url'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +// @ts-expect-error +import { genTemplate } from '../server.js'; -async function readTemplate(name: string) { - const file = await readFile( - path.join(__dirname, `../dist/${name}/index.web.json`), - 'utf-8', - ); - const rawTemplate = JSON.parse(file); - return rawTemplate as any; -} describe('server-tests', () => { - test('basic-performance-div-10000', async ({ task }) => { - const testName = task.name; - const rawTemplate = await readTemplate(testName); - const lynxView = await createLynxView({ - browserConfig: { - pixelRatio: 1, - pixelHeight: 800, - pixelWidth: 375, - }, - tagMap: {}, - initData: {}, - globalProps: {}, - template: rawTemplate, - templateName: testName, + for ( + const testName of [ + 'basic-performance-div-10000', + 'basic-performance-div-1000', + 'basic-performance-div-100', + 'basic-performance-div-10', + 'basic-performance-nest-level-100', + ] + ) { + test(testName, async () => { + const html = await genTemplate(testName); + expect(html).toMatchSnapshot(); }); - - const html = await lynxView.renderToString(); - expect(html).toMatchSnapshot(); - }); - - test('basic-performance-div-1000', async ({ task }) => { - const testName = task.name; - const rawTemplate = await readTemplate(testName); - const lynxView = await createLynxView({ - browserConfig: { - pixelRatio: 1, - pixelHeight: 800, - pixelWidth: 375, - }, - tagMap: {}, - initData: {}, - globalProps: {}, - template: rawTemplate, - templateName: testName, - }); - - const html = await lynxView.renderToString(); - expect(html).toMatchSnapshot(); - }); - test('basic-performance-div-100', async ({ task }) => { - const testName = task.name; - const rawTemplate = await readTemplate(testName); - const lynxView = await createLynxView({ - browserConfig: { - pixelRatio: 1, - pixelHeight: 800, - pixelWidth: 375, - }, - tagMap: {}, - initData: {}, - globalProps: {}, - template: rawTemplate, - templateName: testName, - }); - - const html = await lynxView.renderToString(); - expect(html).toMatchSnapshot(); - }); - test('basic-performance-div-10', async ({ task }) => { - const testName = task.name; - const rawTemplate = await readTemplate(testName); - const lynxView = await createLynxView({ - browserConfig: { - pixelRatio: 1, - pixelHeight: 800, - pixelWidth: 375, - }, - tagMap: {}, - initData: {}, - globalProps: {}, - template: rawTemplate, - templateName: testName, - }); - - const html = await lynxView.renderToString(); - expect(html).toMatchSnapshot(); - }); - - test('basic-performance-nest-level-100', async ({ task }) => { - const testName = task.name; - const rawTemplate = await readTemplate(testName); - const lynxView = await createLynxView({ - browserConfig: { - pixelRatio: 1, - pixelHeight: 800, - pixelWidth: 375, - }, - tagMap: {}, - initData: {}, - globalProps: {}, - template: rawTemplate, - templateName: testName, - }); - - const html = await lynxView.renderToString(); - expect(html).toMatchSnapshot(); - }); + } }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd2e5349db..dd94021b30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -664,9 +664,15 @@ importers: packages/web-platform/web-core-server: devDependencies: + '@lynx-js/offscreen-document': + specifier: workspace:* + version: link:../offscreen-document '@lynx-js/web-constants': specifier: workspace:* version: link:../web-constants + '@lynx-js/web-elements-template': + specifier: workspace:* + version: link:../web-elements-template '@lynx-js/web-worker-rpc': specifier: workspace:* version: link:../web-worker-rpc