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
5 changes: 5 additions & 0 deletions .changeset/evil-lights-say.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/web-core-server": patch
---

feat: dump dehydrate string with shadow root template
5 changes: 5 additions & 0 deletions .changeset/stale-zebras-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/offscreen-document": patch
---

revert get() innerHTML
17 changes: 17 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = 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,
Expand All @@ -28,6 +40,10 @@ export class OffscreenCSSStyleDeclaration {
value: value,
priority: priority,
});
this[styleMapSymbol].set(
property,
priority ? `${value} !important` : value,
);
}

removeProperty(property: string): void {
Expand All @@ -36,5 +52,6 @@ export class OffscreenCSSStyleDeclaration {
uid: this._parent[uniqueId],
property,
});
this[styleMapSymbol].delete(property);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>,
) => string)
| string;

function getInnerHTMLImpl(
buffer: string[],
element: OffscreenElement,
shadowrootTemplates: Record<string, ShadowrootTemplates>,
tagTransformMap: Record<string, string> = {},
): 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])) {
Expand All @@ -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('<template shadowrootmode="open">', template, '</template>');
}
for (const child of element.children) {
getInnerHTMLImpl(buffer, child as OffscreenElement);
getInnerHTMLImpl(
buffer,
child as OffscreenElement,
shadowrootTemplates,
tagTransformMap,
);
}
buffer.push('</');
buffer.push(tagName);
buffer.push('>');
}

export function dumpHTMLString(
element: OffscreenDocument,
shadowrootTemplates: Record<string, ShadowrootTemplates>,
tagTransformMap: Record<string, string>,
): string {
const buffer: string[] = [];
for (const child of element.children) {
getInnerHTMLImpl(
buffer,
child as OffscreenElement,
shadowrootTemplates,
tagTransformMap,
);
}
return buffer.join('');
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 2 additions & 0 deletions packages/web-platform/web-core-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*"
}
Expand Down
77 changes: 76 additions & 1 deletion packages/web-platform/web-core-server/src/createLynxView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -14,8 +30,40 @@ interface LynxViewConfig extends
>
{
templateName?: string;
hydrateUrl: string;
injectStyles: string;
overrideElemenTemplates?: Record<
string,
((attributes: Record<string, string>) => string) | string
>;
overrideTagTransformMap?: Record<string, string>;
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,
) {
Expand All @@ -25,6 +73,11 @@ export async function createLynxView(
tagMap,
initData,
globalProps,
overrideElemenTemplates = {},
overrideTagTransformMap = {},
hydrateUrl,
autoSize,
injectStyles,
} = config;

const mainToUIChannel = new MessageChannel();
Expand Down Expand Up @@ -55,9 +108,31 @@ export async function createLynxView(
},
);

const elementTemplates = {
...builtinElementTemplates,
...overrideElemenTemplates,
};
const tagTransformMap = {
...builtinTagTransformMap,
...overrideTagTransformMap,
};

async function renderToString(): Promise<string> {
await firstPaintReadyPromise;
return offscreenDocument.innerHTML;
const innerShadowRootHTML = dumpHTMLString(
offscreenDocument,
elementTemplates,
tagTransformMap,
);
return `
<lynx-view url="${hydrateUrl}" ssr ${
autoSize ? 'height="auto" width="auto"' : ''
}>
<template shadowrootmode="open">
<style>${injectStyles}</style>
${innerShadowRootHTML}
</template>
</lynx-view>`;
}
return {
renderToString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions packages/web-platform/web-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 22 additions & 2 deletions packages/web-platform/web-tests/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
Loading
Loading