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
18 changes: 18 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,22 @@ export default tseslint.config(
'headers/header-format': 'off',
},
},
{
files: [
'packages/react/worklet-runtime/src/*.ts',
'packages/react/worklet-runtime/src/*/*.ts',
'packages/react/worklet-runtime/src/*/*/*.ts',
],
languageOptions: {
parserOptions: {
project: './packages/react/worklet-runtime/tsconfig.eslint.json',
projectService: false,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'headers/header-format': 'off',
'import/export': 'off',
},
},
);
2 changes: 1 addition & 1 deletion packages/react/runtime/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function isEmptyObject(obj?: object): obj is Record<string, never> {
}

export function isSdkVersionGt(major: number, minor: number): boolean {
const lynxSdkVersion: string = SystemInfo.lynxSdkVersion || '1.0';
const lynxSdkVersion: string = SystemInfo.lynxSdkVersion ?? '1.0';
const version = lynxSdkVersion.split('.');
return Number(version[0]) > major || (Number(version[0]) == major && Number(version[1]) > minor);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2025 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 type { KeyframeEffect } from './effect.js';

export enum AnimationOperation {
START = 0, // Start a new animation
PLAY = 1, // Play/resume a paused animation
PAUSE = 2, // Pause an existing animation
CANCEL = 3, // Cancel an animation
}

export class Animation {
static count = 0;
public readonly effect: KeyframeEffect;
public readonly id: string;

constructor(effect: KeyframeEffect) {
this.effect = effect;
this.id = '__lynx-inner-js-animation-' + Animation.count++;
this.start();
}

public cancel(): void {
// @ts-expect-error accessing private member 'element'
return __ElementAnimate(this.effect.target.element, [AnimationOperation.CANCEL, this.id]);
}

public pause(): void {
// @ts-expect-error accessing private member 'element'
return __ElementAnimate(this.effect.target.element, [AnimationOperation.PAUSE, this.id]);
}

public play(): void {
// @ts-expect-error accessing private member 'element'
return __ElementAnimate(this.effect.target.element, [AnimationOperation.PLAY, this.id]);
}

private start(): void {
// @ts-expect-error accessing private member 'element'
return __ElementAnimate(this.effect.target.element, [
AnimationOperation.START,
this.id,
this.effect.keyframes,
this.effect.options,
]);
}
}
20 changes: 20 additions & 0 deletions packages/react/runtime/src/worklet-runtime/api/animation/effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2025 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 type { Element } from '../element.js';

export class KeyframeEffect {
public readonly target: Element;
public readonly keyframes: Record<string, number | string>[];
public readonly options: Record<string, number | string>;

constructor(
target: Element,
keyframes: Record<string, number | string>[],
options: Record<string, number | string>,
) {
this.target = target;
this.keyframes = keyframes;
this.options = options;
}
}
152 changes: 152 additions & 0 deletions packages/react/runtime/src/worklet-runtime/api/element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// 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.
import { Animation } from './animation/animation.js';
import { KeyframeEffect } from './animation/effect.js';
import {
mainThreadFlushLoopMark,
mainThreadFlushLoopOnFlushMicrotask,
mainThreadFlushLoopReport,
} from '../utils/mainThreadFlushLoopGuard.js';
import { isSdkVersionGt } from '../utils/version.js';

let willFlush = false;
let shouldFlush = true;

export function setShouldFlush(value: boolean): void {
shouldFlush = value;
}

export class Element {
// @ts-expect-error set in constructor
private readonly element: ElementNode;

constructor(element: ElementNode) {
// In Lynx versions prior to and including 2.15,
// a crash occurs when printing or transferring refCounted across threads.
// Bypass this problem by hiding the element object.
Object.defineProperty(this, 'element', {
get() {
return element;
},
});
}

public setAttribute(name: string, value: unknown): void {
/* v8 ignore next 3 */
if (__DEV__) {
mainThreadFlushLoopMark(`element:setAttribute ${name}`);
}
__SetAttribute(this.element, name, value);
this.flushElementTree();
}

public setStyleProperty(name: string, value: string): void {
/* v8 ignore next 3 */
if (__DEV__) {
mainThreadFlushLoopMark(`element:setStyleProperty ${name}`);
}
__AddInlineStyle(this.element, name, value);
this.flushElementTree();
}

public setStyleProperties(styles: Record<string, string>): void {
/* v8 ignore next 5 */
if (__DEV__) {
mainThreadFlushLoopMark(
`element:setStyleProperties keys=${Object.keys(styles).length}`,
);
}
for (const key in styles) {
__AddInlineStyle(this.element, key, styles[key]!);
}
this.flushElementTree();
}

public getAttribute(attributeName: string): unknown {
return __GetAttributeByName(this.element, attributeName);
}

public getAttributeNames(): string[] {
return __GetAttributeNames(this.element);
}

public querySelector(selector: string): Element | null {
const ref = __QuerySelector(this.element, selector, {});
return ref ? new Element(ref) : null;
}

public querySelectorAll(selector: string): Element[] {
return __QuerySelectorAll(this.element, selector, {}).map((element) => {
return new Element(element);
});
}

public getComputedStyleProperty(key: string): string {
if (!isSdkVersionGt(3, 4)) {
throw new Error(
'getComputedStyleProperty requires Lynx sdk version 3.5',
);
}

if (!key) {
throw new Error('getComputedStyleProperty: key is required');
}
return __GetComputedStyleByKey(this.element, key);
}

public animate(
keyframes: Record<string, number | string>[],
options?: number | Record<string, number | string>,
): Animation {
const normalizedOptions = typeof options === 'number' ? { duration: options } : options ?? {};
return new Animation(new KeyframeEffect(this, keyframes, normalizedOptions));
}

public invoke(
methodName: string,
params?: Record<string, unknown>,
): Promise<unknown> {
/* v8 ignore next 3 */
if (__DEV__) {
mainThreadFlushLoopMark(`element:invoke ${methodName}`);
}
return new Promise((resolve, reject) => {
__InvokeUIMethod(
this.element,
methodName,
params ?? {},
(res: { code: number; data: unknown }) => {
if (res.code === 0) {
resolve(res.data);
} else {
reject(new Error('UI method invoke: ' + JSON.stringify(res)));
}
},
);
this.flushElementTree();
});
}

private flushElementTree() {
if (willFlush || !shouldFlush) {
return;
}
willFlush = true;
void Promise.resolve().then(() => {
willFlush = false;
if (__DEV__) {
mainThreadFlushLoopMark('render');
const error = mainThreadFlushLoopOnFlushMicrotask();
if (error) {
// Stop scheduling further flushes so we can surface the error.
// This is DEV-only behavior guarded internally by the dev guard.
shouldFlush = false;
mainThreadFlushLoopReport(error);
return;
}
}
__FlushElementTree();
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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.
import { Element } from './element.js';

class PageElement {
private static pageElement: ElementNode | undefined;

static get() {
PageElement.pageElement ??= __GetPageElement();
return PageElement.pageElement;
}
}

export function querySelector(cssSelector: string): Element | null {
const element = __QuerySelector(PageElement.get(), cssSelector, {});
return element ? new Element(element) : null;
}

export function querySelectorAll(cssSelector: string): Element[] {
return __QuerySelectorAll(PageElement.get(), cssSelector, {}).map(
(element) => {
return new Element(element);
},
);
}
42 changes: 42 additions & 0 deletions packages/react/runtime/src/worklet-runtime/api/lynxApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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.
import { querySelector, querySelectorAll } from './lepusQuerySelector.js';
import { isSdkVersionGt } from '../utils/version.js';

function initApiEnv(): void {
// @ts-expect-error type
lynx.querySelector = querySelector;
// @ts-expect-error type
lynx.querySelectorAll = querySelectorAll;
// @ts-expect-error type
globalThis.setTimeout = lynx.setTimeout as (cb: () => void, timeout: number) => number;
// @ts-expect-error type
globalThis.setInterval = lynx.setInterval as (cb: () => void, timeout: number) => number;
// @ts-expect-error type
globalThis.clearTimeout = lynx.clearTimeout as (timeout: number) => void;
// In lynx 2.14 `clearInterval` is mistakenly spelled as `clearTimeInterval`. This is fixed in lynx 2.15.
// @ts-expect-error type
globalThis.clearInterval = (lynx.clearInterval ?? lynx.clearTimeInterval) as (timeout: number) => void;

{
// @ts-expect-error type
const requestAnimationFrame = lynx.requestAnimationFrame as (callback: () => void) => number;
// @ts-expect-error type
lynx.requestAnimationFrame = globalThis.requestAnimationFrame = (
callback: () => void,
) => {
if (!isSdkVersionGt(2, 15)) {
throw new Error(
'requestAnimationFrame in main thread script requires Lynx sdk version 2.16',
);
}
return requestAnimationFrame(callback);
};
}

// @ts-expect-error type
globalThis.cancelAnimationFrame = lynx.cancelAnimationFrame as (requestId: number) => void;
}

export { initApiEnv };
Loading
Loading