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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/heavy-pigs-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/web-worker-runtime": patch
---

feat: return the offscreenDocument instance for startMainThread()
5 changes: 5 additions & 0 deletions .changeset/smart-drinks-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/offscreen-document": patch
---

feat: support OffscreenDocument.innerHTML
2 changes: 1 addition & 1 deletion .github/workflows/workflow-bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ jobs:
- name: Run benchmark
run: |
. "$HOME/.cargo/env"
codspeed run -- pnpm -r run bench
codspeed run -- pnpm -r run --workspace-concurrency=1 bench
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
OperationType,
type ElementOperation,
} from '../types/ElementOperation.js';
import { OffscreenElement } from './OffscreenElement.js';
import { _attributes, OffscreenElement } from './OffscreenElement.js';
import {
eventPhase,
OffscreenEvent,
Expand Down Expand Up @@ -131,4 +131,33 @@ export class OffscreenDocument extends OffscreenNode {
}
}
};

get innerHTML(): string {
const buffer: string[] = [];
for (const child of this.children) {
getInnerHTMLImpl(buffer, child as OffscreenElement);
}
return buffer.join('');
}
}

function getInnerHTMLImpl(buffer: string[], element: OffscreenElement): void {
const tagName = element.tagName.toLowerCase();
buffer.push('<');
buffer.push(tagName);
for (const [key, value] of Object.entries(element[_attributes])) {
buffer.push(' ');
buffer.push(key);
buffer.push('="');
buffer.push(value);
buffer.push('"');
}

buffer.push('>');
for (const child of element.children) {
getInnerHTMLImpl(buffer, child as OffscreenElement);
}
buffer.push('</');
buffer.push(tagName);
buffer.push('>');
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { OperationType } from '../types/ElementOperation.js';
import { OffscreenNode, uniqueId } from './OffscreenNode.js';

export const ancestorDocument = Symbol('ancestorDocument');
export const _attributes = Symbol('_attributes');
const _style = Symbol('_style');
const _attributes = Symbol('_attributes');
export class OffscreenElement extends OffscreenNode {
private [_style]?: OffscreenCSSStyleDeclaration;
private readonly [_attributes]: Record<string, string> = {};
Expand Down
1 change: 1 addition & 0 deletions packages/web-platform/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
{ "path": "./web-constants/tsconfig.json" },
{ "path": "./web-worker-rpc/tsconfig.json" },
{ "path": "./web-mainthread-apis/tsconfig.json" },
{ "path": "./web-core-server/tsconfig.json" },
{ "path": "./web-core/tsconfig.json" },
{ "path": "./web-rsbuild-plugin/tsconfig.json" },
/** packages-end */
Expand Down
3 changes: 3 additions & 0 deletions packages/web-platform/web-core-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build
Makefile
CMakeFiles
29 changes: 29 additions & 0 deletions packages/web-platform/web-core-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@lynx-js/web-core-server",
"version": "0.12.0",
"private": false,
"description": "",
"keywords": [],
"repository": {
"type": "git",
"url": "https://github.com/lynx-family/lynx-stack.git",
"directory": "packages/web-platform/web-core-server"
},
"license": "Apache-2.0",
"type": "module",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"!dist/**/*.js.map",
"LICENSE.txt",
"Notice.txt",
"CHANGELOG.md",
"README.md"
],
"devDependencies": {
"@lynx-js/web-constants": "workspace:*",
"@lynx-js/web-worker-rpc": "workspace:*",
"@lynx-js/web-worker-runtime": "workspace:*"
}
}
64 changes: 64 additions & 0 deletions packages/web-platform/web-core-server/src/createLynxView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
flushElementTreeEndpoint,
mainThreadStartEndpoint,
type MainThreadStartConfigs,
} from '@lynx-js/web-constants';
import { Rpc } from '@lynx-js/web-worker-rpc';
import { startMainThread } from '@lynx-js/web-worker-runtime';
import { loadTemplate } from './utils/loadTemplate.js';

interface LynxViewConfig extends
Pick<
MainThreadStartConfigs,
'browserConfig' | 'tagMap' | 'initData' | 'globalProps' | 'template'
>
{
}

export function createLynxView(
config: LynxViewConfig,
) {
const {
template: rawTemplate,
browserConfig,
tagMap,
initData,
globalProps,
} = config;

const mainToUIChannel = new MessageChannel();
const mainWithBackgroundChannel = new MessageChannel();
const mainToUIMessagePort = mainToUIChannel.port2;
const uiToMainRpc = new Rpc(mainToUIChannel.port1, 'main-to-ui');
const { docu: offscreenDocument } = startMainThread(
mainToUIMessagePort,
mainWithBackgroundChannel.port2,
);
const { promise: firstPaintReadyPromise, resolve: firstPaintReady } = Promise
.withResolvers<void>();
const template = loadTemplate(rawTemplate);
const mainThreadStart = uiToMainRpc.createCall(mainThreadStartEndpoint);
mainThreadStart({
template,
initData,
globalProps,
browserConfig,
nativeModulesMap: {}, // the bts won't start
napiModulesMap: {}, // the bts won't start
tagMap,
});
uiToMainRpc.registerHandler(
flushElementTreeEndpoint,
() => {
firstPaintReady();
},
);

async function renderToString(): Promise<string> {
await firstPaintReadyPromise;
return offscreenDocument.innerHTML;
}
return {
renderToString,
};
}
12 changes: 12 additions & 0 deletions packages/web-platform/web-core-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createLynxView } from './createLynxView.js';

export { createLynxView };

