diff --git a/.changeset/cold-parts-tan.md b/.changeset/cold-parts-tan.md
new file mode 100644
index 0000000000..853d812bb3
--- /dev/null
+++ b/.changeset/cold-parts-tan.md
@@ -0,0 +1,3 @@
+---
+
+---
diff --git a/.changeset/config.json b/.changeset/config.json
index 6c4d79c3ff..0f258b9fcb 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -37,7 +37,8 @@
},
"ignore": [
"@lynx-js/web-tests",
- "@lynx-js/example-react"
+ "@lynx-js/example-react",
+ "@lynx-js/web-core-wasm-e2e"
],
"changedFilePatterns": [
"src/**"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c8ff664245..6051506f5d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -92,6 +92,7 @@ jobs:
runs-on: lynx-custom-container
is-web: true
codecov-flags: "e2e"
+ web-report-path: "packages/web-platform/web-elements/playwright-report"
run: |
export NODE_OPTIONS="--max-old-space-size=32768"
export PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-report.junit.xml
@@ -128,6 +129,30 @@ jobs:
export PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-report.junit.xml
pnpm --filter @lynx-js/web-tests run test --reporter='github,dot,junit,html' --shard=${{ matrix.shard }}/4
pnpm --filter @lynx-js/web-tests run coverage:ci
+ web-core-wasm-e2e:
+ needs: build
+ uses: ./.github/workflows/workflow-test.yml
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ name: Playwright ${{ matrix.render }} (${{ matrix.shard }}/2)
+ strategy:
+ fail-fast: false
+ matrix:
+ render: [CSR]
+ shard: [1, 2]
+ with:
+ runs-on: lynx-custom-container
+ is-web: true
+ web-report-name: "playwright-${{ matrix.render }}-shard${{ matrix.shard }}"
+ web-report-path: "packages/web-platform/web-core-wasm-e2e/playwright-report"
+ codecov-flags: "e2e"
+ run: |
+ if [ "${{ matrix.render }}" = "SSR" ]; then
+ export ENABLE_SSR=true
+ fi
+ export NODE_OPTIONS="--max-old-space-size=32768"
+ export PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-report.junit.xml
+ pnpm --filter @lynx-js/web-core-wasm-e2e run test --reporter='github,dot,junit,html' --shard=${{ matrix.shard }}/4
test-api:
needs: build
uses: ./.github/workflows/workflow-test.yml
diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml
index b13b9def28..766fa8ecc8 100644
--- a/.github/workflows/workflow-test.yml
+++ b/.github/workflows/workflow-test.yml
@@ -21,6 +21,10 @@ on:
required: false
type: string
default: "playwright-report"
+ web-report-path:
+ required: false
+ type: string
+ default: "packages/web-platform/web-tests/playwright-report"
codecov-flags:
required: false
type: string
@@ -94,7 +98,7 @@ jobs:
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ inputs.web-report-name }}
- path: packages/web-platform/web-tests/playwright-report
+ path: ${{ inputs.web-report-path}}
if-no-files-found: error
retention-days: 1
overwrite: true
diff --git a/AGENTS.md b/AGENTS.md
index 6fa4c93d24..1747fec8da 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -245,6 +245,7 @@ These instructions were generated through comprehensive analysis and testing of
- Includes E2E test suite requiring Playwright
- Many packages have complex interdependencies
- Contains performance-critical rendering code
+- See `packages/web-platform/web-core-wasm/AGENTS.md` for specific instructions on `web-core-wasm`.
Remember: This is a complex, multi-language monorepo. Always allow extra time for builds and tests, and follow the exact command sequences provided.
diff --git a/packages/web-platform/playwright-fixtures/src/coverage-fixture.ts b/packages/web-platform/playwright-fixtures/src/coverage-fixture.ts
index 5fb66f3cda..58b17154a0 100644
--- a/packages/web-platform/playwright-fixtures/src/coverage-fixture.ts
+++ b/packages/web-platform/playwright-fixtures/src/coverage-fixture.ts
@@ -5,7 +5,7 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
-
+import { existsSync } from 'node:fs';
import { test as base } from '@playwright/test';
import type { Page } from '@playwright/test';
import v8ToIstanbul from 'v8-to-istanbul';
@@ -36,8 +36,19 @@ export const test: typeof base = base.extend({
await Promise.all(
Array.from(pages.values()).flatMap(async (page, index) => {
const coverage = await page.coverage.stopJSCoverage();
- const converter = v8ToIstanbul(
+ const sourceFilePath = [
path.join(path.dirname(testInfo.file), '..', 'www', 'main.js'),
+ path.join(
+ path.dirname(testInfo.file),
+ '..',
+ 'www',
+ 'static',
+ 'js',
+ 'index.js',
+ ),
+ ].find((p) => existsSync(p))!;
+ const converter = v8ToIstanbul(
+ sourceFilePath,
);
await converter.load();
diff --git a/packages/web-platform/web-core-wasm-e2e/resources/web-core.main-thread.json b/packages/web-platform/web-core-wasm-e2e/resources/web-core.main-thread.json
new file mode 100644
index 0000000000..e3db5b426d
--- /dev/null
+++ b/packages/web-platform/web-core-wasm-e2e/resources/web-core.main-thread.json
@@ -0,0 +1,31 @@
+{
+ "styleInfo": {},
+ "lepusCode": {
+ "root": "const self = globalThis.parent??globalThis;self.runtime = globalThis;self.__lynx_worker_type = 'main'; globalThis.registerDataProcessor='pass'; self.registerDataProcessor = registerDataProcessor;",
+ "manifest-chunk.js": "module.exports = 'hello';",
+ "manifest-chunk2.js": "module.exports = 'world';"
+ },
+ "manifest": {
+ "/app-service.js": "globalThis.runtime = lynxCoreInject.tt; globalThis.__lynx_worker_type = 'background'",
+ "/manifest-chunk.js": "module.exports = 'hello';",
+ "/manifest-chunk2.js": "module.exports = 'world';",
+ "/json": "{}"
+ },
+ "customSections": {},
+ "cardType": "react",
+ "appType": "card",
+ "pageConfig": {
+ "enableFiberArch": true,
+ "useLepusNG": true,
+ "enableReuseContext": true,
+ "bundleModuleMode": "ReturnByFunction",
+ "templateDebugUrl": "",
+ "debugInfoOutside": true,
+ "defaultDisplayLinear": true,
+ "enableCSSInvalidation": true,
+ "enableCSSSelector": true,
+ "enableLepusDebug": false,
+ "enableRemoveCSSScope": true,
+ "targetSdkVersion": "2.10"
+ }
+}
diff --git a/packages/web-platform/web-core-wasm-e2e/rsbuild.config.ts b/packages/web-platform/web-core-wasm-e2e/rsbuild.config.ts
index 2e123d32b4..ab5845c3ce 100644
--- a/packages/web-platform/web-core-wasm-e2e/rsbuild.config.ts
+++ b/packages/web-platform/web-core-wasm-e2e/rsbuild.config.ts
@@ -1,5 +1,4 @@
import { defineConfig } from '@rsbuild/core';
-import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
@@ -31,6 +30,7 @@ export default defineConfig({
watch: false,
},
],
+ htmlFallback: false,
},
html: {
tags: [
diff --git a/packages/web-platform/web-core-wasm-e2e/shell-project/index.ts b/packages/web-platform/web-core-wasm-e2e/shell-project/index.ts
index 1642c0157f..97ac04c107 100644
--- a/packages/web-platform/web-core-wasm-e2e/shell-project/index.ts
+++ b/packages/web-platform/web-core-wasm-e2e/shell-project/index.ts
@@ -21,13 +21,125 @@ export const lynxViewTests = (
});
if (!lynxView.parentElement) document.body.append(lynxView);
- Object.assign(globalThis, { lynxView });
+ const nativeModulesMap = {
+ CustomModule: URL.createObjectURL(
+ new Blob(
+ [
+ `export default function(NativeModules, NativeModulesCall) {
+ return {
+ async getColor(data, callback) {
+ const color = await NativeModulesCall('getColor', data);
+ callback(color);
+ },
+ }
+ };`,
+ ],
+ { type: 'text/javascript' },
+ ),
+ ),
+ };
+ lynxView.nativeModulesMap = nativeModulesMap;
+ const color_environment = URL.createObjectURL(
+ new Blob(
+ [
+ `export default function(NapiModules, NapiModulesCall) {
+ return {
+ getColor() {
+ NapiModules.color_methods.getColor({ color: 'green' }, color => {
+ console.log(color);
+ });
+ },
+ ColorEngine: class ColorEngine {
+ getColor(name) {
+ NapiModules.color_methods.getColor({ color: 'green' }, color => {
+ console.log(color);
+ });
+ }
+ },
+ };
+ };`,
+ ],
+ { type: 'text/javascript' },
+ ),
+ );
+
+ const color_methods = URL.createObjectURL(
+ new Blob(
+ [
+ `export default function(NapiModules, NapiModulesCall) {
+ return {
+ async getColor(data, callback) {
+ const color = await NapiModulesCall('getColor', data);
+ callback(color);
+ },
+ };
+ };`,
+ ],
+ { type: 'text/javascript' },
+ ),
+ );
+ const event_method = URL.createObjectURL(
+ new Blob(
+ [
+ `export default function(NapiModules, NapiModulesCall, handleDispatch) {
+ return {
+ async bindEvent() {
+ await NapiModulesCall('bindEvent');
+ handleDispatch((data) => console.log(\`bts:\${data}\`));
+ },
+ };
+ };`,
+ ],
+ { type: 'text/javascript' },
+ ),
+ );
+
+ lynxView.napiModulesMap = {
+ color_environment,
+ color_methods,
+ event_method,
+ };
+ lynxView.onNapiModulesCall = async (
+ name,
+ data,
+ moduleName,
+ dispatchNapiModules,
+ ) => {
+ if (name === 'getColor' && moduleName === 'color_methods') {
+ return {
+ data: { color: data.color, tagName: lynxView.tagName },
+ };
+ }
+ if (name === 'bindEvent' && moduleName === 'event_method') {
+ document.querySelector('lynx-view')?.addEventListener('click', () => {
+ dispatchNapiModules('lynx-view');
+ });
+ return;
+ }
+ return undefined;
+ };
+
+ lynxView.initI18nResources = [
+ {
+ options: {
+ locale: 'en',
+ channel: '1',
+ fallback_url: '',
+ },
+ resource: {
+ hello: 'hello',
+ lynx: 'lynx web platform1',
+ },
+ },
+ ];
+
return lynxView;
};
const searchParams = new URLSearchParams(document.location.search);
const casename = searchParams.get('casename');
const casename2 = searchParams.get('casename2');
+const resourceName = searchParams.get('resourceName');
const hasdir = searchParams.get('hasdir') === 'true';
const isSSR = document.location.pathname.includes('ssr');
@@ -42,12 +154,15 @@ if (casename) {
const lynxView = lynxViewTests(
document.querySelector('lynx-view') as LynxViewElement | undefined,
);
+
+ if (casename === 'api-inject-style-rules') {
+ lynxView.injectStyleRules = [`.injected-style-rules{background:green}`];
+ }
lynxView.setAttribute('url', lynxTemplateUrl);
lynxView.id = 'lynxview1';
if (casename2) {
lynxView.setAttribute('lynx-group-id', '2');
}
- lynxView.injectStyleRules = [`.injected-style-rules{background:green}`];
if (casename === 'api-nativemodules-call-delay') {
setTimeout(() => {
lynxView.onNativeModulesCall = (name, data, moduleName) => {
@@ -78,3 +193,9 @@ if (casename) {
} else {
console.error('cannot find casename');
}
+if (resourceName) {
+ const lynxView = lynxViewTests(
+ document.querySelector('lynx-view') as LynxViewElement | undefined,
+ );
+ lynxView.setAttribute('url', `/resources/${resourceName}`);
+}
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts
index d8574b4310..29c5987412 100644
--- a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts
+++ b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts
@@ -1653,15 +1653,15 @@ test.describe('reactlynx3 tests', () => {
}) => {
await goto(page, title);
const message: string[] = [];
+ await wait(200);
await page.on('console', (msg) => {
const text = msg.text();
if (text.startsWith('pass')) {
message.push(msg.text());
}
});
- await wait(200);
await page.locator('#button').click();
- await wait(200);
+ await wait(300);
expect(message).toContain('pass:dataset2');
});
test('api-exposure-stop-events-has-dataset', async ({ page }, {
@@ -2065,15 +2065,16 @@ test.describe('reactlynx3 tests', () => {
const elementName = 'lynx-view';
test('basic-element-lynx-view-not-auto', async ({ page }, { title }) => {
await goto(page, title);
+ await wait(100);
await page.evaluate(() => {
- document.querySelector('lynx-view')!.setAttribute('width', '100vw');
- document.querySelector('lynx-view')!.setAttribute('height', '100vh');
+ document.querySelector('lynx-view')!.removeAttribute('height');
+ document.querySelector('lynx-view')!.removeAttribute('weight');
document.querySelector('lynx-view')!.setAttribute(
'style',
- 'width: 100vw; height: 100vh',
+ 'display:flex; width: 100vw; height: 100vh',
);
});
- await wait(100);
+ await wait(200);
await diffScreenShot(
page,
elementName,
@@ -4560,137 +4561,88 @@ test.describe('reactlynx3 tests', () => {
expect(scrollend).toBeTruthy();
},
);
-
- test(
- 'basic-element-list-scroll-to-position',
- async ({ page }, { title }) => {
- await goto(page, title);
- await diffScreenShot(page, elementName, title, 'initial');
- await wait(1000);
- await page.locator('#scrollToPosition').click();
- await diffScreenShot(page, elementName, title, 'scroll-to-position');
- },
- );
-
- test(
- 'basic-element-list-waterfall',
- async ({ page }, { title }) => {
- await goto(page, title);
- await wait(500);
- await diffScreenShot(page, elementName, title, 'initial');
- },
- );
-
test(
- 'basic-element-list-estimated-main-axis-size-px',
+ 'basic-element-list-basic-size',
async ({ page, browserName }, { title }) => {
- let scrolltolower = false;
+ let scrolled = false;
+ let scrollend = false;
await page.on('console', async (msg) => {
const event = await msg.args()[0]?.evaluate((e) => ({
type: e.type,
}));
if (!event) return;
- if (event.type === 'scrolltolower') {
- scrolltolower = true;
+ if (event.type === 'scroll') {
+ scrolled = true;
+ }
+ if (event.type === 'scrollend') {
+ scrollend = true;
}
});
await goto(page, title);
- await wait(3000);
- expect(scrolltolower).toBeTruthy();
- },
- );
- test(
- 'basic-element-list-estimated-main-axis-size-px-default',
- async ({ page }, { title }) => {
- await goto(page, title);
- await diffScreenShot(page, elementName, title, 'initial');
- await wait(1000);
+ await diffScreenShot(page, elementName, title);
await page.evaluate(() => {
document.querySelector('lynx-view')!.shadowRoot!.querySelector(
'x-list',
)?.shadowRoot?.querySelector(
'#content',
)
- ?.scrollTo(0, 300);
+ ?.scrollTo(0, 500);
});
await wait(1000);
- await diffScreenShot(page, elementName, title, 'scroll');
+ expect(scrolled).toBeTruthy();
+ expect(scrollend).toBeTruthy();
},
);
test(
- 'basic-element-list-estimated-main-axis-size-px-horizontal-default',
+ 'basic-element-list-scroll-to-position',
async ({ page }, { title }) => {
await goto(page, title);
await diffScreenShot(page, elementName, title, 'initial');
await wait(1000);
- await page.evaluate(() => {
- document.querySelector('lynx-view')!.shadowRoot!.querySelector(
- 'x-list',
- )?.shadowRoot?.querySelector(
- '#content',
- )
- ?.scrollTo(300, 0);
- });
- await wait(1000);
- await diffScreenShot(page, elementName, title, 'scroll');
+ await page.locator('#scrollToPosition').click();
+ await diffScreenShot(page, elementName, title, 'scroll-to-position');
},
);
- test(
- 'basic-element-list-estimated-main-axis-size-px-waterfall',
- async ({ page, browserName }, { title }) => {
- let scrolltolower = false;
- await page.on('console', async (msg) => {
- const event = await msg.args()[0]?.evaluate((e) => ({
- type: e.type,
- }));
- if (!event) return;
- if (event.type === 'scrolltolower') {
- scrolltolower = true;
- }
- });
- await goto(page, title);
- await wait(5000);
- expect(scrolltolower).toBeTruthy();
- },
- );
test(
- 'basic-element-list-horizontal-estimated-main-axis-size-px',
- async ({ page, browserName }, { title }) => {
- let scrolltolower = false;
- await page.on('console', async (msg) => {
- const event = await msg.args()[0]?.evaluate((e) => ({
- type: e.type,
- }));
- if (!event) return;
- if (event.type === 'scrolltolower') {
- scrolltolower = true;
- }
- });
-
+ 'basic-element-list-waterfall',
+ async ({ page }, { title }) => {
await goto(page, title);
- await wait(5000);
- expect(scrolltolower).toBeTruthy();
+ await wait(500);
+ await diffScreenShot(page, elementName, title, 'initial');
},
);
+
test(
- 'basic-element-list-horizontal-estimated-main-axis-size-px-waterfall',
+ 'basic-element-list-estimated-main-axis-size-px',
async ({ page, browserName }, { title }) => {
- let scrolltolower = false;
- await page.on('console', async (msg) => {
- const event = await msg.args()[0]?.evaluate((e) => ({
- type: e.type,
- }));
- if (!event) return;
- if (event.type === 'scrolltolower') {
- scrolltolower = true;
- }
- });
-
await goto(page, title);
- await wait(5000);
- expect(scrolltolower).toBeTruthy();
+ expect(
+ await page.locator('#target').evaluate((e) =>
+ getComputedStyle(e).getPropertyValue('height')
+ ),
+ )
+ .toBe(
+ '100px',
+ );
+ await page.evaluate(() => {
+ document.querySelector('lynx-view')!.shadowRoot!.querySelector(
+ 'x-list',
+ )?.shadowRoot?.querySelector(
+ '#content',
+ )
+ ?.scrollTo(0, 5000);
+ });
+ await wait(500);
+ expect(
+ await page.locator('#target').evaluate((e) =>
+ getComputedStyle(e).getPropertyValue('height')
+ ),
+ )
+ .toBe(
+ '200px',
+ );
},
);
});
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/scroll-chromium-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-basic-size/index-chromium-linux.png
similarity index 56%
rename from packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/scroll-chromium-linux.png
rename to packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-basic-size/index-chromium-linux.png
index 59073a508d..3694bdb029 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/scroll-chromium-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-basic-size/index-chromium-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-basic-size/index-firefox-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-basic-size/index-firefox-linux.png
new file mode 100644
index 0000000000..adcf75dae8
Binary files /dev/null and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-basic-size/index-firefox-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-basic-size/index-webkit-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-basic-size/index-webkit-linux.png
new file mode 100644
index 0000000000..e4ed6843f7
Binary files /dev/null and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-basic-size/index-webkit-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/initial-chromium-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/initial-chromium-linux.png
deleted file mode 100644
index 246ebc19d4..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/initial-chromium-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/initial-firefox-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/initial-firefox-linux.png
deleted file mode 100644
index 94880ac40c..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/initial-firefox-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/initial-webkit-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/initial-webkit-linux.png
deleted file mode 100644
index 3e4df700a8..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/initial-webkit-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/scroll-firefox-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/scroll-firefox-linux.png
deleted file mode 100644
index 58f0daef01..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/scroll-firefox-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/scroll-webkit-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/scroll-webkit-linux.png
deleted file mode 100644
index a44b95b406..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-default/scroll-webkit-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/initial-chromium-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/initial-chromium-linux.png
deleted file mode 100644
index 246ebc19d4..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/initial-chromium-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/initial-firefox-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/initial-firefox-linux.png
deleted file mode 100644
index 94880ac40c..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/initial-firefox-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/initial-webkit-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/initial-webkit-linux.png
deleted file mode 100644
index 3e4df700a8..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/initial-webkit-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/scroll-chromium-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/scroll-chromium-linux.png
deleted file mode 100644
index 16ec543972..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/scroll-chromium-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/scroll-firefox-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/scroll-firefox-linux.png
deleted file mode 100644
index 2a769d2dec..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/scroll-firefox-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/scroll-webkit-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/scroll-webkit-linux.png
deleted file mode 100644
index 14b1ad3962..0000000000
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/list/basic-element-list-estimated-main-axis-size-px-horizontal-default/scroll-webkit-linux.png and /dev/null differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-blur-view/basic-element-x-blur-view-blur-radius/index-chromium-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-blur-view/basic-element-x-blur-view-blur-radius/index-chromium-linux.png
index 07fc99cf5a..f8e4d3476f 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-blur-view/basic-element-x-blur-view-blur-radius/index-chromium-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-blur-view/basic-element-x-blur-view-blur-radius/index-chromium-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-chromium-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-chromium-linux.png
index 3694ae7610..b3b7301ae3 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-chromium-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-chromium-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-firefox-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-firefox-linux.png
index 58f44260bc..917d603c88 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-firefox-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-firefox-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-webkit-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-webkit-linux.png
index 3d4711ce67..d8f36eb9b8 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-webkit-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-button-1-webkit-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-chromium-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-chromium-linux.png
index 3694ae7610..b3b7301ae3 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-chromium-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-chromium-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-firefox-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-firefox-linux.png
index 58f44260bc..917d603c88 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-firefox-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-firefox-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-webkit-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-webkit-linux.png
index 3d4711ce67..d8f36eb9b8 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-webkit-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-content-do-not-through-webkit-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-chromium-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-chromium-linux.png
index c5e34694cd..f271455428 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-chromium-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-chromium-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-firefox-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-firefox-linux.png
index 50b2c33492..270bb44cd2 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-firefox-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-firefox-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-webkit-linux.png b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-webkit-linux.png
index 719d18e1e9..31b50a27d8 100644
Binary files a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-webkit-linux.png and b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx.spec.ts-snapshots/x-overlay-ng/playground-test-1/click-overlay-out-of-content-to-trigger-bottom-button-webkit-linux.png differ
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-default/index.css b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-basic-size/index.css
similarity index 78%
rename from packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-default/index.css
rename to packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-basic-size/index.css
index 26c2ba4893..ec9ba28926 100644
--- a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-default/index.css
+++ b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-basic-size/index.css
@@ -4,8 +4,8 @@
// LICENSE file in the root directory of this source tree.
*/
.page {
- width: 100%;
- height: 100%;
+ width: 100vw;
+ height: 100vh;
display: flex;
flex-direction: column;
}
@@ -16,5 +16,12 @@ list {
}
list-item {
+ border-width: 5px;
+ border-color: green;
+ width: 100%;
+}
+
+.item {
+ height: 100px;
background-color: hsl(calc(15 * var(--item-index)), 100%, 50%);
}
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-basic-size/index.jsx b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-basic-size/index.jsx
new file mode 100644
index 0000000000..77781eca1b
--- /dev/null
+++ b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-basic-size/index.jsx
@@ -0,0 +1,57 @@
+// 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.
+import { root } from '@lynx-js/react';
+import './index.css';
+
+function App() {
+ const handleScroll = (e) => {
+ console.log(e);
+ };
+ const handleScrollEnd = (e) => {
+ console.log(e);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+root.render();
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-default/index.jsx b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-default/index.jsx
deleted file mode 100644
index 6a1f11b25b..0000000000
--- a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-default/index.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-// 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.
-import { root, useEffect, useRef } from '@lynx-js/react';
-import './index.css';
-
-function App() {
- const ref = useRef(null);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-root.render();
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-horizontal-default/index.css b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-horizontal-default/index.css
deleted file mode 100644
index 26c2ba4893..0000000000
--- a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-horizontal-default/index.css
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
-// 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.
-*/
-.page {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-list {
- width: 100%;
- height: 500px;
-}
-
-list-item {
- background-color: hsl(calc(15 * var(--item-index)), 100%, 50%);
-}
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-horizontal-default/index.jsx b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-horizontal-default/index.jsx
deleted file mode 100644
index 687150a6d9..0000000000
--- a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px-horizontal-default/index.jsx
+++ /dev/null
@@ -1,34 +0,0 @@
-// 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.
-import { root, useEffect, useRef } from '@lynx-js/react';
-import './index.css';
-
-function App() {
- const ref = useRef(null);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-root.render();
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px/index.css b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px/index.css
index 03e25d927a..251db4c5a1 100644
--- a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px/index.css
+++ b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px/index.css
@@ -4,8 +4,8 @@
// LICENSE file in the root directory of this source tree.
*/
.page {
- width: 100vw;
- height: 100vh;
+ width: 100%;
+ height: 100%;
display: flex;
flex-direction: column;
}
@@ -16,7 +16,6 @@ list {
}
list-item {
- border-width: 5px;
- border-color: green;
- background-color: yellow;
+ width: 100%;
+ background-color: hsl(calc(15 * var(--item-index)), 100%, 50%);
}
diff --git a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px/index.jsx b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px/index.jsx
index d72a867213..2722140564 100644
--- a/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px/index.jsx
+++ b/packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-element-list-estimated-main-axis-size-px/index.jsx
@@ -7,46 +7,32 @@ import './index.css';
function App() {
const ref = useRef(null);
- const handleScrollToLower = (e) => {
- console.log(e);
- };
-
- useEffect(() => {
- ref.current
- ?.invoke({
- method: 'autoScroll',
- params: {
- rate: '100',
- start: true,
- },
- })
- .exec();
- }, []);
-
return (
-
- 1
-
-
- 2
-
-
- 3
-
-
- 4
-
-
- 5
-
-
- 6
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/web-platform/web-core-wasm/README.md b/packages/web-platform/web-core-wasm/README.md
index 93bc3ed501..7e2e206191 100644
--- a/packages/web-platform/web-core-wasm/README.md
+++ b/packages/web-platform/web-core-wasm/README.md
@@ -1,3 +1 @@
-# @lynx-js/web-mainthread-apis
-
-main-thread apis
+# This is an internal experimental package, do not use
diff --git a/packages/web-platform/web-core-wasm/binary/client/client.d.ts b/packages/web-platform/web-core-wasm/binary/client/client.d.ts
index 2de32f4a0f..b971c02b1c 100644
--- a/packages/web-platform/web-core-wasm/binary/client/client.d.ts
+++ b/packages/web-platform/web-core-wasm/binary/client/client.d.ts
@@ -6,6 +6,7 @@ export class DecodedStyleData {
constructor(buffer: Uint8Array);
query_css_og_declarations_by_css_id(css_id: number, class_name: string[]): string;
static decode_into(buffer: Uint8Array, entry_name: string | null | undefined, config_enable_css_selector: boolean): Uint8Array;
+ static encode_from_raw_style_info(raw_style_info: RawStyleInfo, config_enable_css_selector: boolean, entry_name?: string | null): Uint8Array;
readonly style_content: string;
readonly font_face_content: string;
}
@@ -70,6 +71,102 @@ export class MainThreadWasmContext {
__wasm_get_unique_id_by_component_id(component_id: string): number | undefined;
__wasm_get_css_id_by_unique_id(unique_id: number): number | undefined;
}
+/**
+ *
+ * * key: cssId
+ * * value: StyleSheet
+ *
+ */
+export class RawStyleInfo {
+ free(): void;
+ [Symbol.dispose](): void;
+ constructor();
+ /**
+ *
+ * * Appends an import to the stylesheet identified by `css_id`.
+ * * If the stylesheet does not exist, it is created.
+ * * @param css_id - The ID of the CSS file.
+ * * @param import_css_id - The ID of the imported CSS file.
+ *
+ */
+ append_import(css_id: number, import_css_id: number): void;
+ /**
+ *
+ * * Pushes a rule to the stylesheet identified by `css_id`.
+ * * If the stylesheet does not exist, it is created.
+ * * @param css_id - The ID of the CSS file.
+ * * @param rule - The rule to append.
+ *
+ */
+ push_rule(css_id: number, rule: Rule): void;
+}
+export class Rule {
+ free(): void;
+ [Symbol.dispose](): void;
+ /**
+ *
+ * * Creates a new Rule with the specified type.
+ * * @param rule_type - The type of the rule (e.g., "StyleRule", "FontFaceRule", "KeyframesRule").
+ *
+ */
+ constructor(rule_type: string);
+ /**
+ *
+ * * Sets the prelude for the rule.
+ * * @param prelude - The prelude to set (SelectorList or KeyFramesPrelude).
+ *
+ */
+ set_prelude(prelude: RulePrelude): void;
+ /**
+ *
+ * * Pushes a declaration to the rule's declaration block.
+ * * @param property_name - The property name.
+ * * @param value - The property value.
+ *
+ */
+ push_declaration(property_name: string, value: string): void;
+ /**
+ *
+ * * Pushes a nested rule to the rule.
+ * * @param rule - The nested rule to add.
+ *
+ */
+ push_rule_children(rule: Rule): void;
+}
+/**
+ *
+ * * Either SelectorList or KeyFramesPrelude
+ * * Depending on the RuleType
+ * * If it is SelectorList, then selectors is a list of Selector
+ * * If it is KeyFramesPrelude, then selectors has only one selector which is Prelude text, its simple_selectors is empty
+ * * If the parent is FontFace, then selectors is empty
+ *
+ */
+export class RulePrelude {
+ free(): void;
+ [Symbol.dispose](): void;
+ constructor();
+ /**
+ *
+ * * Pushes a selector to the list.
+ * * @param selector - The selector to add.
+ *
+ */
+ push_selector(selector: Selector): void;
+}
+export class Selector {
+ free(): void;
+ [Symbol.dispose](): void;
+ constructor();
+ /**
+ *
+ * * Pushes a selector section to the selector.
+ * * @param selector_type - The type of the selector section (e.g., "ClassSelector", "IdSelector").
+ * * @param value - The value of the selector section.
+ *
+ */
+ push_one_selector_section(selector_type: string, value: string): void;
+}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
@@ -111,12 +208,28 @@ export interface InitOutput {
readonly mainthreadwasmcontext___wasm_get_css_id_by_unique_id: (a: number, b: number) => number;
readonly __wbg_elementtemplatesection_free: (a: number, b: number) => void;
readonly elementtemplatesection_from_encoded: (a: any) => [number, number, number];
+ readonly __wbg_rawstyleinfo_free: (a: number, b: number) => void;
+ readonly __wbg_rule_free: (a: number, b: number) => void;
+ readonly __wbg_ruleprelude_free: (a: number, b: number) => void;
+ readonly __wbg_selector_free: (a: number, b: number) => void;
+ readonly rawstyleinfo_new: () => number;
+ readonly rawstyleinfo_append_import: (a: number, b: number, c: number) => void;
+ readonly rawstyleinfo_push_rule: (a: number, b: number, c: number) => void;
+ readonly rule_new: (a: number, b: number) => [number, number, number];
+ readonly rule_set_prelude: (a: number, b: number) => void;
+ readonly rule_push_declaration: (a: number, b: number, c: number, d: number, e: number) => void;
+ readonly rule_push_rule_children: (a: number, b: number) => void;
+ readonly ruleprelude_new: () => number;
+ readonly ruleprelude_push_selector: (a: number, b: number) => void;
+ readonly selector_push_one_selector_section: (a: number, b: number, c: number, d: number, e: number) => [number, number];
readonly __wbg_decodedstyledata_free: (a: number, b: number) => void;
readonly decodedstyledata_new: (a: any) => [number, number, number];
readonly decodedstyledata_style_content: (a: number) => [number, number];
readonly decodedstyledata_font_face_content: (a: number) => [number, number];
readonly decodedstyledata_query_css_og_declarations_by_css_id: (a: number, b: number, c: number, d: number) => [number, number];
readonly decodedstyledata_decode_into: (a: any, b: number, c: number, d: number) => [number, number, number];
+ readonly decodedstyledata_encode_from_raw_style_info: (a: number, b: number, c: number, d: number) => [number, number, number];
+ readonly selector_new: () => number;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_exn_store: (a: number) => void;
diff --git a/packages/web-platform/web-core-wasm/binary/client/client_bg.wasm.d.ts b/packages/web-platform/web-core-wasm/binary/client/client_bg.wasm.d.ts
index 1d4f80b75c..1b9a59d4fa 100644
--- a/packages/web-platform/web-core-wasm/binary/client/client_bg.wasm.d.ts
+++ b/packages/web-platform/web-core-wasm/binary/client/client_bg.wasm.d.ts
@@ -37,12 +37,28 @@ export const mainthreadwasmcontext___wasm_get_unique_id_by_component_id: (a: num
export const mainthreadwasmcontext___wasm_get_css_id_by_unique_id: (a: number, b: number) => number;
export const __wbg_elementtemplatesection_free: (a: number, b: number) => void;
export const elementtemplatesection_from_encoded: (a: any) => [number, number, number];
+export const __wbg_rawstyleinfo_free: (a: number, b: number) => void;
+export const __wbg_rule_free: (a: number, b: number) => void;
+export const __wbg_ruleprelude_free: (a: number, b: number) => void;
+export const __wbg_selector_free: (a: number, b: number) => void;
+export const rawstyleinfo_new: () => number;
+export const rawstyleinfo_append_import: (a: number, b: number, c: number) => void;
+export const rawstyleinfo_push_rule: (a: number, b: number, c: number) => void;
+export const rule_new: (a: number, b: number) => [number, number, number];
+export const rule_set_prelude: (a: number, b: number) => void;
+export const rule_push_declaration: (a: number, b: number, c: number, d: number, e: number) => void;
+export const rule_push_rule_children: (a: number, b: number) => void;
+export const ruleprelude_new: () => number;
+export const ruleprelude_push_selector: (a: number, b: number) => void;
+export const selector_push_one_selector_section: (a: number, b: number, c: number, d: number, e: number) => [number, number];
export const __wbg_decodedstyledata_free: (a: number, b: number) => void;
export const decodedstyledata_new: (a: any) => [number, number, number];
export const decodedstyledata_style_content: (a: number) => [number, number];
export const decodedstyledata_font_face_content: (a: number) => [number, number];
export const decodedstyledata_query_css_og_declarations_by_css_id: (a: number, b: number, c: number, d: number) => [number, number];
export const decodedstyledata_decode_into: (a: any, b: number, c: number, d: number) => [number, number, number];
+export const decodedstyledata_encode_from_raw_style_info: (a: number, b: number, c: number, d: number) => [number, number, number];
+export const selector_new: () => number;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_exn_store: (a: number) => void;
diff --git a/packages/web-platform/web-core-wasm/binary/client/client_debug.d.ts b/packages/web-platform/web-core-wasm/binary/client/client_debug.d.ts
index f290e8b9de..60ac6ad70b 100644
--- a/packages/web-platform/web-core-wasm/binary/client/client_debug.d.ts
+++ b/packages/web-platform/web-core-wasm/binary/client/client_debug.d.ts
@@ -6,6 +6,7 @@ export class DecodedStyleData {
constructor(buffer: Uint8Array);
query_css_og_declarations_by_css_id(css_id: number, class_name: string[]): string;
static decode_into(buffer: Uint8Array, entry_name: string | null | undefined, config_enable_css_selector: boolean): Uint8Array;
+ static encode_from_raw_style_info(raw_style_info: RawStyleInfo, config_enable_css_selector: boolean, entry_name?: string | null): Uint8Array;
readonly style_content: string;
readonly font_face_content: string;
}
@@ -70,6 +71,102 @@ export class MainThreadWasmContext {
__GetDataset(unique_id: number): object;
__GetDataByKey(unique_id: number, key: string): any;
}
+/**
+ *
+ * * key: cssId
+ * * value: StyleSheet
+ *
+ */
+export class RawStyleInfo {
+ free(): void;
+ [Symbol.dispose](): void;
+ constructor();
+ /**
+ *
+ * * Appends an import to the stylesheet identified by `css_id`.
+ * * If the stylesheet does not exist, it is created.
+ * * @param css_id - The ID of the CSS file.
+ * * @param import_css_id - The ID of the imported CSS file.
+ *
+ */
+ append_import(css_id: number, import_css_id: number): void;
+ /**
+ *
+ * * Pushes a rule to the stylesheet identified by `css_id`.
+ * * If the stylesheet does not exist, it is created.
+ * * @param css_id - The ID of the CSS file.
+ * * @param rule - The rule to append.
+ *
+ */
+ push_rule(css_id: number, rule: Rule): void;
+}
+export class Rule {
+ free(): void;
+ [Symbol.dispose](): void;
+ /**
+ *
+ * * Creates a new Rule with the specified type.
+ * * @param rule_type - The type of the rule (e.g., "StyleRule", "FontFaceRule", "KeyframesRule").
+ *
+ */
+ constructor(rule_type: string);
+ /**
+ *
+ * * Sets the prelude for the rule.
+ * * @param prelude - The prelude to set (SelectorList or KeyFramesPrelude).
+ *
+ */
+ set_prelude(prelude: RulePrelude): void;
+ /**
+ *
+ * * Pushes a declaration to the rule's declaration block.
+ * * @param property_name - The property name.
+ * * @param value - The property value.
+ *
+ */
+ push_declaration(property_name: string, value: string): void;
+ /**
+ *
+ * * Pushes a nested rule to the rule.
+ * * @param rule - The nested rule to add.
+ *
+ */
+ push_rule_children(rule: Rule): void;
+}
+/**
+ *
+ * * Either SelectorList or KeyFramesPrelude
+ * * Depending on the RuleType
+ * * If it is SelectorList, then selectors is a list of Selector
+ * * If it is KeyFramesPrelude, then selectors has only one selector which is Prelude text, its simple_selectors is empty
+ * * If the parent is FontFace, then selectors is empty
+ *
+ */
+export class RulePrelude {
+ free(): void;
+ [Symbol.dispose](): void;
+ constructor();
+ /**
+ *
+ * * Pushes a selector to the list.
+ * * @param selector - The selector to add.
+ *
+ */
+ push_selector(selector: Selector): void;
+}
+export class Selector {
+ free(): void;
+ [Symbol.dispose](): void;
+ constructor();
+ /**
+ *
+ * * Pushes a selector section to the selector.
+ * * @param selector_type - The type of the selector section (e.g., "ClassSelector", "IdSelector").
+ * * @param value - The value of the selector section.
+ *
+ */
+ push_one_selector_section(selector_type: string, value: string): void;
+}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
@@ -85,6 +182,22 @@ export interface InitOutput {
readonly decodedstyledata_font_face_content: (a: number) => [number, number];
readonly decodedstyledata_query_css_og_declarations_by_css_id: (a: number, b: number, c: number, d: number) => [number, number];
readonly decodedstyledata_decode_into: (a: any, b: number, c: number, d: number) => [number, number, number];
+ readonly decodedstyledata_encode_from_raw_style_info: (a: number, b: number, c: number, d: number) => [number, number, number];
+ readonly __wbg_rawstyleinfo_free: (a: number, b: number) => void;
+ readonly __wbg_rule_free: (a: number, b: number) => void;
+ readonly __wbg_ruleprelude_free: (a: number, b: number) => void;
+ readonly __wbg_selector_free: (a: number, b: number) => void;
+ readonly rawstyleinfo_new: () => number;
+ readonly rawstyleinfo_append_import: (a: number, b: number, c: number) => void;
+ readonly rawstyleinfo_push_rule: (a: number, b: number, c: number) => void;
+ readonly rule_new: (a: number, b: number) => [number, number, number];
+ readonly rule_set_prelude: (a: number, b: number) => void;
+ readonly rule_push_declaration: (a: number, b: number, c: number, d: number, e: number) => void;
+ readonly rule_push_rule_children: (a: number, b: number) => void;
+ readonly ruleprelude_new: () => number;
+ readonly ruleprelude_push_selector: (a: number, b: number) => void;
+ readonly selector_new: () => number;
+ readonly selector_push_one_selector_section: (a: number, b: number, c: number, d: number, e: number) => [number, number];
readonly __wbg_elementtemplatesection_free: (a: number, b: number) => void;
readonly elementtemplatesection_from_encoded: (a: any) => [number, number, number];
readonly __wbg_eventinfo_free: (a: number, b: number) => void;
diff --git a/packages/web-platform/web-core-wasm/binary/client/client_debug_bg.wasm.d.ts b/packages/web-platform/web-core-wasm/binary/client/client_debug_bg.wasm.d.ts
index 567bef1dd3..c297c0e6be 100644
--- a/packages/web-platform/web-core-wasm/binary/client/client_debug_bg.wasm.d.ts
+++ b/packages/web-platform/web-core-wasm/binary/client/client_debug_bg.wasm.d.ts
@@ -11,6 +11,22 @@ export const decodedstyledata_style_content: (a: number) => [number, number];
export const decodedstyledata_font_face_content: (a: number) => [number, number];
export const decodedstyledata_query_css_og_declarations_by_css_id: (a: number, b: number, c: number, d: number) => [number, number];
export const decodedstyledata_decode_into: (a: any, b: number, c: number, d: number) => [number, number, number];
+export const decodedstyledata_encode_from_raw_style_info: (a: number, b: number, c: number, d: number) => [number, number, number];
+export const __wbg_rawstyleinfo_free: (a: number, b: number) => void;
+export const __wbg_rule_free: (a: number, b: number) => void;
+export const __wbg_ruleprelude_free: (a: number, b: number) => void;
+export const __wbg_selector_free: (a: number, b: number) => void;
+export const rawstyleinfo_new: () => number;
+export const rawstyleinfo_append_import: (a: number, b: number, c: number) => void;
+export const rawstyleinfo_push_rule: (a: number, b: number, c: number) => void;
+export const rule_new: (a: number, b: number) => [number, number, number];
+export const rule_set_prelude: (a: number, b: number) => void;
+export const rule_push_declaration: (a: number, b: number, c: number, d: number, e: number) => void;
+export const rule_push_rule_children: (a: number, b: number) => void;
+export const ruleprelude_new: () => number;
+export const ruleprelude_push_selector: (a: number, b: number) => void;
+export const selector_new: () => number;
+export const selector_push_one_selector_section: (a: number, b: number, c: number, d: number, e: number) => [number, number];
export const __wbg_elementtemplatesection_free: (a: number, b: number) => void;
export const elementtemplatesection_from_encoded: (a: any) => [number, number, number];
export const __wbg_eventinfo_free: (a: number, b: number) => void;
diff --git a/packages/web-platform/web-core-wasm/binary/encode/encode.d.ts b/packages/web-platform/web-core-wasm/binary/encode/encode.d.ts
index 89e8a7855b..c1935d1d8f 100644
--- a/packages/web-platform/web-core-wasm/binary/encode/encode.d.ts
+++ b/packages/web-platform/web-core-wasm/binary/encode/encode.d.ts
@@ -17,6 +17,7 @@ export class DecodedStyleData {
constructor(buffer: Uint8Array);
query_css_og_declarations_by_css_id(css_id: number, class_name: string[]): string;
static decode_into(buffer: Uint8Array, entry_name: string | null | undefined, config_enable_css_selector: boolean): Uint8Array;
+ static encode_from_raw_style_info(raw_style_info: RawStyleInfo, config_enable_css_selector: boolean, entry_name?: string | null): Uint8Array;
readonly style_content: string;
readonly font_face_content: string;
}
diff --git a/packages/web-platform/web-core-wasm/binary/encode/encode_bg.wasm.d.ts b/packages/web-platform/web-core-wasm/binary/encode/encode_bg.wasm.d.ts
index 82b1e0a408..256ea9d8b0 100644
--- a/packages/web-platform/web-core-wasm/binary/encode/encode_bg.wasm.d.ts
+++ b/packages/web-platform/web-core-wasm/binary/encode/encode_bg.wasm.d.ts
@@ -39,6 +39,7 @@ export const decodedstyledata_style_content: (a: number) => [number, number];
export const decodedstyledata_font_face_content: (a: number) => [number, number];
export const decodedstyledata_query_css_og_declarations_by_css_id: (a: number, b: number, c: number, d: number) => [number, number];
export const decodedstyledata_decode_into: (a: any, b: number, c: number, d: number) => [number, number, number];
+export const decodedstyledata_encode_from_raw_style_info: (a: number, b: number, c: number, d: number) => [number, number, number];
export const selector_new: () => number;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
diff --git a/packages/web-platform/web-core-wasm/binary/encode/encode_debug.d.ts b/packages/web-platform/web-core-wasm/binary/encode/encode_debug.d.ts
index 89e8a7855b..c1935d1d8f 100644
--- a/packages/web-platform/web-core-wasm/binary/encode/encode_debug.d.ts
+++ b/packages/web-platform/web-core-wasm/binary/encode/encode_debug.d.ts
@@ -17,6 +17,7 @@ export class DecodedStyleData {
constructor(buffer: Uint8Array);
query_css_og_declarations_by_css_id(css_id: number, class_name: string[]): string;
static decode_into(buffer: Uint8Array, entry_name: string | null | undefined, config_enable_css_selector: boolean): Uint8Array;
+ static encode_from_raw_style_info(raw_style_info: RawStyleInfo, config_enable_css_selector: boolean, entry_name?: string | null): Uint8Array;
readonly style_content: string;
readonly font_face_content: string;
}
diff --git a/packages/web-platform/web-core-wasm/binary/encode/encode_debug_bg.wasm.d.ts b/packages/web-platform/web-core-wasm/binary/encode/encode_debug_bg.wasm.d.ts
index afd4d0ae42..19223f25ec 100644
--- a/packages/web-platform/web-core-wasm/binary/encode/encode_debug_bg.wasm.d.ts
+++ b/packages/web-platform/web-core-wasm/binary/encode/encode_debug_bg.wasm.d.ts
@@ -7,6 +7,7 @@ export const decodedstyledata_style_content: (a: number) => [number, number];
export const decodedstyledata_font_face_content: (a: number) => [number, number];
export const decodedstyledata_query_css_og_declarations_by_css_id: (a: number, b: number, c: number, d: number) => [number, number];
export const decodedstyledata_decode_into: (a: any, b: number, c: number, d: number) => [number, number, number];
+export const decodedstyledata_encode_from_raw_style_info: (a: number, b: number, c: number, d: number) => [number, number, number];
export const __wbg_rawelementtemplate_free: (a: number, b: number) => void;
export const rawelementtemplate_new: () => number;
export const rawelementtemplate_append_to_root: (a: number, b: number) => void;
diff --git a/packages/web-platform/web-core-wasm/package.json b/packages/web-platform/web-core-wasm/package.json
index 4f76c54a2e..dfee42d18e 100644
--- a/packages/web-platform/web-core-wasm/package.json
+++ b/packages/web-platform/web-core-wasm/package.json
@@ -1,7 +1,7 @@
{
"name": "@lynx-js/web-core-wasm",
"version": "0.0.0",
- "private": true,
+ "description": "This is an internal experimental package, do not use",
"repository": {
"type": "git",
"url": "https://github.com/lynx-family/lynx-stack.git",
diff --git a/packages/web-platform/web-core-wasm/src/constants.rs b/packages/web-platform/web-core-wasm/src/constants.rs
index cc62a458ea..bd125e6297 100644
--- a/packages/web-platform/web-core-wasm/src/constants.rs
+++ b/packages/web-platform/web-core-wasm/src/constants.rs
@@ -251,6 +251,7 @@ lazy_static::lazy_static! {
("wrapper", "lynx-wrapper"),
("list", "x-list"),
("page", "div"),
+ ("svg", "x-svg"),
]);
pub static ref HTML_TAG_TO_LYNX_TAG_MAP: FnvHashMap<&'static str, &'static str> = FnvHashMap::from_iter(LYNX_TAG_TO_HTML_TAG_MAP
@@ -293,4 +294,33 @@ lazy_static::lazy_static! {
"div",
"svg"
]);
+
+ pub static ref ELEMENT_REACTIVE_EVENTS: FnvHashSet<&'static str> = FnvHashSet::from_iter(vec![
+ "headeroffset",
+ "headershow",
+ "footeroffset",
+ "startrefresh",
+ "headerreleased",
+ "startloadmore",
+ "footerreleased",
+ "scrolltoupper",
+ "scrolltolower",
+ "scroll",
+ "scrollend",
+ "load",
+ "change",
+ "offsetchange",
+ "transition",
+ "scrollstart",
+ "change-event-for-indicator",
+ "layoutchange",
+ "input",
+ "selection",
+ "error",
+ "layout",
+ "offset",
+ "snap",
+ "scrolltoupperedge",
+ "scrolltoloweredge"
+ ]);
}
diff --git a/packages/web-platform/web-core-wasm/src/js_binding/mts_js_binding.rs b/packages/web-platform/web-core-wasm/src/js_binding/mts_js_binding.rs
index 54721dbb88..3d68a30ac7 100644
--- a/packages/web-platform/web-core-wasm/src/js_binding/mts_js_binding.rs
+++ b/packages/web-platform/web-core-wasm/src/js_binding/mts_js_binding.rs
@@ -48,4 +48,18 @@ extern "C" {
unique_id: usize,
to_enable: bool,
);
+
+ #[wasm_bindgen(method, js_name = "enableElementEvent")]
+ pub fn enable_element_event(
+ this: &RustMainthreadContextBinding,
+ unique_id: usize,
+ event_name: &str,
+ );
+
+ #[wasm_bindgen(method, js_name = "disableElementEvent")]
+ pub fn disable_element_event(
+ this: &RustMainthreadContextBinding,
+ unique_id: usize,
+ event_name: &str,
+ );
}
diff --git a/packages/web-platform/web-core-wasm/src/main_thread/element_apis/event_apis.rs b/packages/web-platform/web-core-wasm/src/main_thread/element_apis/event_apis.rs
index 633c1b8c49..248e24f8ab 100644
--- a/packages/web-platform/web-core-wasm/src/main_thread/element_apis/event_apis.rs
+++ b/packages/web-platform/web-core-wasm/src/main_thread/element_apis/event_apis.rs
@@ -5,6 +5,7 @@
*/
use super::MainThreadWasmContext;
+use crate::constants;
use wasm_bindgen::prelude::*;
/**
@@ -31,16 +32,41 @@ impl MainThreadWasmContext {
event_handler_identifier: Option,
) {
let event_name = event_name.to_ascii_lowercase();
+ let event_name_str = event_name.as_str();
let event_type = event_type.to_ascii_lowercase();
self.enable_event(&event_name);
+
+ let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str);
+ let mut should_enable = false;
+ let mut should_disable = false;
+
if let Some(binding) = self.get_element_data_by_unique_id(unique_id) {
let mut element_data = binding.borrow_mut();
+ if is_allowlisted {
+ let old_handler =
+ element_data.get_framework_cross_thread_event_handler(&event_name, &event_type);
+ match (&old_handler, &event_handler_identifier) {
+ (None, Some(_)) => should_enable = true,
+ (Some(_), None) => should_disable = true,
+ _ => {}
+ }
+ }
+
element_data.replace_framework_cross_thread_event_handler(
- event_name,
+ event_name.clone(),
event_type,
event_handler_identifier,
);
}
+ if should_enable {
+ self
+ .mts_binding
+ .enable_element_event(unique_id, event_name_str);
+ } else if should_disable {
+ self
+ .mts_binding
+ .disable_element_event(unique_id, event_name_str);
+ }
}
#[wasm_bindgen(js_name = "__wasm_add_event_run_worklet")]
@@ -52,16 +78,41 @@ impl MainThreadWasmContext {
event_handler_identifier: Option,
) {
let event_name = event_name.to_ascii_lowercase();
+ let event_name_str = event_name.as_str();
let event_type = event_type.to_ascii_lowercase();
self.enable_event(&event_name);
+
+ let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str);
+ let mut should_enable = false;
+ let mut should_disable = false;
+
if let Some(binding) = self.get_element_data_by_unique_id(unique_id) {
let mut element_data = binding.borrow_mut();
+ if is_allowlisted {
+ let old_handler =
+ element_data.get_framework_run_worklet_event_handler(&event_name, &event_type);
+ match (&old_handler, &event_handler_identifier) {
+ (None, Some(_)) => should_enable = true,
+ (Some(_), None) => should_disable = true,
+ _ => {}
+ }
+ }
+
element_data.replace_framework_run_worklet_event_handler(
- event_name,
+ event_name.clone(),
event_type,
event_handler_identifier,
);
}
+ if should_enable {
+ self
+ .mts_binding
+ .enable_element_event(unique_id, event_name_str);
+ } else if should_disable {
+ self
+ .mts_binding
+ .disable_element_event(unique_id, event_name_str);
+ }
}
#[wasm_bindgen(js_name = "__GetEvent")]
diff --git a/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/decoded_style_info.rs b/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/decoded_style_info.rs
index 7b7686a9cd..c4aff9159f 100644
--- a/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/decoded_style_info.rs
+++ b/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/decoded_style_info.rs
@@ -28,7 +28,7 @@ pub(crate) struct StyleInfoDecoder {
temp_child_rules_buffer: String,
config_enable_css_selector: bool,
entry_name: Option,
- css_og_current_processing_css_id: Option,
+ css_og_current_processing_css_ids: Option>,
css_og_current_processing_class_selector_names: Option>,
}
@@ -51,7 +51,7 @@ impl StyleInfoDecoder {
entry_name,
config_enable_css_selector,
is_processing_font_face: false,
- css_og_current_processing_css_id: None,
+ css_og_current_processing_css_ids: None,
css_og_current_processing_class_selector_names: None,
};
decoded_style_info.decode(flattened_style_info)?;
@@ -62,12 +62,12 @@ impl StyleInfoDecoder {
&mut self,
flattened_style_info: FlattenedStyleInfo,
) -> Result<(), wasm_bindgen::JsError> {
- for (css_id, style_sheet) in flattened_style_info.css_id_to_style_sheet.into_iter() {
+ for (_, style_sheet) in flattened_style_info.css_id_to_style_sheet.into_iter() {
for mut style_rule in style_sheet.rules.into_iter() {
match style_rule.rule_type {
RuleType::Declaration => {
if !self.config_enable_css_selector {
- self.css_og_current_processing_css_id = Some(css_id);
+ self.css_og_current_processing_css_ids = Some(style_sheet.imported_by.clone());
self.css_og_current_processing_class_selector_names = Some(Vec::new());
}
let mut new_selectors_to_add = Vec::new(); // selectors will be added for removeCSSScope false
@@ -128,12 +128,12 @@ impl StyleInfoDecoder {
== OneSimpleSelectorType::PseudoElementSelector
&& simple_selector.value == "placeholder"
{
- // transform ::placeholder to ::part(placeholder)::placeholder
+ // transform ::placeholder to ::part(input)::placeholder
selector.simple_selectors.insert(
simple_selector_index,
OneSimpleSelector {
selector_type: OneSimpleSelectorType::PseudoElementSelector,
- value: "part(placeholder)".to_string(),
+ value: "part(input)".to_string(),
},
);
simple_selector_index += 1; // skip the newly inserted simple selector
@@ -296,38 +296,42 @@ impl Generator for StyleInfoDecoder {
fn push_transform_kids_style(&mut self, declaration: ParsedDeclaration) {
declaration.generate_to_string_buf(&mut self.temp_child_rules_buffer);
if !self.config_enable_css_selector {
- if let (Some(map), Some(css_id), Some(names)) = (
+ if let (Some(map), Some(css_ids), Some(names)) = (
self
.css_og_css_id_to_class_selector_name_to_declarations_map
.as_mut(),
- self.css_og_current_processing_css_id,
+ self.css_og_current_processing_css_ids.as_ref(),
self.css_og_current_processing_class_selector_names.as_ref(),
) {
- let class_selector_map = map.entry(css_id).or_default();
- for class_selector_name in names.iter() {
- let string_buf = class_selector_map
- .entry(class_selector_name.clone())
- .or_default();
- declaration.generate_to_string_buf(string_buf);
+ for css_id in css_ids.iter() {
+ let class_selector_map = map.entry(*css_id).or_default();
+ for class_selector_name in names.iter() {
+ let string_buf = class_selector_map
+ .entry(class_selector_name.clone())
+ .or_default();
+ declaration.generate_to_string_buf(string_buf);
+ }
}
}
}
}
fn push_transformed_style(&mut self, declaration: ParsedDeclaration) {
if !self.config_enable_css_selector && !self.is_processing_font_face {
- if let (Some(map), Some(css_id), Some(names)) = (
+ if let (Some(map), Some(css_ids), Some(names)) = (
self
.css_og_css_id_to_class_selector_name_to_declarations_map
.as_mut(),
- self.css_og_current_processing_css_id,
+ self.css_og_current_processing_css_ids.as_ref(),
self.css_og_current_processing_class_selector_names.as_ref(),
) {
- let class_selector_map = map.entry(css_id).or_default();
- for class_selector_name in names.iter() {
- let string_buf = class_selector_map
- .entry(class_selector_name.clone())
- .or_default();
- declaration.generate_to_string_buf(string_buf);
+ for css_id in css_ids.iter() {
+ let class_selector_map = map.entry(*css_id).or_default();
+ for class_selector_name in names.iter() {
+ let string_buf = class_selector_map
+ .entry(class_selector_name.clone())
+ .or_default();
+ declaration.generate_to_string_buf(string_buf);
+ }
}
}
}
@@ -843,4 +847,51 @@ mod test {
let expected = ":not([hidden]):not([l-e-name]){width:100px;}";
assert_eq!(result.style_content, expected);
}
+ #[test]
+ fn test_placeholder_selector() {
+ let raw_style_info = RawStyleInfo {
+ css_id_to_style_sheet: FnvHashMap::from_iter(vec![(
+ 0,
+ StyleSheet {
+ imports: vec![],
+ rules: vec![Rule {
+ nested_rules: vec![],
+ rule_type: RuleType::Declaration,
+ prelude: RulePrelude {
+ selector_list: vec![Selector {
+ simple_selectors: vec![OneSimpleSelector {
+ selector_type: OneSimpleSelectorType::PseudoElementSelector,
+ value: "placeholder".to_string(),
+ }],
+ }],
+ },
+ declaration_block: DeclarationBlock {
+ tokens: vec![
+ ValueToken {
+ token_type: crate::css_tokenizer::token_types::IDENT_TOKEN,
+ value: "color".to_string(),
+ },
+ ValueToken {
+ token_type: crate::css_tokenizer::token_types::COLON_TOKEN,
+ value: ":".to_string(),
+ },
+ ValueToken {
+ token_type: crate::css_tokenizer::token_types::IDENT_TOKEN,
+ value: "red".to_string(),
+ },
+ ValueToken {
+ token_type: crate::css_tokenizer::token_types::SEMICOLON_TOKEN,
+ value: ";".to_string(),
+ },
+ ],
+ },
+ }],
+ },
+ )]),
+ style_content_str_size_hint: 0,
+ };
+ let result = generate_string_buf(raw_style_info, true, None);
+ let expected = ":not([l-e-name])::part(input)::placeholder{--lynx-text-bg-color:initial;-webkit-background-clip:initial;background-clip:initial;color:red;}";
+ assert_eq!(result.style_content, expected);
+ }
}
diff --git a/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/mod.rs b/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/mod.rs
index 319b92f99d..ca51f8b4c6 100644
--- a/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/mod.rs
+++ b/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/mod.rs
@@ -100,4 +100,17 @@ impl DecodedStyleData {
.map_err(|e| wasm_bindgen::JsError::new(&format!("Failed to encode to Uint8Array: {e}",)))?;
Ok(js_sys::Uint8Array::from(data.as_slice()))
}
+
+ #[wasm_bindgen]
+ pub fn encode_from_raw_style_info(
+ raw_style_info: RawStyleInfo,
+ config_enable_css_selector: bool,
+ entry_name: Option,
+ ) -> Result {
+ let decode_data: DecodedStyleData =
+ StyleInfoDecoder::new(raw_style_info, entry_name, config_enable_css_selector)?.into();
+ let data = &bincode::encode_to_vec(&decode_data, bincode::config::standard())
+ .map_err(|e| wasm_bindgen::JsError::new(&format!("Failed to encode to Uint8Array: {e}",)))?;
+ Ok(js_sys::Uint8Array::from(data.as_slice()))
+ }
}
diff --git a/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/raw_style_info.rs b/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/raw_style_info.rs
index 15bf0b30f8..4e8d4ff0e8 100644
--- a/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/raw_style_info.rs
+++ b/packages/web-platform/web-core-wasm/src/template/template_sections/style_info/raw_style_info.rs
@@ -4,31 +4,28 @@
* LICENSE file in the root directory of this source tree.
*/
-#[cfg(feature = "encode")]
use crate::css_tokenizer::token_types::{COLON_TOKEN, IDENT_TOKEN, SEMICOLON_TOKEN};
-#[cfg(feature = "encode")]
use crate::css_tokenizer::tokenize;
use bincode::Decode;
#[cfg(feature = "encode")]
use bincode::Encode;
use fnv::FnvHashMap;
-#[cfg(feature = "encode")]
use wasm_bindgen::prelude::*;
/**
* key: cssId
* value: StyleSheet
*/
-#[derive(Decode)]
-#[cfg_attr(feature = "encode", derive(Encode, Clone, Default))]
-#[cfg_attr(feature = "encode", wasm_bindgen)]
+#[derive(Decode, Default)]
+#[cfg_attr(feature = "encode", derive(Encode, Clone))]
+#[wasm_bindgen]
pub struct RawStyleInfo {
pub(super) css_id_to_style_sheet: FnvHashMap,
pub(super) style_content_str_size_hint: usize,
}
-#[derive(Decode)]
-#[cfg_attr(feature = "encode", derive(Encode, Default, Clone))]
+#[derive(Decode, Default)]
+#[cfg_attr(feature = "encode", derive(Encode, Clone))]
pub(crate) struct StyleSheet {
pub(super) imports: Vec,
pub(super) rules: Vec,
@@ -36,7 +33,7 @@ pub(crate) struct StyleSheet {
#[derive(Decode)]
#[cfg_attr(feature = "encode", derive(Encode, Clone))]
-#[cfg_attr(feature = "encode", wasm_bindgen)]
+#[wasm_bindgen]
pub struct Rule {
pub(super) rule_type: RuleType,
pub(super) prelude: RulePrelude,
@@ -54,7 +51,7 @@ pub(super) enum RuleType {
#[derive(Decode, Default)]
#[cfg_attr(feature = "encode", derive(Encode, Clone))]
-#[cfg_attr(feature = "encode", wasm_bindgen)]
+#[wasm_bindgen]
/**
* Either SelectorList or KeyFramesPrelude
* Depending on the RuleType
@@ -68,7 +65,7 @@ pub struct RulePrelude {
#[derive(Decode, Clone, Default)]
#[cfg_attr(feature = "encode", derive(Encode))]
-#[cfg_attr(feature = "encode", wasm_bindgen)]
+#[wasm_bindgen]
pub struct Selector {
pub(super) simple_selectors: Vec,
}
@@ -110,10 +107,8 @@ pub(super) struct ValueToken {
pub(super) value: String,
}
-#[cfg(feature = "encode")]
#[wasm_bindgen]
impl RawStyleInfo {
- #[cfg(feature = "encode")]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self::default()
@@ -125,7 +120,6 @@ impl RawStyleInfo {
* @param css_id - The ID of the CSS file.
* @param import_css_id - The ID of the imported CSS file.
*/
- #[cfg(feature = "encode")]
#[wasm_bindgen]
pub fn append_import(&mut self, css_id: i32, import_css_id: i32) {
// if css_id not exist, create a new StyleSheet
@@ -139,7 +133,6 @@ impl RawStyleInfo {
* @param css_id - The ID of the CSS file.
* @param rule - The rule to append.
*/
- #[cfg(feature = "encode")]
#[wasm_bindgen]
pub fn push_rule(&mut self, css_id: i32, rule: Rule) {
let style_sheet = self.css_id_to_style_sheet.entry(css_id).or_default();
@@ -162,13 +155,12 @@ impl RawStyleInfo {
}
}
-#[cfg_attr(feature = "encode", wasm_bindgen)]
+#[wasm_bindgen]
impl Rule {
/**
* Creates a new Rule with the specified type.
* @param rule_type - The type of the rule (e.g., "StyleRule", "FontFaceRule", "KeyframesRule").
*/
- #[cfg(feature = "encode")]
#[wasm_bindgen(constructor)]
pub fn new(rule_type: String) -> Result {
let rule_type_enum = match rule_type.as_str() {
@@ -193,7 +185,6 @@ impl Rule {
* Sets the prelude for the rule.
* @param prelude - The prelude to set (SelectorList or KeyFramesPrelude).
*/
- #[cfg(feature = "encode")]
#[wasm_bindgen]
pub fn set_prelude(&mut self, prelude: RulePrelude) {
self.prelude = prelude;
@@ -204,7 +195,6 @@ impl Rule {
* @param property_name - The property name.
* @param value - The property value.
*/
- #[cfg(feature = "encode")]
#[wasm_bindgen]
pub fn push_declaration(&mut self, property_name: String, value: String) {
// 1. property name
@@ -238,16 +228,14 @@ impl Rule {
* Pushes a nested rule to the rule.
* @param rule - The nested rule to add.
*/
- #[cfg(feature = "encode")]
#[wasm_bindgen]
pub fn push_rule_children(&mut self, rule: Rule) {
self.nested_rules.push(rule);
}
}
-#[cfg_attr(feature = "encode", wasm_bindgen)]
+#[wasm_bindgen]
impl RulePrelude {
- #[cfg(feature = "encode")]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
@@ -259,16 +247,14 @@ impl RulePrelude {
* Pushes a selector to the list.
* @param selector - The selector to add.
*/
- #[cfg(feature = "encode")]
#[wasm_bindgen]
pub fn push_selector(&mut self, selector: Selector) {
self.selector_list.push(selector);
}
}
-#[cfg_attr(feature = "encode", wasm_bindgen)]
+#[wasm_bindgen]
impl Selector {
- #[cfg(feature = "encode")]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
@@ -281,7 +267,6 @@ impl Selector {
* @param selector_type - The type of the selector section (e.g., "ClassSelector", "IdSelector").
* @param value - The value of the selector section.
*/
- #[cfg(feature = "encode")]
#[wasm_bindgen]
pub fn push_one_selector_section(
&mut self,
@@ -356,12 +341,10 @@ impl Selector {
}
}
}
-#[cfg(feature = "encode")]
struct DeclarationParser {
value_token_list: Vec,
}
-#[cfg(feature = "encode")]
impl tokenize::Parser for DeclarationParser {
fn on_token(&mut self, token_type: u8, token_value: &str) {
let value_token = ValueToken {
diff --git a/packages/web-platform/web-core-wasm/tests/StyleManager.spec.ts b/packages/web-platform/web-core-wasm/tests/StyleManager.spec.ts
index 610a1a74cd..de72dd5003 100644
--- a/packages/web-platform/web-core-wasm/tests/StyleManager.spec.ts
+++ b/packages/web-platform/web-core-wasm/tests/StyleManager.spec.ts
@@ -6,6 +6,7 @@ import { describe, it, expect, beforeEach, vi, beforeAll } from 'vitest';
import { encodeCSS } from '../ts/encode/encodeCSS.js';
import { DecodedStyle } from '../ts/client/wasm.js';
import * as CSS from '@lynx-js/css-serializer';
+import { StyleManager } from '../ts/client/mainthread/StyleManager.js';
vi.mock('wasm-feature-detect', () => ({
referenceTypes: async () => true,
@@ -13,13 +14,7 @@ vi.mock('wasm-feature-detect', () => ({
describe('StyleManager', () => {
let rootNode: HTMLElement;
- let styleManager: any;
- let StyleManager: any;
-
- beforeAll(async () => {
- const module = await import('../ts/client/mainthread/StyleManager.js');
- StyleManager = module.StyleManager;
- });
+ let styleManager: StyleManager;
beforeEach(() => {
rootNode = document.createElement('div');
@@ -115,4 +110,51 @@ describe('StyleManager', () => {
expect(sheet.cssRules.length).toBe(1);
}
});
+ it('should not include styles from other css-ids if not imported in Non-CSS Selector mode', () => {
+ styleManager = new StyleManager(rootNode);
+
+ const encoded = encodeCSS({
+ '1': CSS.parse(`
+ .background-green {
+ background-color: green;
+ }
+ `).root,
+ '2': CSS.parse(`
+ .background-yellow {
+ background-color: yellow;
+ }
+ `).root,
+ '123': CSS.parse(`
+ @import "1";
+ `).root,
+ });
+
+ const decodedStyle = new DecodedStyle(
+ DecodedStyle.webWorkerDecode(encoded, false),
+ );
+
+ styleManager.pushStyleSheet(decodedStyle);
+
+ // Node has cssId = 1
+ styleManager.updateCssOgStyle(
+ 1,
+ 123,
+ ['background-yellow', 'background-green'] as any,
+ );
+
+ const styleElement = rootNode.querySelector(
+ 'style:last-of-type',
+ ) as HTMLStyleElement;
+ const sheet = styleElement?.sheet;
+ expect(sheet).toBeDefined();
+ if (sheet) {
+ expect(sheet.cssRules.length).toBe(1);
+ const rule = sheet.cssRules[0] as CSSStyleRule;
+ // Should contain green
+ expect(rule.style.backgroundColor).toBe('green');
+ // Should NOT contain yellow
+ expect(rule.style.backgroundColor).not.toBe('yellow');
+ expect(rule.cssText).not.toContain('yellow');
+ }
+ });
});
diff --git a/packages/web-platform/web-core-wasm/tests/element-apis.spec.ts b/packages/web-platform/web-core-wasm/tests/element-apis.spec.ts
index 67422ec71a..cab95aa442 100644
--- a/packages/web-platform/web-core-wasm/tests/element-apis.spec.ts
+++ b/packages/web-platform/web-core-wasm/tests/element-apis.spec.ts
@@ -1339,4 +1339,59 @@ describe('Element APIs', () => {
}).toThrow();
});
});
+
+ test('should optimize event enable/disable for whitelisted events', () => {
+ const root = mtsGlobalThis.__CreatePage('page', 0);
+ const element = mtsGlobalThis.__CreateScrollView(0);
+ mtsGlobalThis.__AppendElement(root, element);
+
+ const enableSpy = vi.spyOn(mtsBinding, 'enableElementEvent');
+ const disableSpy = vi.spyOn(mtsBinding, 'disableElementEvent');
+
+ mtsGlobalThis.__AddEvent(element, 'bindEvent', 'input', 'handler1');
+ expect(enableSpy).toHaveBeenCalledTimes(1);
+ expect(enableSpy).toHaveBeenCalledWith(expect.anything(), 'input');
+ enableSpy.mockClear();
+
+ mtsGlobalThis.__AddEvent(element, 'bindEvent', 'input', 'handler2');
+ expect(enableSpy).not.toHaveBeenCalled();
+
+ // 3. Remove first scroll listener (by setting null/undefined or however removal works)
+ // __AddEvent doesn't explicitly support removal in the type signature shown in createElementAPI ??
+ // Actually createElementAPI.__AddEvent just calls __wasm_add_event_bts.
+ // To remove, we usually pass null/undefined as identifier?
+ // Looking at createElementAPI.ts: if frameworkCrossThreadIdentifier == null, it calls with undefined.
+ // But how to remove?
+ // In Rust: replace_framework_cross_thread_event_handler takes Option.
+ // If we call __AddEvent with null identifier?
+
+ // Let's check createElementAPI.ts again.
+ // if (frameworkCrossThreadIdentifier == null) { wasmContext.__wasm_add_event_bts(..., undefined); ... }
+ // So passing null/undefined removes it?
+ // Wait, the Rust side: `replace_framework_cross_thread_event_handler` puts the new identifier.
+ // If new identifier is None (from undefined), it removes.
+ // But we need to target the *specific* event name.
+
+ // Warning: `AddEventPAPI` signature usually implies adding.
+ // "replace_framework_cross_thread_event_handler" suggests we REPLACE the handler for (event_name, event_type).
+ // So there is only ONE "bindEvent" handler for "lynxscroll" at a time?
+ // Use `event_apis.rs`: `framework_cross_thread_identifier: FnvHashMap` where key is like "bind", "capture-bind".
+ // So YES, for a given (event_name, type="bindEvent"), there is only ONE handler identifier.
+
+ // So my test step 2 "Add second scroll listener" actually REPLACES the first one.
+ // Rust logic:
+ // Old: Some("handler1"), New: Some("handler2").
+ // match (Some, Some) => nothing.
+ // Correct.
+
+ // 4. Remove listener.
+ // Call __AddEvent with null identifier.
+ mtsGlobalThis.__AddEvent(element, 'bindEvent', 'input', null as any);
+
+ // Rust logic:
+ // Old: Some("handler2"), New: None.
+ // match (Some, None) => should_disable = true.
+ expect(disableSpy).toHaveBeenCalledTimes(1);
+ expect(disableSpy).toHaveBeenCalledWith(expect.anything(), 'input');
+ });
});
diff --git a/packages/web-platform/web-core-wasm/tests/encode.spec.ts b/packages/web-platform/web-core-wasm/tests/encode.spec.ts
index 2fae003636..da229e148c 100644
--- a/packages/web-platform/web-core-wasm/tests/encode.spec.ts
+++ b/packages/web-platform/web-core-wasm/tests/encode.spec.ts
@@ -9,6 +9,8 @@ import {
} from '../ts/encode/encodeCSS.js';
import * as CSS from '@lynx-js/css-serializer';
import { DecodedStyle } from '../ts/client/wasm.js';
+import { encode, TasmJSONInfo } from '../ts/encode/webEncoder.js';
+import { TemplateSectionLabel } from '../ts/constants.js';
describe('RawStyleInfo', () => {
test('should encode StyleRule correctly', () => {
@@ -359,3 +361,65 @@ describe('encodeCSS', () => {
// expect(decodedString.trim()).toBe('');
// })
});
+
+describe('webEncoder', () => {
+ test('should skip elementTemplates section if empty', () => {
+ const tasmJSON: TasmJSONInfo = {
+ styleInfo: {},
+ manifest: {},
+ cardType: 'card',
+ appType: 'card',
+ pageConfig: {},
+ lepusCode: {},
+ customSections: {},
+ elementTemplates: {},
+ };
+ const buffer = encode(tasmJSON);
+ const view = new DataView(buffer.buffer);
+ let offset = 8 + 4; // Magic + Version
+
+ while (offset < buffer.byteLength) {
+ const label = view.getUint32(offset, true);
+ offset += 4;
+ const length = view.getUint32(offset, true);
+ offset += 4;
+ if (label === TemplateSectionLabel.ElementTemplates) {
+ throw new Error('ElementTemplates section should not be present');
+ }
+ offset += length;
+ }
+ });
+
+ test('should include elementTemplates section if not empty', () => {
+ const tasmJSON: TasmJSONInfo = {
+ styleInfo: {},
+ manifest: {},
+ cardType: 'card',
+ appType: 'card',
+ pageConfig: {},
+ lepusCode: {},
+ customSections: {},
+ elementTemplates: {
+ '0': {
+ type: 'view',
+ },
+ },
+ };
+ const buffer = encode(tasmJSON);
+ const view = new DataView(buffer.buffer);
+ let offset = 8 + 4; // Magic + Version
+ let found = false;
+
+ while (offset < buffer.byteLength) {
+ const label = view.getUint32(offset, true);
+ offset += 4;
+ const length = view.getUint32(offset, true);
+ offset += 4;
+ if (label === TemplateSectionLabel.ElementTemplates) {
+ found = true;
+ }
+ offset += length;
+ }
+ expect(found).toBe(true);
+ });
+});
diff --git a/packages/web-platform/web-core-wasm/tests/template-manager.spec.ts b/packages/web-platform/web-core-wasm/tests/template-manager.spec.ts
index d7702261cf..49ba7c4b64 100644
--- a/packages/web-platform/web-core-wasm/tests/template-manager.spec.ts
+++ b/packages/web-platform/web-core-wasm/tests/template-manager.spec.ts
@@ -43,6 +43,9 @@ const mockLynxViewInstance = {
onStyleInfoReady: vi.fn(),
onMTSScriptsLoaded: vi.fn(),
onBTSScriptsLoaded: vi.fn(),
+ backgroundThread: vi.mockObject({
+ markTiming: vi.fn(),
+ }),
} as unknown as LynxViewInstance;
describe('Template Manager', () => {
@@ -253,4 +256,82 @@ describe('Template Manager', () => {
}),
);
});
+
+ test('should load web-core.main-thread.json correctly', async () => {
+ const jsonContent = {
+ 'styleInfo': {
+ '0': {
+ 'rules': [],
+ 'content': [],
+ },
+ },
+ 'lepusCode': {
+ 'app-service.js':
+ 'globalThis.runtime = lynxCoreInject.tt; globalThis.__lynx_worker_type = \'background\'',
+ 'manifest-chunk.js': 'module.exports = \'hello\';',
+ 'manifest-chunk2.js': 'module.exports = \'world\';',
+ },
+ 'manifest': {
+ '/app-service.js':
+ 'globalThis.runtime = lynxCoreInject.tt; globalThis.__lynx_worker_type = \'background\'',
+ '/manifest-chunk.js': 'module.exports = \'hello\';',
+ '/manifest-chunk2.js': 'module.exports = \'world\';',
+ '/json': '{}',
+ },
+ 'customSections': {},
+ 'cardType': 'react',
+ 'appType': 'card',
+ 'pageConfig': {
+ 'enableFiberArch': true,
+ 'useLepusNG': true,
+ 'enableReuseContext': true,
+ 'bundleModuleMode': 'ReturnByFunction',
+ 'templateDebugUrl': '',
+ 'debugInfoOutside': true,
+ 'defaultDisplayLinear': true,
+ 'enableCSSInvalidation': true,
+ 'enableCSSSelector': true,
+ 'enableLepusDebug': false,
+ 'enableRemoveCSSScope': true,
+ 'targetSdkVersion': '2.10',
+ },
+ };
+
+ const jsonString = JSON.stringify(jsonContent);
+ const encoded = new TextEncoder().encode(jsonString);
+
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoded);
+ controller.close();
+ },
+ });
+
+ (globalThis.fetch as any).mockResolvedValue({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ body: stream,
+ });
+
+ const templateUrl = 'http://example.com/web-core.main-thread.json';
+ await templateManager.fetchBundle(
+ templateUrl,
+ Promise.resolve(mockLynxViewInstance),
+ );
+
+ // Verify config
+ expect(mockLynxViewInstance.onPageConfigReady).toHaveBeenCalledWith(
+ expect.objectContaining(jsonContent.pageConfig),
+ );
+
+ // Verify style info
+ expect(mockLynxViewInstance.onStyleInfoReady).toHaveBeenCalled();
+
+ // Verify script decoding (LepusCode)
+ // The worker sends section: LepusCode with a blob URL map.
+ // TemplateManager handles this but doesn't expose the blob map directly easily in tests unless we mock the handler side effect or inspect mockLynxViewInstance.
+ // TemplateManager calls lynxViewInstance.onMTSScriptsLoaded(url, data).
+ expect(mockLynxViewInstance.onMTSScriptsLoaded).toHaveBeenCalled();
+ });
});
diff --git a/packages/web-platform/web-core-wasm/tests/testing-library-port.spec.ts b/packages/web-platform/web-core-wasm/tests/testing-library-port.spec.ts
index 6d4296ccc8..96b41d2f93 100644
--- a/packages/web-platform/web-core-wasm/tests/testing-library-port.spec.ts
+++ b/packages/web-platform/web-core-wasm/tests/testing-library-port.spec.ts
@@ -91,7 +91,7 @@ describe('Testing Library Port', () => {
const el = rootDom.querySelector('[data-testid="svg-element"]');
expect(el).not.toBeNull();
- expect(el?.tagName.toLowerCase()).toBe('svg');
+ expect(el?.tagName.toLowerCase()).toBe('x-svg');
expect(mtsGlobalThis.__GetTag(view1)).toBe('svg');
});
diff --git a/packages/web-platform/web-core-wasm/ts/client/decodeWorker/cssLoader.ts b/packages/web-platform/web-core-wasm/ts/client/decodeWorker/cssLoader.ts
new file mode 100644
index 0000000000..76f7c000cd
--- /dev/null
+++ b/packages/web-platform/web-core-wasm/ts/client/decodeWorker/cssLoader.ts
@@ -0,0 +1,122 @@
+import { wasmInstance } from '../wasm.js';
+
+interface CSSRule {
+ sel: string[][][];
+ decl: [string, string][];
+}
+
+interface OneInfo {
+ content: string[];
+ rules: CSSRule[];
+ imports?: string[];
+}
+
+type StyleInfo = Record;
+
+export function loadStyleFromJSON(
+ styleInfo: StyleInfo,
+ configEnableCssSelector: boolean,
+ entryName?: string,
+): Uint8Array {
+ const rawStyleInfo = new wasmInstance.RawStyleInfo();
+
+ for (const [cssIdStr, info] of Object.entries(styleInfo)) {
+ const cssId = parseInt(cssIdStr, 10);
+
+ // Handle imports
+ if (info.imports) {
+ // imports in StyleInfo are filenames/hrefs, but RawStyleInfo expects cssIds.
+ // Wait, genStyleInfo output imports as string hrefs?
+ // RawStyleInfo: imports: Vec
+ // It seems genStyleInfo.ts produces imports array of strings, but RawStyleInfo needs integers.
+ // If the JSON only contains strings, we might have a problem mapping them back to IDs unless we have a map.
+ // However, WebEncodePlugin usually handles mapping.
+ // Let's check genStyleInfo again.
+ // node.type === 'ImportRule' => imports.push(node.href).
+ // If imports are paths, we can't easily convert to ID without extra info.
+ // BUT, current usage of imports in RawStyleInfo is strictly ID-based.
+ // If the input JSON has hrefs, we might skip imports or error.
+ // For now, I will omit imports if they are strings, or try to parse if they look like IDs.
+ // Actually, in the ecosystem, imports might not be fully supported in JSON mode yet or resolved differently.
+ // I will log or ignore for now, focusing on Rules.
+ }
+
+ // Handle rules
+ for (const rule of info.rules) {
+ const wasmRule = new wasmInstance.Rule('StyleRule');
+
+ // Declarations
+ for (const [prop, val] of rule.decl) {
+ wasmRule.push_declaration(prop, val);
+ }
+
+ // Selectors
+ const prelude = new wasmInstance.RulePrelude();
+ for (const selectorChain of rule.sel) {
+ const selector = new wasmInstance.Selector();
+
+ // Iterate in chunks of 4
+ for (let i = 0; i < selectorChain.length; i += 4) {
+ const plain = selectorChain[i] || [];
+ const pseudoClass = selectorChain[i + 1] || [];
+ const pseudoElement = selectorChain[i + 2] || [];
+ const combinator = selectorChain[i + 3] || [];
+
+ for (const s of plain) {
+ parseAndPushSelector(selector, s);
+ }
+ for (const s of pseudoClass) {
+ // Strip leading :
+ const val = s.startsWith(':') ? s.substring(1) : s;
+ selector.push_one_selector_section('PseudoClassSelector', val);
+ }
+ for (const s of pseudoElement) {
+ // Strip leading ::
+ const val = s.startsWith('::')
+ ? s.substring(2)
+ : s.startsWith(':')
+ ? s.substring(1)
+ : s;
+ selector.push_one_selector_section('PseudoElementSelector', val);
+ }
+ if (combinator.length > 0) {
+ selector.push_one_selector_section('Combinator', combinator[0]!);
+ }
+ }
+ prelude.push_selector(selector);
+ }
+ wasmRule.set_prelude(prelude);
+ rawStyleInfo.push_rule(cssId, wasmRule);
+ }
+ }
+
+ // Use the new Rust method
+ return wasmInstance.DecodedStyleData.encode_from_raw_style_info(
+ rawStyleInfo,
+ configEnableCssSelector,
+ entryName,
+ );
+}
+
+function parseAndPushSelector(selector: any, s: string) {
+ if (s.startsWith('.')) {
+ selector.push_one_selector_section('ClassSelector', s.substring(1));
+ } else if (s.startsWith('#')) {
+ selector.push_one_selector_section('IdSelector', s.substring(1));
+ } else if (s.startsWith('[')) {
+ // Attribute: [attr=val]
+ // Remove enclosing []
+ const content = s.substring(1, s.length - 1);
+ selector.push_one_selector_section('AttributeSelector', content);
+ } else if (s === '*') {
+ selector.push_one_selector_section('UniversalSelector', '*');
+ } else {
+ // Type selector
+ // It comes as [lynx-tag="div"] usually.
+ let content = s;
+ if (s.startsWith('[') && s.endsWith(']')) {
+ content = s.substring(1, s.length - 1);
+ }
+ selector.push_one_selector_section('AttributeSelector', content);
+ }
+}
diff --git a/packages/web-platform/web-core-wasm/ts/client/decodeWorker/decode.worker.ts b/packages/web-platform/web-core-wasm/ts/client/decodeWorker/decode.worker.ts
index d98851c982..e7297699e2 100644
--- a/packages/web-platform/web-core-wasm/ts/client/decodeWorker/decode.worker.ts
+++ b/packages/web-platform/web-core-wasm/ts/client/decodeWorker/decode.worker.ts
@@ -1,5 +1,6 @@
import { TemplateSectionLabel, MagicHeader } from '../../constants.js';
import type { InitMessage, LoadTemplateMessage, MainMessage } from './types.js';
+
import { DecodedStyle, wasmInstance } from '../wasm.js';
import type { PageConfig } from '../../types/PageConfig.js';
@@ -8,6 +9,8 @@ const wasmModuleLoadedPromise: Promise = new Promise((resolve) => {
wasmModuleLoadedResolve = resolve;
});
+import { loadStyleFromJSON } from './cssLoader.js';
+
class StreamReader {
#reader: ReadableStreamDefaultReader;
#buffer: Uint8Array = new Uint8Array(0);
@@ -51,6 +54,24 @@ class StreamReader {
this.#buffer = this.#buffer.slice(size);
return result;
}
+
+ async readRest(): Promise {
+ while (true) {
+ const { done, value } = await this.#reader.read();
+ if (value) {
+ const newBuffer = new Uint8Array(this.#buffer.length + value.length);
+ newBuffer.set(this.#buffer);
+ newBuffer.set(value, this.#buffer.length);
+ this.#buffer = newBuffer;
+ }
+ if (done) {
+ break;
+ }
+ }
+ const result = this.#buffer;
+ this.#buffer = new Uint8Array(0);
+ return result;
+ }
}
function decodeJSONMap(buffer: Uint8Array): Record {
@@ -159,6 +180,17 @@ async function handleStream(
if (!headerBytes) {
throw new Error('Empty stream');
}
+
+ // Check if JSON (starts with {)
+ if (headerBytes[0] === 123) {
+ const rest = await streamReader.readRest();
+ const decoder = new TextDecoder();
+ const jsonStr = decoder.decode(headerBytes) + decoder.decode(rest);
+ const json = JSON.parse(jsonStr);
+ await handleJSON(json, url, overrideConfig);
+ return;
+ }
+
const view = new DataView(
headerBytes.buffer,
headerBytes.byteOffset,
@@ -303,4 +335,115 @@ async function handleStream(
}
}
+async function handleJSON(
+ json: any,
+ url: string,
+ overrideConfig?: Partial,
+) {
+ // Configurations
+ let config: Partial = {};
+ if (json.pageConfig) {
+ config = { ...json.pageConfig };
+ }
+ if (overrideConfig) {
+ config = { ...config, ...overrideConfig };
+ }
+ postMessage({
+ type: 'section',
+ label: TemplateSectionLabel.Configurations,
+ url,
+ data: config,
+ } as MainMessage);
+
+ // StyleInfo
+ if (json.styleInfo) {
+ await wasmModuleLoadedPromise;
+ const buffer = loadStyleFromJSON(
+ json.styleInfo,
+ config['enableCSSSelector'] === 'true',
+ config['isLazy'] === 'true' ? url : undefined,
+ );
+ postMessage(
+ {
+ type: 'section',
+ label: TemplateSectionLabel.StyleInfo,
+ url,
+ data: buffer.buffer,
+ config,
+ } as MainMessage,
+ {
+ transfer: [buffer.buffer],
+ },
+ );
+ }
+
+ // LepusCode
+ if (json.lepusCode) {
+ // Flattened structure in json: { root: "...", chunk1: "..." }
+ const isLazy = config['isLazy'] === 'true';
+ const blobMap: Record = {};
+ for (const [key, code] of Object.entries(json.lepusCode)) {
+ if (typeof code !== 'string') continue;
+ const prefix =
+ `(function(){ "use strict"; const navigator=void 0,postMessage=void 0,window=void 0; ${
+ isLazy ? 'module.exports=' : ''
+ } `;
+ const suffix = ` \n })()\n//# sourceURL=${url}/${key}\n`;
+ const blob = new Blob([prefix, code, suffix], {
+ type: 'text/javascript;',
+ });
+ blobMap[key] = URL.createObjectURL(blob);
+ }
+ postMessage({
+ type: 'section',
+ label: TemplateSectionLabel.LepusCode,
+ url,
+ data: blobMap,
+ config,
+ } as MainMessage);
+ }
+
+ // Manifest
+ if (json.manifest) {
+ const blobMap: Record = {};
+ for (const [key, code] of Object.entries(json.manifest)) {
+ if (typeof code !== 'string') continue;
+ const blob = new Blob([code], {
+ type: 'text/javascript;',
+ });
+ blobMap[key] = URL.createObjectURL(blob);
+ }
+ postMessage({
+ type: 'section',
+ label: TemplateSectionLabel.Manifest,
+ url,
+ data: blobMap,
+ } as MainMessage);
+ }
+
+ // CustomSections
+ if (json.customSections) {
+ // Currently we don't have a way to encode custom sections here.
+ // If main thread accepts generic object, we send it.
+ // But TemplateManager expects buffer?
+ // TemplateManager: case CustomSections: #setCustomSection(url, data). data: any.
+ // So passing object is fine!
+ postMessage({
+ type: 'section',
+ label: TemplateSectionLabel.CustomSections,
+ url,
+ data: json.customSections,
+ } as MainMessage);
+ }
+
+ // ElementTemplates
+ if (json.elementTemplates) {
+ // TemplateManager expects Uint8Array for ElementTemplates.
+ // We can't support this easily for JSON.
+ throw new Error(
+ 'ElementTemplates in JSON artifacts are not supported yet.',
+ );
+ }
+}
+
postMessage({ type: 'ready' } as MainMessage);
diff --git a/packages/web-platform/web-core-wasm/ts/client/mainthread/Background.ts b/packages/web-platform/web-core-wasm/ts/client/mainthread/Background.ts
index 9668c3b60e..d9d522e397 100644
--- a/packages/web-platform/web-core-wasm/ts/client/mainthread/Background.ts
+++ b/packages/web-platform/web-core-wasm/ts/client/mainthread/Background.ts
@@ -86,6 +86,10 @@ export class BackgroundThread implements AsyncDisposable {
readonly #lynxGroupId: number | undefined;
readonly #lynxViewInstance: LynxViewInstance;
+ readonly #btsReady: Promise;
+
+ #btsReadyResolver!: () => void;
+
#btsStarted = false;
constructor(
@@ -101,6 +105,9 @@ export class BackgroundThread implements AsyncDisposable {
receiveEventEndpoint: dispatchJSContextOnMainThreadEndpoint,
sendEventEndpoint: dispatchCoreContextOnBackgroundEndpoint,
});
+ this.#btsReady = new Promise((resolve) => {
+ this.#btsReadyResolver = resolve;
+ });
this.jsContext.__start();
this.#batchSendTimingInfo = this.#rpc.createCall(markTimingEndpoint);
this.postTimingFlags = this.#rpc.createCall(postTimingFlagsEndpoint);
@@ -195,7 +202,7 @@ export class BackgroundThread implements AsyncDisposable {
this.#lynxViewInstance.reportError(
e,
release,
- this.#lynxViewInstance.templateUrl,
+ 'app-service.js',
);
},
);
@@ -241,7 +248,9 @@ export class BackgroundThread implements AsyncDisposable {
this.#lynxViewInstance,
);
- this.#rpc.invoke(BackgroundThreadStartEndpoint, []);
+ this.#rpc.invoke(BackgroundThreadStartEndpoint, []).then(
+ this.#btsReadyResolver,
+ );
}
markTiming(
@@ -252,7 +261,7 @@ export class BackgroundThread implements AsyncDisposable {
this.#caughtTimingInfo.push({
timingKey,
pipelineId,
- timeStamp: timeStamp ?? performance.now(),
+ timeStamp: timeStamp ?? (performance.now() + performance.timeOrigin),
});
if (this.#nextMacroTask === null) {
this.#nextMacroTask = setTimeout(() => {
@@ -273,7 +282,16 @@ export class BackgroundThread implements AsyncDisposable {
}
}
- [Symbol.asyncDispose](): Promise {
+ async [Symbol.asyncDispose](): Promise {
+ await this.#btsReady;
+ /*
+ * TODO:
+ * Potential deadlock if startBTS() was never called.
+ * If [Symbol.asyncDispose]() is invoked on a BackgroundThread instance where startBTS() was never called,
+ * #btsReady will never resolve, causing the disposal to hang indefinitely.
+ * Consider guarding with the existing #btsStarted flag.
+ */
+ await this.#rpc.invoke(disposeEndpoint, []);
if (this.#lynxGroupId !== undefined) {
const group =
BackgroundThread.contextIdToBackgroundWorker[this.#lynxGroupId];
@@ -290,6 +308,5 @@ export class BackgroundThread implements AsyncDisposable {
this.#webWorker?.terminate();
}
this.#nextMacroTask && clearTimeout(this.#nextMacroTask);
- return this.#rpc.invoke(disposeEndpoint, []);
}
}
diff --git a/packages/web-platform/web-core-wasm/ts/client/mainthread/ExposureServices.ts b/packages/web-platform/web-core-wasm/ts/client/mainthread/ExposureServices.ts
index eef3ea951e..26812dd3de 100644
--- a/packages/web-platform/web-core-wasm/ts/client/mainthread/ExposureServices.ts
+++ b/packages/web-platform/web-core-wasm/ts/client/mainthread/ExposureServices.ts
@@ -50,47 +50,21 @@ export class ExposureServices {
*/
updateExposureStatus(
elementsToBeEnabled: HTMLElement[],
+ elementsToBeDisabled: HTMLElement[],
) {
const elementsToBeEnabledSet = new Set(elementsToBeEnabled);
- for (
- const element of this.#exposureEnabledElementsToIntersectionObserver
- .keys()
- ) {
- if (!elementsToBeEnabledSet.has(element)) {
- // stop observing elements that are no longer enabled
- this.#exposureEnabledElementsToIntersectionObserver.get(element)
- ?.disconnect();
- this.#exposureEnabledElementsToIntersectionObserver.delete(element);
- this.#exposureEnabledElementsToOldExposureIdAttributeValue.delete(
- element,
- );
- }
- }
// start observing newly enabled elements
- elementsToBeEnabledSet.forEach((element) => {
- const currentExposureId = element.getAttribute('exposure-id') || '';
- if (!this.#exposureEnabledElementsToIntersectionObserver.has(element)) {
- this.#exposureEnabledElementsToOldExposureIdAttributeValue.set(
- element,
- currentExposureId,
- );
- this.#startIntersectionObserver(element);
- } else {
- // check if exposure-id attribute has changed
- const oldExposureId = this
- .#exposureEnabledElementsToOldExposureIdAttributeValue.get(element);
- if (oldExposureId !== currentExposureId) {
- this.#exposureEnabledElementsToOldExposureIdAttributeValue.set(
- element,
- currentExposureId,
- );
- if (oldExposureId != null) {
- // send disexposure event with old exposure id
- this.#sendExposureEvent(element, false, oldExposureId, false);
- }
- }
+ for (const element of elementsToBeEnabledSet.values()) {
+ if (this.#exposureEnabledElementsToIntersectionObserver.has(element)) {
+ this.#stopIntersectionObserver(element);
}
- });
+ this.#startIntersectionObserver(element);
+ }
+ const elementsToBeDisabledSet = new Set(elementsToBeDisabled);
+ // stop observing newly disabled elements
+ for (const element of elementsToBeDisabledSet.values()) {
+ this.#stopIntersectionObserver(element);
+ }
}
#IntersectionObserverEventHandler = (
@@ -109,6 +83,26 @@ export class ExposureServices {
});
};
+ #stopIntersectionObserver(element: HTMLElement) {
+ const intersectionObserver = this
+ .#exposureEnabledElementsToIntersectionObserver.get(element);
+ if (intersectionObserver) {
+ const oldExposureId = this
+ .#exposureEnabledElementsToOldExposureIdAttributeValue.get(element);
+ intersectionObserver.unobserve(element);
+ intersectionObserver.disconnect();
+ this.#exposureEnabledElementsToIntersectionObserver.delete(element);
+ this.#exposureEnabledElementsToOldExposureIdAttributeValue.delete(
+ element,
+ );
+ const currentExposureId = element.getAttribute('exposure-id');
+ if (oldExposureId != null && currentExposureId !== oldExposureId) {
+ this.#sendExposureEvent(element, false, oldExposureId, false);
+ }
+ }
+ this.#exposedElements.delete(element);
+ }
+
#startIntersectionObserver(target: HTMLElement) {
const threshold = parseFloat(target.getAttribute('exposure-area') ?? '0')
/ 100;
@@ -182,6 +176,13 @@ export class ExposureServices {
target,
intersectionObserver,
);
+ const currentExposureId = target.getAttribute('exposure-id');
+ if (currentExposureId != null) {
+ this.#exposureEnabledElementsToOldExposureIdAttributeValue.set(
+ target,
+ currentExposureId,
+ );
+ }
}
#sendExposureEvent(
@@ -226,7 +227,7 @@ export class ExposureServices {
?? ({} as CloneableObject),
);
const globalEvent: GlobalExposureEvent = {
- ...serializedTargetInfo.dataset,
+ dataset: serializedTargetInfo.dataset,
...detail,
type: isIntersecting ? 'exposure' : 'disexposure',
target: serializedTargetInfo,
@@ -252,15 +253,22 @@ export class ExposureServices {
const currentDisexposureEvents = this.#globalDisexposureEventCache;
this.#globalExposureEventCache = [];
this.#globalDisexposureEventCache = [];
- this.#lynxViewInstance.backgroundThread?.sendGlobalEvent('exposure', [
- currentExposureEvents,
- ]);
- this.#lynxViewInstance.backgroundThread?.sendGlobalEvent(
- 'disexposure',
- [
- currentDisexposureEvents,
- ],
- );
+ if (currentExposureEvents.length > 0) {
+ this.#lynxViewInstance.backgroundThread?.sendGlobalEvent(
+ 'exposure',
+ [
+ currentExposureEvents,
+ ],
+ );
+ }
+ if (currentDisexposureEvents.length > 0) {
+ this.#lynxViewInstance.backgroundThread?.sendGlobalEvent(
+ 'disexposure',
+ [
+ currentDisexposureEvents,
+ ],
+ );
+ }
}
this.#globalExposureEventBatchTimer = null;
}, 1000 / 20);
diff --git a/packages/web-platform/web-core-wasm/ts/client/mainthread/I18n.ts b/packages/web-platform/web-core-wasm/ts/client/mainthread/I18n.ts
index 011807d708..f29e510403 100644
--- a/packages/web-platform/web-core-wasm/ts/client/mainthread/I18n.ts
+++ b/packages/web-platform/web-core-wasm/ts/client/mainthread/I18n.ts
@@ -69,6 +69,8 @@ export class I18nManager {
) {
const event = new CustomEvent(i18nResourceMissedEventName, {
detail: options as CloneableObject,
+ bubbles: true,
+ composed: true,
});
this.#rootDom.dispatchEvent(event);
}
diff --git a/packages/web-platform/web-core-wasm/ts/client/mainthread/LynxView.ts b/packages/web-platform/web-core-wasm/ts/client/mainthread/LynxView.ts
index cf646ca487..f5c90b0fba 100644
--- a/packages/web-platform/web-core-wasm/ts/client/mainthread/LynxView.ts
+++ b/packages/web-platform/web-core-wasm/ts/client/mainthread/LynxView.ts
@@ -92,6 +92,41 @@ export class LynxViewElement extends HTMLElement {
#connected = false;
#url?: string;
+
+ /**
+ * @public
+ * @property nativeModulesMap
+ * @default {}
+ */
+ nativeModulesMap: NativeModulesMap | undefined;
+
+ /**
+ * @param
+ * @property napiModulesMap
+ * @default {}
+ */
+ napiModulesMap: NapiModulesMap | undefined;
+
+ /**
+ * @param
+ * @property
+ */
+ onNapiModulesCall: NapiModulesCall | undefined;
+
+ constructor() {
+ super();
+ if (!this.onNativeModulesCall) {
+ this.onNativeModulesCall = (name, data, moduleName) => {
+ return new Promise((resolve) => {
+ this.#cachedNativeModulesCall.push({
+ args: [name, data, moduleName],
+ resolve,
+ });
+ });
+ };
+ }
+ }
+
/**
* @public
* @property the url of lynx view output entry file
@@ -208,46 +243,6 @@ export class LynxViewElement extends HTMLElement {
this.#cachedNativeModulesCall = [];
}
- #nativeModulesMap: NativeModulesMap = {};
- /**
- * @public
- * @property nativeModulesMap
- * @default {}
- */
- get nativeModulesMap(): NativeModulesMap | undefined {
- return this.#nativeModulesMap;
- }
- set nativeModulesMap(map: NativeModulesMap) {
- this.#nativeModulesMap = map;
- }
-
- #napiModulesMap: NapiModulesMap = {};
- /**
- * @param
- * @property napiModulesMap
- * @default {}
- */
- get napiModulesMap(): NapiModulesMap | undefined {
- return this.#napiModulesMap;
- }
- set napiModulesMap(map: NapiModulesMap) {
- this.#napiModulesMap = map;
- }
-
- #onNapiModulesCall?: NapiModulesCall;
- /**
- * @param
- * @property
- */
- get onNapiModulesCall(): NapiModulesCall | undefined {
- return this.#onNapiModulesCall;
- }
- set onNapiModulesCall(handler: INapiModulesCall) {
- this.#onNapiModulesCall = (name, data, moduleName, dispatchNapiModules) => {
- return handler(name, data, moduleName, this, dispatchNapiModules);
- };
- }
-
/**
* @param
* @property
@@ -342,24 +337,33 @@ export class LynxViewElement extends HTMLElement {
}
}
- public injectStyleRules: string[] = [];
+ public injectStyleRules?: string[];
/**
* @private
*/
disconnectedCallback() {
- this.#instance?.[Symbol.asyncDispose]();
- this.#instance = undefined;
- // under the all-on-ui strategy, when reload() triggers dsl flush, the previously removed pageElement will be used in __FlushElementTree.
- // This attribute is added to filter this issue.
+ /* TODO:
+ * Await async disposal before re-rendering to prevent concurrent instance mutations.
+
+ Currently disconnectedCallback() triggers asyncDispose() without awaiting, allowing #render() to immediately create a new instance while the old one is still cleaning up on the background thread. This causes both instances to render into the shadowRoot concurrently, producing multiple page elements.
+
+ The basic-reload-page-only-one test confirms this issue by checking that exactly one page element exists after reload. The disposal must complete before the new instance begins rendering.
+
+ Extract an async #disposeInstance() method that marks the old page as disposed, awaits the instance cleanup, clears the shadowRoot, and resets adoptedStyleSheets to prevent stylesheet accumulation. Then await this in the microtask before instantiating the new LynxViewInstance.
+
+ This also fixes a secondary bug where lynxGroupId is referenced before declaration.
+ */
this.shadowRoot?.querySelector('[part="page"]')
?.setAttribute(
lynxDisposedAttribute,
'',
);
+ this.#instance?.[Symbol.asyncDispose]();
if (this.shadowRoot) {
this.shadowRoot.innerHTML = '';
}
+ this.#instance = undefined;
}
/**
@@ -381,6 +385,14 @@ export class LynxViewElement extends HTMLElement {
}
const mtsRealmPromise = createIFrameRealm(this.shadowRoot!);
queueMicrotask(async () => {
+ if (this.injectStyleRules && this.injectStyleRules.length > 0) {
+ const styleSheet = new CSSStyleSheet();
+ for (const rule of this.injectStyleRules) {
+ styleSheet.insertRule(rule);
+ }
+ this.shadowRoot!.adoptedStyleSheets = this.shadowRoot!
+ .adoptedStyleSheets.concat(styleSheet);
+ }
const mtsRealm = await mtsRealmPromise;
if (this.#url) {
const lynxViewInstance = import(
@@ -396,8 +408,8 @@ export class LynxViewElement extends HTMLElement {
this.shadowRoot!,
mtsRealm,
lynxGroupId,
- this.#nativeModulesMap,
- this.#napiModulesMap,
+ this.nativeModulesMap,
+ this.napiModulesMap,
this.#initI18nResources,
);
});
diff --git a/packages/web-platform/web-core-wasm/ts/client/mainthread/LynxViewInstance.ts b/packages/web-platform/web-core-wasm/ts/client/mainthread/LynxViewInstance.ts
index b01a454200..aecdaacfcc 100644
--- a/packages/web-platform/web-core-wasm/ts/client/mainthread/LynxViewInstance.ts
+++ b/packages/web-platform/web-core-wasm/ts/client/mainthread/LynxViewInstance.ts
@@ -24,7 +24,10 @@ import { ExposureServices } from './ExposureServices.js';
import { createElementAPI } from './elementAPIs/createElementAPI.js';
import { createMainThreadGlobalAPIs } from './createMainThreadGlobalAPIs.js';
import { templateManager } from './TemplateManager.js';
-import { loadWebElement } from '../webElementsDynamicLoader.js';
+import {
+ loadAllWebElements,
+ loadWebElement,
+} from '../webElementsDynamicLoader.js';
import type { LynxViewElement } from './LynxView.js';
import { StyleManager } from './StyleManager.js';
@@ -35,8 +38,8 @@ export const systemInfo = Object.freeze({
...systemInfoBase,
// some information only available on main thread, we should read and pass to worker
pixelRatio,
- screenWidth,
- screenHeight,
+ pixelWidth: screenWidth,
+ pixelHeight: screenHeight,
});
export interface LynxViewConfigs {
@@ -107,6 +110,7 @@ export class LynxViewInstance implements AsyncDisposable {
this.exposureServices = new ExposureServices(
this,
);
+ this.backgroundThread.markTiming('create_lynx_start');
}
onPageConfigReady(config: PageConfig) {
@@ -158,6 +162,7 @@ export class LynxViewInstance implements AsyncDisposable {
}
onMTSScriptsLoaded(currentUrl: string, isLazy: boolean) {
+ this.backgroundThread.markTiming('lepus_execute_start');
const urlMap = templateManager.getTemplate(currentUrl)
?.lepusCode as Record;
this.lepusCodeUrls.set(
@@ -172,14 +177,24 @@ export class LynxViewInstance implements AsyncDisposable {
}
async onMTSScriptsExecuted() {
+ this.backgroundThread.markTiming('lepus_execute_end');
+
+ if (
+ !templateManager.getTemplate(this.templateUrl)?.elementTemplates
+ ) {
+ this.webElementsLoadingPromises.push(loadAllWebElements());
+ }
+
await Promise.all([
...this.webElementsLoadingPromises,
this.styleReadyPromise,
]);
this.webElementsLoadingPromises.length = 0;
+ this.backgroundThread.markTiming('data_processor_start');
const processedData = this.mainThreadGlobalThis.processData
- ? this.mainThreadGlobalThis.processData(this.initData)
+ ? this.mainThreadGlobalThis.processData?.(this.initData)
: this.initData;
+ this.backgroundThread.markTiming('data_processor_end');
this.backgroundThread.startWebWorker(
processedData,
this.globalprops,
@@ -285,6 +300,9 @@ export class LynxViewInstance implements AsyncDisposable {
release,
fileName,
},
+ bubbles: true,
+ cancelable: true,
+ composed: true,
}),
);
}
diff --git a/packages/web-platform/web-core-wasm/ts/client/mainthread/TemplateManager.ts b/packages/web-platform/web-core-wasm/ts/client/mainthread/TemplateManager.ts
index aed9ab4f14..6b1cd5d617 100644
--- a/packages/web-platform/web-core-wasm/ts/client/mainthread/TemplateManager.ts
+++ b/packages/web-platform/web-core-wasm/ts/client/mainthread/TemplateManager.ts
@@ -38,8 +38,6 @@ export class TemplateManager {
#worker: Worker | null = null;
#workerReadyPromise: Promise | null = null;
#resolveWorkerReady: (() => void) | null = null;
- #activeUrls: Set = new Set();
- #terminateTimer: ReturnType | null = null;
constructor() {
this.#ensureWorker();
@@ -55,6 +53,7 @@ export class TemplateManager {
const template = this.#templates.get(url);
const config = (template?.config || {}) as PageConfig;
const lynxViewInstance = await lynxViewInstancePromise;
+ lynxViewInstance.backgroundThread.markTiming('decode_start');
lynxViewInstance.onPageConfigReady(config);
const styleInfo = template?.styleInfo;
if (styleInfo) {
@@ -74,12 +73,15 @@ export class TemplateManager {
lynxViewInstancePromise: Promise,
overrideConfig?: Partial,
): Promise {
- this.#activeUrls.add(url);
+ const currentTime = performance.now() + performance.timeOrigin;
+ lynxViewInstancePromise.then((instance) => {
+ instance.backgroundThread.markTiming(
+ 'fetch_start',
+ undefined,
+ currentTime,
+ );
+ });
this.#lynxViewInstancesMap.set(url, lynxViewInstancePromise);
- if (this.#terminateTimer) {
- clearTimeout(this.#terminateTimer);
- this.#terminateTimer = null;
- }
await this.#ensureWorker();
@@ -171,7 +173,14 @@ export class TemplateManager {
break;
case 'done':
this.#cleanup(url);
- this.#resolvePromise(url);
+ /* TODO: The promise resolution is deferred inside .then() without error handling.
+ *
+ */
+ lynxViewInstancePromise.then((instance) => {
+ instance.backgroundThread.markTiming('decode_end');
+ instance.backgroundThread.markTiming('load_template_start');
+ this.#resolvePromise(url);
+ });
break;
}
}
@@ -193,6 +202,7 @@ export class TemplateManager {
const { label, data, url, config } = msg;
switch (label) {
case TemplateSectionLabel.Configurations: {
+ instance.backgroundThread.markTiming('decode_start');
this.#setConfig(url, data);
instance.onPageConfigReady(data);
break;
@@ -234,18 +244,7 @@ export class TemplateManager {
}
#cleanup(url: string) {
- this.#activeUrls.delete(url);
this.#lynxViewInstancesMap.delete(url);
- if (this.#activeUrls.size === 0) {
- this.#terminateTimer = setTimeout(() => {
- if (this.#activeUrls.size === 0 && this.#worker) {
- this.#worker.terminate();
- this.#worker = null;
- this.#workerReadyPromise = null;
- this.#resolveWorkerReady = null;
- }
- }, 10000);
- }
}
createTemplate(url: string) {
diff --git a/packages/web-platform/web-core-wasm/ts/client/mainthread/elementAPIs/WASMJSBinding.ts b/packages/web-platform/web-core-wasm/ts/client/mainthread/elementAPIs/WASMJSBinding.ts
index bbcb88bc3e..2d05e4e2d7 100644
--- a/packages/web-platform/web-core-wasm/ts/client/mainthread/elementAPIs/WASMJSBinding.ts
+++ b/packages/web-platform/web-core-wasm/ts/client/mainthread/elementAPIs/WASMJSBinding.ts
@@ -31,6 +31,7 @@ export class WASMJSBinding implements RustMainthreadContextBinding {
wasmContext: InstanceType | undefined;
uniqueIdToElement: (HTMLElement | undefined)[] = [undefined];
toBeEnabledElement: Set = new Set();
+ toBeDisabledElement: Set = new Set();
constructor(
public readonly lynxViewInstance: WASMJSBindingInjectedHandler,
@@ -49,9 +50,11 @@ export class WASMJSBinding implements RustMainthreadContextBinding {
const dom = this.uniqueIdToElement[uniqueId];
if (dom) {
if (toEnable) {
+ this.toBeDisabledElement.delete(dom);
this.toBeEnabledElement.add(dom);
} else {
this.toBeEnabledElement.delete(dom);
+ this.toBeDisabledElement.add(dom);
}
}
}
@@ -201,9 +204,27 @@ export class WASMJSBinding implements RustMainthreadContextBinding {
updateExposureStatus(
enabledExposureElements: HTMLElement[],
+ disabledExposureElements: HTMLElement[],
) {
this.lynxViewInstance.exposureServices.updateExposureStatus(
enabledExposureElements,
+ disabledExposureElements,
);
}
+
+ enableElementEvent(uniqueId: number, eventName: string) {
+ const element = this.getElementByUniqueId(uniqueId);
+ if (element) {
+ // @ts-expect-error
+ element.enableEvent?.(LynxEventNameToW3cCommon[eventName] ?? eventName);
+ }
+ }
+
+ disableElementEvent(uniqueId: number, eventName: string) {
+ const element = this.getElementByUniqueId(uniqueId);
+ if (element) {
+ // @ts-expect-error
+ element.disableEvent?.(LynxEventNameToW3cCommon[eventName] ?? eventName);
+ }
+ }
}
diff --git a/packages/web-platform/web-core-wasm/ts/client/mainthread/elementAPIs/createElementAPI.ts b/packages/web-platform/web-core-wasm/ts/client/mainthread/elementAPIs/createElementAPI.ts
index b0deb60102..49848c23f1 100644
--- a/packages/web-platform/web-core-wasm/ts/client/mainthread/elementAPIs/createElementAPI.ts
+++ b/packages/web-platform/web-core-wasm/ts/client/mainthread/elementAPIs/createElementAPI.ts
@@ -2,12 +2,14 @@ import { wasmInstance } from '../../wasm.js';
import { templateManager } from '../TemplateManager.js';
import {
+ LYNX_TIMING_FLAG_ATTRIBUTE,
lynxDisposedAttribute,
lynxDefaultDisplayLinearAttribute,
lynxEntryNameAttribute,
uniqueIdSymbol,
LYNX_TAG_TO_HTML_TAG_MAP,
cssIdAttribute,
+ lynxDefaultOverflowVisibleAttribute,
} from '../../../constants.js';
import {
__SwapElement,
@@ -134,6 +136,12 @@ export function createElementAPI(
frameworkCrossThreadIdentifier,
);
}
+ if (eventName === 'uiappear' || eventName === 'uidisappear') {
+ mtsBinding.markExposureRelatedElementByUniqueId(
+ uniqueId,
+ frameworkCrossThreadIdentifier != null,
+ );
+ }
};
return {
__CreateView(parentComponentUniqueId: number) {
@@ -248,7 +256,7 @@ export function createElementAPI(
componentID,
);
if (config_default_overflow_visible) {
- dom.setAttribute(lynxDefaultDisplayLinearAttribute, 'true');
+ dom.setAttribute(lynxDefaultOverflowVisibleAttribute, 'true');
}
if (!config_default_display_linear) {
dom.setAttribute(lynxDefaultDisplayLinearAttribute, 'false');
@@ -436,6 +444,8 @@ export function createElementAPI(
const uniqueId = (element as DecoratedHTMLElement)[uniqueIdSymbol];
mtsBinding.markExposureRelatedElementByUniqueId(uniqueId, false);
}
+ } else if (name === LYNX_TIMING_FLAG_ATTRIBUTE) {
+ timingFlags.push(String(value));
}
}
},
@@ -518,8 +528,13 @@ export function createElementAPI(
...mtsBinding.toBeEnabledElement,
];
mtsBinding.toBeEnabledElement.clear();
+ const disabledExposureElements = [
+ ...mtsBinding.toBeDisabledElement,
+ ];
+ mtsBinding.toBeDisabledElement.clear();
mtsBinding?.updateExposureStatus(
enabledExposureElements,
+ disabledExposureElements,
);
},
};
diff --git a/packages/web-platform/web-core-wasm/ts/client/webElementsDynamicLoader.ts b/packages/web-platform/web-core-wasm/ts/client/webElementsDynamicLoader.ts
index c5901a4796..9e1794b7fe 100644
--- a/packages/web-platform/web-core-wasm/ts/client/webElementsDynamicLoader.ts
+++ b/packages/web-platform/web-core-wasm/ts/client/webElementsDynamicLoader.ts
@@ -80,3 +80,14 @@ export function loadWebElement(id: number): Promise | undefined {
loadedWebElementsCSSIds.add(id);
});
}
+
+export function loadAllWebElements(): Promise {
+ const promises: Promise[] = [];
+ for (let i = 0; i <= 8; i++) {
+ const p = loadWebElement(i);
+ if (p) {
+ promises.push(p);
+ }
+ }
+ return Promise.all(promises) as Promise as Promise;
+}
diff --git a/packages/web-platform/web-core-wasm/ts/constants.ts b/packages/web-platform/web-core-wasm/ts/constants.ts
index fc86dbda3e..b21c97bbe8 100644
--- a/packages/web-platform/web-core-wasm/ts/constants.ts
+++ b/packages/web-platform/web-core-wasm/ts/constants.ts
@@ -21,7 +21,8 @@ export const lynxDefaultDisplayLinearAttribute = /*#__PURE__*/
export const lynxDefaultOverflowVisibleAttribute /*#__PURE__*/ =
'lynx-default-overflow-visible' as const;
-export const __lynx_timing_flag = /*#__PURE__*/ '__lynx_timing_flag' as const;
+export const LYNX_TIMING_FLAG_ATTRIBUTE =
+ /*#__PURE__*/ '__lynx_timing_flag' as const;
export const i18nResourceMissedEventName = 'i18nResourceMissed' as const;
@@ -42,15 +43,10 @@ export const W3cEventNameToLynx: Record = /*#__PURE__*/ {
lynxinput: 'input',
};
-export const LynxEventNameToW3cCommon: Record = /*#__PURE__*/ {
- tap: 'click',
- scroll: 'lynxscroll',
- scrollend: 'lynxscrollend',
- touch: 'overlaytouch',
- 'lynxblur': 'lynxblur',
- 'lynxfocus': 'lynxfocus',
- 'lynxinput': 'lynxinput',
-};
+export const LynxEventNameToW3cCommon: Record =
+ /*#__PURE__*/ Object.fromEntries(
+ Object.entries(W3cEventNameToLynx).map(([k, v]) => [v, k]),
+ );
export const MagicHeader = /*#__PURE__*/ 0x464F525741524453; // random magic number for verifying the stream is a Lynx encoded template
@@ -89,6 +85,7 @@ export const LYNX_TAG_TO_HTML_TAG_MAP: Record =
'page': 'div',
'input': 'x-input',
'x-input-ng': 'x-input',
+ 'svg': 'x-svg',
}),
);
diff --git a/packages/web-platform/web-core-wasm/ts/encode/webEncoder.ts b/packages/web-platform/web-core-wasm/ts/encode/webEncoder.ts
index 9ce70d5bb2..4fc01c68d3 100644
--- a/packages/web-platform/web-core-wasm/ts/encode/webEncoder.ts
+++ b/packages/web-platform/web-core-wasm/ts/encode/webEncoder.ts
@@ -82,7 +82,10 @@ export function encode(tasmJSON: TasmJSONInfo): Uint8Array {
elementTemplates,
} = tasmJSON;
const encodedStyleInfo = encodeCSS(styleInfo);
- const encodedElementTemplates = encodeElementTemplates(elementTemplates);
+ const hasElementTemplates = Object.keys(elementTemplates).length > 0;
+ const encodedElementTemplates = hasElementTemplates
+ ? encodeElementTemplates(elementTemplates)
+ : new Uint8Array(0);
const encodedManifest = encodeStringMap(manifest);
const encodedLepusCode = encodeStringMap(lepusCode);
@@ -103,7 +106,7 @@ export function encode(tasmJSON: TasmJSONInfo): Uint8Array {
/*section length*/
+ 4 + 4 + encodedConfigurations.length // Configurations
+ 4 + 4 + encodedStyleInfo.length // Style Info
- + 4 + 4 + encodedElementTemplates.length // Element Templates
+ + (hasElementTemplates ? 4 + 4 + encodedElementTemplates.length : 0) // Element Templates
+ 4 + 4 + encodedLepusCode.length // Lepus Code
+ 4 + 4 + encodedCustomSections.length // Custom Sections
+ 4 + 4 + encodedManifest.length // Manifest
@@ -128,12 +131,14 @@ export function encode(tasmJSON: TasmJSONInfo): Uint8Array {
buffer.set(encodedConfigurations, offset);
offset += encodedConfigurations.length;
// Element Templates
- dataView.setUint32(offset, TemplateSectionLabel.ElementTemplates, true); // section label
- offset += 4;
- dataView.setUint32(offset, encodedElementTemplates.length, true); // section length
- offset += 4;
- buffer.set(encodedElementTemplates, offset);
- offset += encodedElementTemplates.length;
+ if (hasElementTemplates) {
+ dataView.setUint32(offset, TemplateSectionLabel.ElementTemplates, true); // section label
+ offset += 4;
+ dataView.setUint32(offset, encodedElementTemplates.length, true); // section length
+ offset += 4;
+ buffer.set(encodedElementTemplates, offset);
+ offset += encodedElementTemplates.length;
+ }
// Lepus Code
dataView.setUint32(offset, TemplateSectionLabel.LepusCode, true); // section label
offset += 4;
diff --git a/packages/webpack/template-webpack-plugin/package.json b/packages/webpack/template-webpack-plugin/package.json
index 8a76e9c25b..74a1e52a55 100644
--- a/packages/webpack/template-webpack-plugin/package.json
+++ b/packages/webpack/template-webpack-plugin/package.json
@@ -46,6 +46,7 @@
"devDependencies": {
"@lynx-js/test-tools": "workspace:*",
"@lynx-js/vitest-setup": "workspace:*",
+ "@lynx-js/web-core-wasm": "workspace:*",
"@microsoft/api-extractor": "catalog:",
"@types/css-tree": "^2.3.11",
"@types/object.groupby": "^1.0.4",
diff --git a/packages/webpack/template-webpack-plugin/src/WebEncodePlugin.ts b/packages/webpack/template-webpack-plugin/src/WebEncodePlugin.ts
index f8993f694a..ab3885678b 100644
--- a/packages/webpack/template-webpack-plugin/src/WebEncodePlugin.ts
+++ b/packages/webpack/template-webpack-plugin/src/WebEncodePlugin.ts
@@ -4,6 +4,8 @@
import type { Compilation, Compiler } from 'webpack';
+import type { TasmJSONInfo } from '@lynx-js/web-core-wasm/encode';
+
import type { LynxStyleNode } from './css/index.js';
import {
LynxTemplatePlugin,
@@ -80,31 +82,58 @@ export class WebEncodePlugin {
return encodeOptions;
});
- hooks.encode.tap({
+ hooks.encode.tapPromise({
name: WebEncodePlugin.name,
stage: WebEncodePlugin.ENCODE_HOOK_STAGE,
- }, ({ encodeOptions }) => {
- return {
- buffer: Buffer.from(JSON.stringify({
- styleInfo: genStyleInfo(
- (encodeOptions['css'] as {
- cssMap: Record;
- }).cssMap,
- ),
- manifest: encodeOptions.manifest,
- cardType: encodeOptions['cardType'],
- appType: encodeOptions['appType'],
- pageConfig: encodeOptions['pageConfig'],
- lepusCode: {
- // flatten the lepusCode to a single object
- ...encodeOptions.lepusCode.lepusChunk,
- root: encodeOptions.lepusCode.root,
- },
- customSections: encodeOptions.customSections,
- elementTemplate: encodeOptions['elementTemplate'],
- })),
- debugInfo: '',
+ }, async ({ encodeOptions }) => {
+ const tasmJSONInfo: Record = {
+ styleInfo: (encodeOptions['css'] as {
+ cssMap: Record;
+ }).cssMap,
+ manifest: encodeOptions.manifest as Record,
+ cardType: encodeOptions['cardType'] as string,
+ appType: encodeOptions['appType'] as string,
+ pageConfig: encodeOptions['pageConfig'] as Record,
+ lepusCode: {
+ // flatten the lepusCode to a single object
+ ...encodeOptions.lepusCode.lepusChunk,
+ root: encodeOptions.lepusCode.root!,
+ },
+ customSections: encodeOptions.customSections ?? {},
+ elementTemplates: encodeOptions['elementTemplates'] ?? {},
};
+ const isExperimentalWebBinary = !!process
+ .env['EXPERIMENTAL_USE_WEB_BINARY_TEMPLATE'];
+ if (isExperimentalWebBinary) {
+ const { encode } = await import('@lynx-js/web-core-wasm/encode')
+ .catch(
+ () => {
+ throw new Error(
+ `FLAG EXPERIMENTAL_USE_WEB_BINARY_TEMPLATE IS INTERNAL USED ONLY`,
+ );
+ },
+ );
+ return {
+ buffer: Buffer.from(encode(tasmJSONInfo as TasmJSONInfo)),
+ debugInfo: '',
+ };
+ } else {
+ return {
+ buffer: Buffer.from(
+ JSON.stringify({
+ ...tasmJSONInfo,
+ styleInfo: genStyleInfo(
+ tasmJSONInfo['styleInfo'] as Record<
+ string,
+ LynxStyleNode[]
+ >,
+ ),
+ }),
+ 'utf-8',
+ ),
+ debugInfo: '',
+ };
+ }
});
},
);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bf845ecfae..2d07188fa6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1573,6 +1573,9 @@ importers:
'@lynx-js/vitest-setup':
specifier: workspace:*
version: link:../../tools/vitest-setup
+ '@lynx-js/web-core-wasm':
+ specifier: workspace:*
+ version: link:../../web-platform/web-core-wasm
'@microsoft/api-extractor':
specifier: 'catalog:'
version: 7.55.2(@types/node@24.6.1)