if (!globalThis.requestAnimationFrame) {
globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => {
return setTimeout(cb, 0);
};
globalThis.cancelAnimationFrame = (id: number) => {
clearTimeout(id);
};
}
143 changes: 143 additions & 0 deletions packages/web-platform/web-core-server/src/utils/loadTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { globalMuteableVars, type LynxTemplate } from '@lynx-js/web-constants';

const templateCache: Map<LynxTemplate, LynxTemplate> = new Map();

function createJsModuleUrl(content: string): string {
const dataUrl = `data:text/javascript,${encodeURIComponent(content)}`;
return dataUrl;
}

function generateJavascriptUrl<T extends Record<string, string>>(
obj: T,
injectVars: string[],
injectWithBind: string[],
muteableVars: readonly string[],
) {
injectVars = injectVars.concat(muteableVars);
return Object.fromEntries(
Object.entries(obj).map(([name, content]) => {
return [
name,
createJsModuleUrl(
[
'globalThis.module.exports = function(lynx_runtime) {',
'const module= {exports:{}};let exports = module.exports;',
'var {',
injectVars.join(','),
'} = lynx_runtime;',
...injectWithBind.map((nm) =>
`const ${nm} = lynx_runtime.${nm}?.bind(lynx_runtime);`
),
';var globDynamicComponentEntry = \'__Card__\';',
'var {__globalProps} = lynx;',
Comment thread
PupilTong marked this conversation as resolved.
'lynx_runtime._updateVars=()=>{',
...muteableVars.map((nm) =>
`${nm} = lynx_runtime.__lynxGlobalBindingValues.${nm};`
),
'};\n',
content,
'\n return module.exports;}',
].join(''),
),
];
}),
) as T;
}

const mainThreadInjectVars = [
'lynx',
'globalThis',
'_ReportError',
'__AddConfig',
'__AddDataset',
'__GetAttributes',
'__GetComponentID',
'__GetDataByKey',
'__GetDataset',
'__GetElementConfig',
'__GetElementUniqueID',
'__GetID',
'__GetTag',
'__SetAttribute',
'__SetConfig',
'__SetDataset',
'__SetID',
'__UpdateComponentID',
'__GetConfig',
'__UpdateListCallbacks',
'__AppendElement',
'__ElementIsEqual',
'__FirstElement',
'__GetChildren',
'__GetParent',
'__InsertElementBefore',
'__LastElement',
'__NextElement',
'__RemoveElement',
'__ReplaceElement',
'__ReplaceElements',
'__SwapElement',
'__CreateComponent',
'__CreateElement',
'__CreatePage',
'__CreateView',
'__CreateText',
'__CreateRawText',
'__CreateImage',
'__CreateScrollView',
'__CreateWrapperElement',
'__CreateList',
'__AddEvent',
'__GetEvent',
'__GetEvents',
'__SetEvents',
'__AddClass',
'__SetClasses',
'__GetClasses',
'__AddInlineStyle',
'__SetInlineStyles',
'__SetCSSId',
'__OnLifecycleEvent',
'__FlushElementTree',
'__LoadLepusChunk',
'SystemInfo',
];

const backgroundInjectVars = [
'NativeModules',
'globalThis',
'lynx',
'lynxCoreInject',
'SystemInfo',
];

const backgroundInjectWithBind = [
'Card',
'Component',
];

export function loadTemplate(
rawTemplate: LynxTemplate,
): LynxTemplate {
const decodedTemplate: LynxTemplate = templateCache.get(rawTemplate) ?? {
...rawTemplate,
lepusCode: generateJavascriptUrl(
rawTemplate.lepusCode,
mainThreadInjectVars,
[],
globalMuteableVars,
),
manifest: generateJavascriptUrl(
rawTemplate.manifest,
backgroundInjectVars,
backgroundInjectWithBind,
[],
),
};
templateCache.set(rawTemplate, decodedTemplate);
/**
* This will cause a memory leak, which is expected.
* We cannot ensure that the `URL.createObjectURL` created url will never be used, therefore here we keep it for the entire lifetime of this page.
*/
return decodedTemplate;
}
17 changes: 17 additions & 0 deletions packages/web-platform/web-core-server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ESNext", "WebWorker"],
"exactOptionalPropertyTypes": true,
},
"include": ["src"],
"references": [
{ "path": "../web-constants/tsconfig.json" },
{ "path": "../web-worker-rpc/tsconfig.json" },
{ "path": "../web-worker-runtime/tsconfig.json" },
{ "path": "../offscreen-document/tsconfig.json" },
],
}
2 changes: 2 additions & 0 deletions packages/web-platform/web-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
}
},
"scripts": {
"bench": "vitest bench",
"build": "if [ \"$CI\" = \"1\" ]; then pnpm build:cases; fi",
"build:cases": "rm -rf dist && node ./scripts/generate-build-command.js",
"coverage": "nyc report --cwd=$(realpath ../)",
Expand All @@ -29,6 +30,7 @@
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/web-constants": "workspace:*",
"@lynx-js/web-core": "workspace:*",
"@lynx-js/web-core-server": "workspace:*",
"@lynx-js/web-elements": "workspace:*",
"@lynx-js/web-elements-compat": "workspace:*",
"@lynx-js/web-elements-reactive": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions packages/web-platform/web-tests/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default defineConfig({
globalTimeout: 20 * 60 * 1000,
testDir: './tests',
testMatch: ALL_ON_UI ? '**/{react,web-core}.{test,spec}.ts' : undefined,
testIgnore: '**vitest**',
/* Run tests in files in parallel */
fullyParallel: true,
workers: isCI ? workerLimit : undefined,
Expand Down

Large diffs are not rendered by default.

Loading
Loading