diff --git a/.changeset/better-crews-wash.md b/.changeset/better-crews-wash.md deleted file mode 100644 index dd25040030..0000000000 --- a/.changeset/better-crews-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/react-webpack-plugin": patch ---- - -feat: add `experimental_isLazyBundle` option, it will disable snapshot HMR for standalone lazy bundle diff --git a/.changeset/brave-ants-report.md b/.changeset/brave-ants-report.md deleted file mode 100644 index 78a3ae1929..0000000000 --- a/.changeset/brave-ants-report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/offscreen-document": patch ---- - -feat: support touch events diff --git a/.changeset/brave-symbols-laugh.md b/.changeset/brave-symbols-laugh.md deleted file mode 100644 index 853d812bb3..0000000000 --- a/.changeset/brave-symbols-laugh.md +++ /dev/null @@ -1,3 +0,0 @@ ---- - ---- diff --git a/.changeset/calm-apples-buy.md b/.changeset/calm-apples-buy.md deleted file mode 100644 index 860a0b0562..0000000000 --- a/.changeset/calm-apples-buy.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@lynx-js/react": patch ---- - -Add support for batch rendering in `` with async resolution of sub-tree properties and element trees. - -Use the `experimental-batch-render-strategy` attribute of ``: - -```tsx - -; -``` diff --git a/.changeset/clear-cobras-work.md b/.changeset/clear-cobras-work.md deleted file mode 100644 index bfcf1f119a..0000000000 --- a/.changeset/clear-cobras-work.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@lynx-js/web-mainthread-apis": patch -"@lynx-js/web-core": patch ---- - -fix: some inline style properties cause crash - -add support for the following css properties - -- mask -- mask-repeat -- mask-position -- mask-clip -- mask-origin -- mask-size -- gap -- column-gap -- row-gap -- image-rendering -- hyphens -- offset-path -- offset-distance diff --git a/.changeset/cuddly-beans-enter.md b/.changeset/cuddly-beans-enter.md deleted file mode 100644 index 1fcff226cc..0000000000 --- a/.changeset/cuddly-beans-enter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/web-worker-runtime": patch ---- - -feat: support for using `lynx.queueMicrotask`. diff --git a/.changeset/deep-heads-talk.md b/.changeset/deep-heads-talk.md deleted file mode 100644 index 2344e4dba3..0000000000 --- a/.changeset/deep-heads-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/web-explorer": patch ---- - -feat: use nativeModulesPath instead of nativeModulesMap to lynx-view. diff --git a/.changeset/easy-regions-say.md b/.changeset/easy-regions-say.md deleted file mode 100644 index 0d5e0fca74..0000000000 --- a/.changeset/easy-regions-say.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/web-elements": patch ---- - -fix(web): x-swiper-item threshold updated to 20 diff --git a/.changeset/eight-geckos-tease.md b/.changeset/eight-geckos-tease.md deleted file mode 100644 index 853d812bb3..0000000000 --- a/.changeset/eight-geckos-tease.md +++ /dev/null @@ -1,3 +0,0 @@ ---- - ---- diff --git a/.changeset/eight-swans-mix.md b/.changeset/eight-swans-mix.md deleted file mode 100644 index 47c4e16d31..0000000000 --- a/.changeset/eight-swans-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/web-explorer": patch ---- - -fix: fork @vant/touch-emulator and make it work with shadowroot diff --git a/.changeset/empty-worms-buy.md b/.changeset/empty-worms-buy.md deleted file mode 100644 index e927bc75a5..0000000000 --- a/.changeset/empty-worms-buy.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@lynx-js/web-mainthread-apis": patch -"@lynx-js/web-worker-runtime": patch -"@lynx-js/web-constants": patch -"@lynx-js/web-core": patch ---- - -feat: support touch events for MTS - -now we support - -- main-thread:bindtouchstart -- main-thread:bindtouchend -- main-thread:bindtouchmove -- main-thread:bindtouchcancel diff --git a/.changeset/fast-goats-mix.md b/.changeset/fast-goats-mix.md deleted file mode 100644 index e0ba7ad136..0000000000 --- a/.changeset/fast-goats-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/rspeedy": patch ---- - -Bump Rsbuild v1.3.17 with Rspack v1.3.9. diff --git a/.changeset/fifty-moose-boil.md b/.changeset/fifty-moose-boil.md deleted file mode 100644 index 15ffc9a320..0000000000 --- a/.changeset/fifty-moose-boil.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/react-rsbuild-plugin": patch ---- - -Fix runtime error: "SyntaxError: Identifier 'i' has already been declared". diff --git a/.changeset/fine-ants-rush.md b/.changeset/fine-ants-rush.md deleted file mode 100644 index 53d262f7e4..0000000000 --- a/.changeset/fine-ants-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/web-explorer": patch ---- - -fix: loading errors caused by script import order diff --git a/.changeset/five-frogs-report.md b/.changeset/five-frogs-report.md deleted file mode 100644 index 8028b00c3f..0000000000 --- a/.changeset/five-frogs-report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/web-elements": patch ---- - -fix: In React19, setter and getter functions are treated as properties, making it impossible to retrieve the current value via attributes. diff --git a/.changeset/fluffy-insects-eat.md b/.changeset/fluffy-insects-eat.md deleted file mode 100644 index c95e7c11b2..0000000000 --- a/.changeset/fluffy-insects-eat.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@lynx-js/testing-environment": patch -"@lynx-js/react": patch ---- - -rename @lynx-js/test-environment to @lynx-js/testing-environment diff --git a/.changeset/full-rocks-smoke.md b/.changeset/full-rocks-smoke.md deleted file mode 100644 index db46c27343..0000000000 --- a/.changeset/full-rocks-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/react-rsbuild-plugin": patch ---- - -Enable runtime profiling when `performance.profile` is set to true. diff --git a/.changeset/good-eagles-laugh.md b/.changeset/good-eagles-laugh.md deleted file mode 100644 index afe82c3c2e..0000000000 --- a/.changeset/good-eagles-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/react-webpack-plugin": patch ---- - -Add the `profile` option to control whether `__PROFILE__` is enabled. diff --git a/.changeset/hip-toes-admire.md b/.changeset/hip-toes-admire.md deleted file mode 100644 index d2e6a83b30..0000000000 --- a/.changeset/hip-toes-admire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/react": patch ---- - -Auto import `@lynx-js/react/experimental/lazy/import` when using `import(url)` diff --git a/.changeset/hot-adults-wear.md b/.changeset/hot-adults-wear.md deleted file mode 100644 index 774d83251e..0000000000 --- a/.changeset/hot-adults-wear.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -"@lynx-js/web-platform-rsbuild-plugin": minor ---- - -feat: add new parameter `nativeModulesPath` to `pluginWebPlatform({})`. - -After this commit, you can use `nativeModulesPath` to package custom nativeModules directly into the worker, and no longer need to pass `nativeModulesMap` to lynx-view. - -Here is an example: - -- `native-modules.ts`: - -```ts -// index.native-modules.ts -export default { - CustomModule: function(NativeModules, NativeModulesCall) { - return { - async getColor(data, callback) { - const color = await NativeModulesCall('getColor', data); - callback(color); - }, - }; - }, -}; -``` - -- plugin config: - -```ts -// rsbuild.config.ts -import { pluginWebPlatform } from '@lynx-js/web-platform-rsbuild-plugin'; -import { defineConfig } from '@rsbuild/core'; - -export default defineConfig({ - plugins: [pluginWebPlatform({ - // replace with your actual native-modules file path - nativeModulesPath: path.resolve(__dirname, './index.native-modules.ts'), - })], -}); -``` diff --git a/.changeset/itchy-islands-float.md b/.changeset/itchy-islands-float.md deleted file mode 100644 index 347393e8f7..0000000000 --- a/.changeset/itchy-islands-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/rspeedy": patch ---- - -Support `performance.profile`. diff --git a/.changeset/lemon-bars-walk.md b/.changeset/lemon-bars-walk.md deleted file mode 100644 index 5bd1569d7f..0000000000 --- a/.changeset/lemon-bars-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/react": patch ---- - -Auto import `@lynx-js/react/experimental/lazy/import` when using `` diff --git a/.changeset/loud-mangos-enjoy.md b/.changeset/loud-mangos-enjoy.md deleted file mode 100644 index 3c7d2cc2d8..0000000000 --- a/.changeset/loud-mangos-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/web-core": patch ---- - -feat: add SystemInfo.screenWidth and SystemInfo.screenHeight diff --git a/.changeset/lovely-pots-tap.md b/.changeset/lovely-pots-tap.md deleted file mode 100644 index 853d812bb3..0000000000 --- a/.changeset/lovely-pots-tap.md +++ /dev/null @@ -1,3 +0,0 @@ ---- - ---- diff --git a/.changeset/modern-peaches-marry.md b/.changeset/modern-peaches-marry.md deleted file mode 100644 index 1994b45796..0000000000 --- a/.changeset/modern-peaches-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/web-platform-rsbuild-plugin": minor ---- - -feat: Provides Rsbuild plugin for Web projects in Lynx Web Platform, currently supports polyfill about lynx. diff --git a/.changeset/nice-needles-press.md b/.changeset/nice-needles-press.md deleted file mode 100644 index 1aff40f798..0000000000 --- a/.changeset/nice-needles-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/rspeedy": patch ---- - -Support CLI flag `--mode` to specify the build mode. diff --git a/.changeset/shaky-months-count.md b/.changeset/shaky-months-count.md deleted file mode 100644 index f1a9f28be2..0000000000 --- a/.changeset/shaky-months-count.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/web-explorer": patch ---- - -chore: update homepage diff --git a/.changeset/short-crabs-lead.md b/.changeset/short-crabs-lead.md deleted file mode 100644 index 64187db7ab..0000000000 --- a/.changeset/short-crabs-lead.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/react-rsbuild-plugin": patch ---- - -fix: resolve page crash on development mode when enabling `experimental_isLazyBundle: true` diff --git a/.changeset/silly-rocks-guess.md b/.changeset/silly-rocks-guess.md deleted file mode 100644 index d1845455b0..0000000000 --- a/.changeset/silly-rocks-guess.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@lynx-js/react": minor ---- - -Reverts #239: "batch multiple patches for main thread communication" - -This reverts the change that batched updates sent to the main thread in a single render pass. diff --git a/.changeset/strong-rockets-itch.md b/.changeset/strong-rockets-itch.md deleted file mode 100644 index 7d675e7aba..0000000000 --- a/.changeset/strong-rockets-itch.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@lynx-js/react-rsbuild-plugin": patch -"@lynx-js/react-webpack-plugin": patch ---- - -Support `@lynx-js/react` v0.108.0. diff --git a/.changeset/tangy-dancers-jam.md b/.changeset/tangy-dancers-jam.md deleted file mode 100644 index 7956abc56b..0000000000 --- a/.changeset/tangy-dancers-jam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/react": patch ---- - -Fix error like `Unterminated string constant` when using multi-line JSX StringLiteral. diff --git a/.changeset/thick-pots-clap.md b/.changeset/thick-pots-clap.md deleted file mode 100644 index bed1ed1641..0000000000 --- a/.changeset/thick-pots-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/web-worker-runtime": patch ---- - -feat: provide comments for `@lynx-js/web-platform-rsbuild-plugin`. diff --git a/.changeset/three-baboons-hunt.md b/.changeset/three-baboons-hunt.md deleted file mode 100644 index 1909b6f51b..0000000000 --- a/.changeset/three-baboons-hunt.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -"@lynx-js/rspeedy": patch ---- - -Enable native Rsdoctor plugin by default. - -Set `tools.rsdoctor.experiments.enableNativePlugin` to `false` to use the old JS plugin. - -```js -import { defineConfig } from '@lynx-js/rspeedy'; - -export default defineConfig({ - tools: { - rsdoctor: { - experiments: { - enableNativePlugin: false, - }, - }, - }, -}); -``` - -See [Rsdoctor - 1.0](https://rsdoctor.dev/blog/release/release-note-1_0#-faster-analysis) for more details. diff --git a/.changeset/tired-drinks-give.md b/.changeset/tired-drinks-give.md deleted file mode 100644 index 52f5229d3f..0000000000 --- a/.changeset/tired-drinks-give.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@lynx-js/react": minor ---- - -Fixed closure variable capture issue in effect hooks to prevent stale values and ensured proper execution order between refs, effects, and event handlers. - -**Breaking Changes**: - -- The execution timing of `ref` and `useEffect()` side effects has been moved forward. These effects will now execute before hydration is complete, rather than waiting for the main thread update to complete. -- For components inside ``, `ref` callbacks will now be triggered during background thread rendering, regardless of component visibility. If your code depends on component visibility timing, use `main-thread:ref` instead of regular `ref`. diff --git a/.changeset/twenty-tools-teach.md b/.changeset/twenty-tools-teach.md deleted file mode 100644 index f559ebabed..0000000000 --- a/.changeset/twenty-tools-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/rspeedy": patch ---- - -Bump Rsbuild v1.3.14 with Rspack v1.3.8. diff --git a/.changeset/vast-trains-wave.md b/.changeset/vast-trains-wave.md deleted file mode 100644 index 01b1935f6b..0000000000 --- a/.changeset/vast-trains-wave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/runtime-wrapper-webpack-plugin": patch ---- - -feat: add `experimental_isLazyBundle` option, it will disable lynxChunkEntries for standalone lazy bundle diff --git a/.changeset/wild-sheep-dream.md b/.changeset/wild-sheep-dream.md deleted file mode 100644 index a71a91af9b..0000000000 --- a/.changeset/wild-sheep-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/tailwind-preset": patch ---- - -Support `hidden`, `no-underline` and `line-through` utilities. diff --git a/.changeset/young-seals-train.md b/.changeset/young-seals-train.md deleted file mode 100644 index 2cce760f2f..0000000000 --- a/.changeset/young-seals-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@lynx-js/testing-environment": minor ---- - -Switch to ESM package format by setting `"type": "module"`. diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 9ce3501516..6281f255b6 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,45 @@ # @lynx-js/react +## 0.108.0 + +### Minor Changes + +- Reverts #239: "batch multiple patches for main thread communication" ([#649](https://github.com/lynx-family/lynx-stack/pull/649)) + + This reverts the change that batched updates sent to the main thread in a single render pass. + +### Patch Changes + +- Add support for batch rendering in `` with async resolution of sub-tree properties and element trees. ([#624](https://github.com/lynx-family/lynx-stack/pull/624)) + + Use the `experimental-batch-render-strategy` attribute of ``: + + ```tsx + + ; + ``` + +- rename @lynx-js/test-environment to @lynx-js/testing-environment ([#704](https://github.com/lynx-family/lynx-stack/pull/704)) + +- Auto import `@lynx-js/react/experimental/lazy/import` when using `import(url)` ([#667](https://github.com/lynx-family/lynx-stack/pull/667)) + +- Auto import `@lynx-js/react/experimental/lazy/import` when using `` ([#666](https://github.com/lynx-family/lynx-stack/pull/666)) + +- Fixed a race condition when updating states and GlobalProps simultaneously. ([#707](https://github.com/lynx-family/lynx-stack/pull/707)) + + This fix prevents the "Attempt to render more than one ``" error from occurring during normal application usage. + +- Fix error like `Unterminated string constant` when using multi-line JSX StringLiteral. ([#654](https://github.com/lynx-family/lynx-stack/pull/654)) + ## 0.107.1 ### Patch Changes diff --git a/packages/react/package.json b/packages/react/package.json index 2f76b9ed66..521b27e333 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/react", - "version": "0.107.1", + "version": "0.108.0", "description": "ReactLynx is a framework for developing Lynx applications with familiar React.", "repository": { "type": "git", diff --git a/packages/react/runtime/__test__/delayed-lifecycle-events.test.jsx b/packages/react/runtime/__test__/delayed-lifecycle-events.test.jsx index 2b7dbeed9e..b5694e293d 100644 --- a/packages/react/runtime/__test__/delayed-lifecycle-events.test.jsx +++ b/packages/react/runtime/__test__/delayed-lifecycle-events.test.jsx @@ -31,6 +31,7 @@ describe('delayedLifecycleEvents', () => { "rLynxFirstScreen", { "jsReadyEventIdSwap": {}, + "refPatch": "{}", "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_1"}]}", }, ], @@ -44,6 +45,7 @@ describe('delayedLifecycleEvents', () => { "rLynxFirstScreen", { "jsReadyEventIdSwap": {}, + "refPatch": "{}", "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_1"}]}", }, ], diff --git a/packages/react/runtime/__test__/lifecycle.test.jsx b/packages/react/runtime/__test__/lifecycle.test.jsx index a7684a8150..bfb64ad1e5 100644 --- a/packages/react/runtime/__test__/lifecycle.test.jsx +++ b/packages/react/runtime/__test__/lifecycle.test.jsx @@ -42,21 +42,243 @@ describe('useEffect', () => { } initGlobalSnapshotPatch(); + let mtCallbacks = lynx.getNativeApp().callLepusMethod; globalEnvManager.switchToBackground(); render(, __root); + expect(callback).toHaveBeenCalledTimes(0); + expect(cleanUp).toHaveBeenCalledTimes(0); + + expect(callback).toHaveBeenCalledTimes(0); + expect(mtCallbacks.mock.calls.length).toBe(1); + mtCallbacks.mock.calls[0][2](); + lynx.getNativeApp().callLepusMethod.mockClear(); + expect(callback).toHaveBeenCalledTimes(1); + expect(cleanUp).toHaveBeenCalledTimes(0); await waitSchedule(); + expect(callback).toHaveBeenCalledTimes(1); + expect(cleanUp).toHaveBeenCalledTimes(0); + render(, __root); expect(callback).toHaveBeenCalledTimes(1); expect(cleanUp).toHaveBeenCalledTimes(0); + expect(mtCallbacks.mock.calls.length).toBe(1); + mtCallbacks.mock.calls[0][2](); + lynx.getNativeApp().callLepusMethod.mockClear(); + + await waitSchedule(); + expect(callback).toHaveBeenCalledTimes(2); + expect(cleanUp).toHaveBeenCalledTimes(1); + }); + + it('should call after main thread returns', async function() { + globalEnvManager.switchToBackground(); + + let mtCallbacks = lynx.getNativeApp().callLepusMethod.mock.calls; + + const cleanUp = vi.fn(); + const callback = vi.fn().mockImplementation(() => cleanUp); + + function Comp() { + const [val, setVal] = useState(1); + useLayoutEffect(callback); + return {val}; + } + + initGlobalSnapshotPatch(); + + render(, __root); render(, __root); + render(, __root); + expect(callback).toHaveBeenCalledTimes(0); + expect(cleanUp).toHaveBeenCalledTimes(0); + let mtCallback; + // expect(mtCallbacks.length).toEqual(3); + mtCallback = mtCallbacks.shift(); + expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate); + expect(mtCallback[1]).toMatchInlineSnapshot(` + { + "data": "{"patchList":[{"id":3,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_2",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}", + "patchOptions": { + "reloadVersion": 0, + }, + } + `); + mtCallback[2](); await waitSchedule(); + expect(callback).toHaveBeenCalledTimes(1); + expect(cleanUp).toHaveBeenCalledTimes(0); + expect(mtCallbacks.length).toEqual(2); + mtCallback = mtCallbacks.shift(); + expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate); + expect(mtCallback[1]).toMatchInlineSnapshot(` + { + "data": "{"patchList":[{"id":4}]}", + "patchOptions": { + "reloadVersion": 0, + }, + } + `); + mtCallback[2](); + await waitSchedule(); expect(callback).toHaveBeenCalledTimes(2); expect(cleanUp).toHaveBeenCalledTimes(1); + + expect(mtCallbacks.length).toEqual(1); + mtCallback = mtCallbacks.shift(); + expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate); + expect(mtCallback[1]).toMatchInlineSnapshot(` + { + "data": "{"patchList":[{"id":5}]}", + "patchOptions": { + "reloadVersion": 0, + }, + } + `); + mtCallback[2](); + await waitSchedule(); + expect(callback).toHaveBeenCalledTimes(3); + expect(cleanUp).toHaveBeenCalledTimes(2); + }); + + it('change before hydration', async function() { + let setVal_; + + const cleanUp = vi.fn(); + const callback = vi.fn(() => { + return cleanUp; + }); + + function Comp() { + const [val, setVal] = useState(1); + setVal_ = setVal; + useLayoutEffect(callback); + return {val}; + } + + // main thread render + { + __root.__jsx = ; + renderPage(); + } + + // background render + { + globalEnvManager.switchToBackground(); + render(, __root); + } + + // background state change + { + setVal_(300); + await waitSchedule(); + expect(lynx.getNativeApp().callLepusMethod).not.toBeCalled(); + } + + // background state change + { + setVal_(400); + await waitSchedule(); + expect(lynx.getNativeApp().callLepusMethod).not.toBeCalled(); + } + + // hydrate + { + // LifecycleConstant.firstScreen + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); + expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( + `"{"patchList":[{"snapshotPatch":[3,-3,0,400],"id":9}]}"`, + ); + globalThis.__OnLifecycleEvent.mockClear(); + + await waitSchedule(); + expect(callback).toHaveBeenCalledTimes(0); + expect(cleanUp).toHaveBeenCalledTimes(0); + + // rLynxChange + globalEnvManager.switchToMainThread(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + rLynxChange[2](); + + await waitSchedule(); + expect(callback).toHaveBeenCalledTimes(3); + expect(cleanUp).toHaveBeenCalledTimes(2); + } + }); + + it('cleanup function should delay when unmounts', async function() { + const cleanUp = vi.fn(); + const callback = vi.fn(() => { + return cleanUp; + }); + + function A() { + useLayoutEffect(callback); + } + + function Comp(props) { + return props.show && ; + } + + // main thread render + { + __root.__jsx = ; + renderPage(); + } + + // background render + { + globalEnvManager.switchToBackground(); + render(, __root); + } + + // hydrate + { + // LifecycleConstant.firstScreen + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + globalThis.__OnLifecycleEvent.mockClear(); + + await waitSchedule(); + expect(callback).toHaveBeenCalledTimes(0); + expect(cleanUp).toHaveBeenCalledTimes(0); + + // rLynxChange + globalEnvManager.switchToMainThread(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + rLynxChange[2](); + await waitSchedule(); + } + + // background unmount + { + globalEnvManager.switchToBackground(); + lynx.getNativeApp().callLepusMethod.mockClear(); + render(, __root); + render(, __root); + expect(callback).toHaveBeenCalledTimes(0); + expect(cleanUp).toHaveBeenCalledTimes(0); + } + + { + expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(2); + let rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + rLynxChange[2](); + await waitSchedule(); + expect(callback).toHaveBeenCalledTimes(1); + expect(cleanUp).toHaveBeenCalledTimes(0); + + rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[1]; + rLynxChange[2](); + await waitSchedule(); + expect(callback).toHaveBeenCalledTimes(1); + expect(cleanUp).toHaveBeenCalledTimes(1); + } }); it('throw', async function() { @@ -65,6 +287,8 @@ describe('useEffect', () => { const catchError = options[CATCH_ERROR]; options[CATCH_ERROR] = vi.fn(); + let mtCallbacks = lynx.getNativeApp().callLepusMethod.mock.calls; + const callback = vi.fn().mockImplementation(() => { throw '???'; }); @@ -79,10 +303,23 @@ describe('useEffect', () => { render(, __root); render(, __root); render(, __root); - expect(callback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenCalledTimes(0); + let mtCallback; + expect(mtCallbacks.length).toEqual(3); + mtCallback = mtCallbacks.shift(); + expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate); + expect(mtCallback[1]).toMatchInlineSnapshot(` + { + "data": "{"patchList":[{"id":14,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_4",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}", + "patchOptions": { + "reloadVersion": 0, + }, + } + `); + mtCallback[2](); await waitSchedule(); - expect(callback).toHaveBeenCalledTimes(3); + expect(callback).toHaveBeenCalledTimes(1); expect(options[CATCH_ERROR]).toHaveBeenCalledWith('???', expect.anything()); options[CATCH_ERROR] = catchError; }); @@ -122,7 +359,7 @@ describe('componentDidMount', () => { expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate); expect(mtCallback[1]).toMatchInlineSnapshot(` { - "data": "{"patchList":[{"id":6,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_3",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}", + "data": "{"patchList":[{"id":17,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_5",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}", "patchOptions": { "reloadVersion": 0, }, @@ -169,7 +406,7 @@ describe('componentDidMount', () => { expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate); expect(mtCallback[1]).toMatchInlineSnapshot(` { - "data": "{"patchList":[{"id":9,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_4",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}", + "data": "{"patchList":[{"id":20,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_6",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}", "patchOptions": { "reloadVersion": 0, }, @@ -502,7 +739,7 @@ describe('useState', () => { await waitSchedule(); expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( - `"{"patchList":[{"id":25,"snapshotPatch":[3,-2,1,"abcd",3,-2,2,{"str":"efgh"}]}]}"`, + `"{"patchList":[{"id":36,"snapshotPatch":[3,-2,1,"abcd",3,-2,2,{"str":"efgh"}]}]}"`, ); } }); @@ -560,7 +797,7 @@ describe('useState', () => { await waitSchedule(); expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( - `"{"patchList":[{"id":28,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_15",2,4,2,[false,{"str":"str"}],1,-1,2,null]}]}"`, + `"{"patchList":[{"id":39,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_17",2,4,2,[false,{"str":"str"}],1,-1,2,null]}]}"`, ); } }); diff --git a/packages/react/runtime/__test__/lifecycle/reload.test.jsx b/packages/react/runtime/__test__/lifecycle/reload.test.jsx index f811d42173..eb7203c291 100644 --- a/packages/react/runtime/__test__/lifecycle/reload.test.jsx +++ b/packages/react/runtime/__test__/lifecycle/reload.test.jsx @@ -301,6 +301,7 @@ describe('reload', () => { [ "rLynxFirstScreen", { + "refPatch": "{}", "root": "{"id":-9,"type":"root","children":[{"id":-13,"type":"__Card__:__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-10,"type":"__Card__:__snapshot_a94a8_test_3","children":[{"id":-15,"type":null,"values":["Enjoy"]}]},{"id":-11,"type":"__Card__:__snapshot_a94a8_test_4","children":[{"id":-16,"type":null,"values":["World"]}]},{"id":-12,"type":"wrapper","children":[{"id":-14,"type":"__Card__:__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}", }, ], @@ -710,6 +711,7 @@ describe('reload', () => { [ "rLynxFirstScreen", { + "refPatch": "{}", "root": "{"id":-9,"type":"root","children":[{"id":-15,"type":"__Card__:__snapshot_a94a8_test_5","children":[{"id":-13,"type":"__Card__:__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-10,"type":"__Card__:__snapshot_a94a8_test_3","children":[{"id":-16,"type":null,"values":["Enjoy"]}]},{"id":-11,"type":"__Card__:__snapshot_a94a8_test_4","children":[{"id":-17,"type":null,"values":["World"]}]},{"id":-12,"type":"wrapper","children":[{"id":-14,"type":"__Card__:__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}]}", }, ], @@ -1312,6 +1314,7 @@ describe('firstScreenSyncTiming - jsReady', () => { "-8": -16, "-9": -17, }, + "refPatch": "{}", "root": "{"id":-17,"type":"root","children":[{"id":-21,"type":"__Card__:__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-18,"type":"__Card__:__snapshot_a94a8_test_3","children":[{"id":-23,"type":null,"values":["Hello 2"]}]},{"id":-19,"type":"__Card__:__snapshot_a94a8_test_4","children":[{"id":-24,"type":null,"values":["World"]}]},{"id":-20,"type":"wrapper","children":[{"id":-22,"type":"__Card__:__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}", }, ], @@ -1515,6 +1518,7 @@ describe('firstScreenSyncTiming - jsReady', () => { "-5": -13, "-9": -17, }, + "refPatch": "{}", "root": "{"id":-17,"type":"root","children":[{"id":-21,"type":"__Card__:__snapshot_a94a8_test_7","children":[{"id":-18,"type":"__Card__:__snapshot_a94a8_test_8","values":[{"item-key":0}],"children":[{"id":-22,"type":"__Card__:__snapshot_a94a8_test_6","values":["a"]}]},{"id":-19,"type":"__Card__:__snapshot_a94a8_test_8","values":[{"item-key":1}],"children":[{"id":-23,"type":"__Card__:__snapshot_a94a8_test_6","values":["b"]}]},{"id":-20,"type":"__Card__:__snapshot_a94a8_test_8","values":[{"item-key":2}],"children":[{"id":-24,"type":"__Card__:__snapshot_a94a8_test_6","values":["c"]}]}]}]}", }, ], @@ -1679,6 +1683,7 @@ describe('firstScreenSyncTiming - jsReady', () => { "-2": -10, "-6": -14, }, + "refPatch": "{}", "root": "{"id":-10,"type":"root","children":[{"id":-14,"type":"__Card__:__snapshot_a94a8_test_9","children":[{"id":-11,"type":"__Card__:__snapshot_a94a8_test_10","values":[{"item-key":0}],"children":[{"id":-15,"type":"__Card__:__snapshot_a94a8_test_6","values":["a"]}]},{"id":-12,"type":"__Card__:__snapshot_a94a8_test_10","values":[{"item-key":1}],"children":[{"id":-16,"type":"__Card__:__snapshot_a94a8_test_6","values":["b"]}]},{"id":-13,"type":"__Card__:__snapshot_a94a8_test_10","values":[{"item-key":2}],"children":[{"id":-17,"type":"__Card__:__snapshot_a94a8_test_6","values":["c"]}]}]}]}", }, ], @@ -1872,6 +1877,7 @@ describe('firstScreenSyncTiming - jsReady', () => { "rLynxFirstScreen", { "jsReadyEventIdSwap": {}, + "refPatch": "{}", "root": "{"id":-17,"type":"root","children":[{"id":-21,"type":"__Card__:__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-18,"type":"__Card__:__snapshot_a94a8_test_3","children":[{"id":-23,"type":null,"values":["Hello 2"]}]},{"id":-19,"type":"__Card__:__snapshot_a94a8_test_4","children":[{"id":-24,"type":null,"values":["World"]}]},{"id":-20,"type":"wrapper","children":[{"id":-22,"type":"__Card__:__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}", }, ], diff --git a/packages/react/runtime/__test__/page.test.jsx b/packages/react/runtime/__test__/page.test.jsx index d08f41c0f3..1a94120aec 100644 --- a/packages/react/runtime/__test__/page.test.jsx +++ b/packages/react/runtime/__test__/page.test.jsx @@ -1,10 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - import { __page } from '../src/snapshot'; -import { globalEnvManager } from './utils/envManager'; import { elementTree } from './utils/nativeMethod'; -import { useRef, useState } from '../src/index'; +import { globalEnvManager } from './utils/envManager'; import { __root } from '../src/root'; +import { useState, useRef } from '../src/index'; beforeEach(() => { globalEnvManager.resetEnv(); @@ -78,7 +77,7 @@ describe('support element attributes', () => { "bindEvent:tap": "-1:0:bindtap", } } - react-ref--1-0={1} + has-react-ref={true} > @@ -316,7 +315,6 @@ describe('support element attributes', () => { expect(__root.__element_root).toMatchInlineSnapshot(` { "-5": -8, "-6": -9, }, + "refPatch": "{}", "root": "{"id":-7,"type":"root","children":[{"id":-8,"type":"__Card__:__snapshot_a94a8_test_12","children":[{"id":-9,"type":"__Card__:__snapshot_a94a8_test_11","values":["-9:0:"]}]}]}", }, ], @@ -1271,6 +1272,7 @@ describe('call `root.render()` async', () => { "rLynxFirstScreen", { "jsReadyEventIdSwap": {}, + "refPatch": "{}", "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_14","children":[{"id":-3,"type":"__Card__:__snapshot_a94a8_test_13","values":["-3:0:"]}]}]}", }, ], diff --git a/packages/react/runtime/__test__/snapshot/ref.test.jsx b/packages/react/runtime/__test__/snapshot/ref.test.jsx index a8b1f1fb09..66e1251f8c 100644 --- a/packages/react/runtime/__test__/snapshot/ref.test.jsx +++ b/packages/react/runtime/__test__/snapshot/ref.test.jsx @@ -6,7 +6,8 @@ import { render } from 'preact'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; -import { Component, createRef, useState } from '../../src/index'; +import { Component, createRef, root, useState } from '../../src/index'; +import { delayedLifecycleEvents } from '../../src/lifecycle/event/delayLifecycleEvents'; import { clearCommitTaskId, replaceCommitHook } from '../../src/lifecycle/patch/commit'; import { injectUpdateMainThread } from '../../src/lifecycle/patch/updateMainThread'; import { __pendingListUpdates } from '../../src/list'; @@ -101,10 +102,10 @@ describe('element ref', () => { > @@ -115,7 +116,8 @@ describe('element ref', () => { "rLynxFirstScreen", { "jsReadyEventIdSwap": {}, - "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_3","values":["react-ref--2-0","react-ref--2-1"]}]}", + "refPatch": "{"-2:0:":3,"-2:1:":4}", + "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_3","values":["-2:0:","-2:1:"]}]}", }, ], ] @@ -126,33 +128,50 @@ describe('element ref', () => { { globalEnvManager.switchToBackground(); render(, __root); + expect(ref1).not.toBeCalled(); + expect(ref2.current).toBe(null); + } + + // hydrate + { + // LifecycleConstant.firstScreen + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); + expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( + `"{"patchList":[{"snapshotPatch":[],"id":2}]}"`, + ); + lynx.getNativeApp().callLepusMethod.mock.calls[0][2](); + await waitSchedule(); expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + { + "selectUniqueID": [Function], + "uid": 3, }, ], ] `); - expect(ref2.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - 2, - 1, - ], - "task": undefined, + expect(ref2).toMatchInlineSnapshot(` + { + "current": { + "selectUniqueID": [Function], + "uid": 4, + }, } `); + + // rLynxChange + globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + expect(globalThis.__OnLifecycleEvent).not.toBeCalled(); } }); - it('should trigger ref when insert node', async function() { + it('insert', async function() { const ref1 = vi.fn(); const ref2 = createRef(); @@ -200,49 +219,69 @@ describe('element ref', () => { render(, __root); expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( - `"{"patchList":[{"id":3,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_4",2,4,2,[1,1],1,-1,2,null]}]}"`, + `"{"patchList":[{"id":3,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_4",2,4,2,[3,4],1,-1,2,null]}]}"`, ); } // rLynxChange { globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; globalThis[rLynxChange[0]](rLynxChange[1]); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); rLynxChange[2](); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "rLynxRef", + { + "commitTaskId": 3, + "refPatch": "{"2:0:":7,"2:1:":8}", + }, + ], + ], + ] + `); } // ref { globalEnvManager.switchToBackground(); + await waitSchedule(); expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + { + "selectUniqueID": [Function], + "uid": 7, }, ], ] `); expect(ref2).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - 2, - 1, - ], - "task": undefined, + "current": { + "selectUniqueID": [Function], + "uid": 8, }, } `); } + + { + globalEnvManager.switchToBackground(); + lynx.getNativeApp().callLepusMethod.mockClear(); + render(, __root); + expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); + expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( + `"{"patchList":[{"id":4,"snapshotPatch":[3,2,0,3,3,2,1,4]}]}"`, + ); + } }); - it('should trigger ref when remove node', async function() { + it('remove', async function() { const ref1 = vi.fn(); const ref2 = createRef(); @@ -295,9 +334,35 @@ describe('element ref', () => { ); } + // rLynxChange + { + globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "rLynxRef", + { + "commitTaskId": 3, + "refPatch": "{"-2:0:":null,"-2:1:":null}", + }, + ], + ], + ] + `); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + globalThis.__OnLifecycleEvent.mockClear(); + rLynxChange[2](); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`[]`); + } + // ref patch { globalEnvManager.switchToBackground(); + await waitSchedule(); expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ @@ -309,7 +374,7 @@ describe('element ref', () => { } }); - it('should trigger ref when remove node with cleanup function', async function() { + it('remove with cleanup function', async function() { const cleanup = vi.fn(); const ref1 = vi.fn(() => { return cleanup; @@ -363,9 +428,33 @@ describe('element ref', () => { ); } + // rLynxChange + { + globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + rLynxChange[2](); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "rLynxRef", + { + "commitTaskId": 3, + "refPatch": "{"-2:0:":null}", + }, + ], + ], + ] + `); + } + // ref patch { globalEnvManager.switchToBackground(); + await waitSchedule(); expect(ref1).not.toBeCalled(); expect(cleanup.mock.calls).toMatchInlineSnapshot(` [ @@ -375,7 +464,7 @@ describe('element ref', () => { } }); - it('should trigger ref when ref and unref deeply', async () => { + it('callback should ref and unref deeply', async () => { const ref1 = [vi.fn(), vi.fn(), vi.fn()]; const ref2 = vi.fn(); let _setShow; @@ -420,15 +509,16 @@ describe('element ref', () => { globalThis.__OnLifecycleEvent.mockClear(); const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; globalThis[rLynxChange[0]](rLynxChange[1]); + expect(globalThis.__OnLifecycleEvent).not.toBeCalled(); } ref1.forEach(ref => { expect(ref).toHaveBeenCalledWith(expect.objectContaining({ - refAttr: expect.any(Array), + uid: expect.any(Number), })); }); expect(ref2).toHaveBeenCalledWith(expect.objectContaining({ - refAttr: expect.any(Array), + uid: expect.any(Number), })); ref1.forEach(ref => ref.mockClear()); ref2.mockClear(); @@ -441,15 +531,39 @@ describe('element ref', () => { await waitSchedule(); } - // ref check + // rLynxChange + { + globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "rLynxRef", + { + "commitTaskId": 3, + "refPatch": "{"-2:0:":null,"-3:0:":null,"-4:0:":null,"-5:0:":null}", + }, + ], + ], + ] + `); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + rLynxChange[2](); + } + + // ref patch { globalEnvManager.switchToBackground(); + await waitSchedule(); ref1.forEach(ref => expect(ref).toHaveBeenCalledWith(null)); expect(ref2).toHaveBeenCalledWith(null); } }); - it('should trigger ref when ref is null in the first screen', async function() { + it('hydrate', async function() { const ref1 = createRef(); const ref2 = createRef(); const ref3 = vi.fn(); @@ -477,14 +591,10 @@ describe('element ref', () => { > - - + + `); @@ -494,7 +604,8 @@ describe('element ref', () => { "rLynxFirstScreen", { "jsReadyEventIdSwap": {}, - "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_9","values":["react-ref--2-0","react-ref--2-1","react-ref--2-2"]}]}", + "refPatch": "{"-2:0:":23}", + "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_9","values":["-2:0:",null,null]}]}", }, ], ] @@ -506,30 +617,6 @@ describe('element ref', () => { globalEnvManager.switchToBackground(); render(, __root); lynx.getNativeApp().callLepusMethod.mockClear(); - - expect(ref1.current).toBeNull(); - expect(ref2.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - 2, - 1, - ], - "task": undefined, - } - `); - expect(ref3.mock.calls).toMatchInlineSnapshot(` - [ - [ - RefProxy { - "refAttr": [ - 2, - 2, - ], - "task": undefined, - }, - ], - ] - `); } // hydrate @@ -538,36 +625,135 @@ describe('element ref', () => { lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( - `"{"patchList":[{"snapshotPatch":[3,-2,0,null],"id":2}]}"`, + `"{"patchList":[{"snapshotPatch":[3,-2,0,null,3,-2,1,13,3,-2,2,14],"id":2}]}"`, ); + expect(ref1.current).toBeNull(); + expect(ref2.current).toBeNull(); + expect(ref3).not.toBeCalled(); + // rLynxChange globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; globalThis[rLynxChange[0]](rLynxChange[1]); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); rLynxChange[2](); - // Keep "-2:0" exists even if it is set to null. This is for the first screen. expect(__root.__element_root).toMatchInlineSnapshot(` `); + + // ref patch + { + globalEnvManager.switchToBackground(); + await waitSchedule(); + expect(ref1.current).toBeNull(); + expect(ref2).toMatchInlineSnapshot(` + { + "current": { + "selectUniqueID": [Function], + "uid": 24, + }, + } + `); + expect(ref3.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "selectUniqueID": [Function], + "uid": 25, + }, + ], + ] + `); + } + } + }); + + it('change before hydration', async function() { + const ref1 = createRef(); + const ref2 = createRef(); + + class Comp extends Component { + x = 'x'; + render() { + return ( + + + + + ); + } + } + + // main thread render + { + __root.__jsx = ; + renderPage(); + } + + // background render + { + globalEnvManager.switchToBackground(); + render(, __root); + lynx.getNativeApp().callLepusMethod.mockClear(); + } + + // background state change + { + render(, __root); + expect(lynx.getNativeApp().callLepusMethod).not.toBeCalled(); + } + + // hydrate + { + // LifecycleConstant.firstScreen + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); + expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( + `"{"patchList":[{"snapshotPatch":[3,-2,0,null,3,-2,1,16],"id":3}]}"`, + ); + globalThis.__OnLifecycleEvent.mockClear(); + + // rLynxChange + globalEnvManager.switchToMainThread(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + rLynxChange[2](); + + // ref patch + { + globalEnvManager.switchToBackground(); + await waitSchedule(); + expect(ref1.current).toBeNull(); + expect(ref2).toMatchInlineSnapshot(` + { + "current": { + "selectUniqueID": [Function], + "uid": 29, + }, + } + `); + } } }); - it('should throw error when ref type is wrong', async function() { + it('wrong ref type', async function() { let ref1 = 1; class Comp extends Component { @@ -605,7 +791,7 @@ describe('element ref', () => { } }); - it('should trigger ref when ref object is updated', async function() { + it('update', async function() { const cleanup = vi.fn(); let ref1 = vi.fn(() => { return cleanup; @@ -614,6 +800,7 @@ describe('element ref', () => { let ref3 = createRef(); class Comp extends Component { + x = 'x'; render() { return ( @@ -663,34 +850,52 @@ describe('element ref', () => { render(, __root); expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( - `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,2,null]}]}"`, + `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,0,20,3,-2,1,21,3,-2,2,null]}]}"`, ); } + // rLynxChange + { + globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + rLynxChange[2](); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "rLynxRef", + { + "commitTaskId": 3, + "refPatch": "{"-2:0:":32,"-2:1:":33}", + }, + ], + ], + ] + `); + } + // ref { globalEnvManager.switchToBackground(); + await waitSchedule(); expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - -2, - 0, - ], - "task": undefined, + { + "selectUniqueID": [Function], + "uid": 32, }, ], ] `); expect(ref2).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - -2, - 1, - ], - "task": undefined, + "current": { + "selectUniqueID": [Function], + "uid": 33, }, } `); @@ -704,14 +909,24 @@ describe('element ref', () => { expect(oldRef3.current).toBeNull(); } }); +}); - it('should work when using ref along with other attributes', async function() { - const ref = createRef(); - const attr1 = 1; +describe('element ref in spread', () => { + it('insert', async function() { + const ref1 = vi.fn(); + const ref2 = createRef(); + let spread1 = {}; + const spread2 = { ref: ref2 }; class Comp extends Component { + x = 'x'; render() { - return ; + return ( + + + + + ); } } @@ -719,107 +934,59 @@ describe('element ref', () => { { __root.__jsx = ; renderPage(); + expect(__root.__element_root).toMatchInlineSnapshot(` + + + + + + + `); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "rLynxFirstScreen", + { + "jsReadyEventIdSwap": {}, + "refPatch": "{"-2:1:ref":38}", + "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_13","values":[{},{"ref":"-2:1:ref"}]}]}", + }, + ], + ], + ] + `); } // background render { globalEnvManager.switchToBackground(); render(, __root); - expect(ref.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, - } - `); - } - }); - - // NOT working for now - it.skip('should work when using error boundary with ref', async function() { - const ref = vi.fn(() => { - throw new Error('error in ref'); - }); - const errorHandler = vi.fn(); - - class Comp extends Component { - state = { hasError: false }; - - componentDidCatch(error, info) { - errorHandler(error, info); - this.setState({ hasError: true }); - } - - render() { - if (this.state.hasError) { - return Error occurred; - } - return ; - } - } - - // main thread render - { - __root.__jsx = ; - renderPage(); + lynx.getNativeApp().callLepusMethod.mockClear(); } - // background render + // hydrate { - globalEnvManager.switchToBackground(); - render(, __root); - expect(ref.current).toMatchInlineSnapshot(`undefined`); - - expect(errorHandler).toHaveBeenCalledTimes(1); - } - }); -}); - -describe('element ref in spread', () => { - it('should trigger ref when insert ref into spread', async function() { - const ref1 = vi.fn(); - const ref2 = createRef(); - let spread1 = {}; - const spread2 = { ref: ref2 }; - - class Comp extends Component { - x = 'x'; - render() { - return ( - - - - - ); - } - } + // LifecycleConstant.firstScreen + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + globalThis.__OnLifecycleEvent.mockClear(); - // main thread render - { - __root.__jsx = ; - renderPage(); - expect(__root.__element_root).toMatchInlineSnapshot(` - - - - - - - `); + // rLynxChange + globalEnvManager.switchToMainThread(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` [ [ [ - "rLynxFirstScreen", + "rLynxRef", { - "jsReadyEventIdSwap": {}, - "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_15","values":[{},{"ref":"react-ref--2-1"}]}]}", + "commitTaskId": 2, + "refPatch": "{"-2:1:ref":38}", }, ], ], @@ -827,36 +994,15 @@ describe('element ref in spread', () => { `); } - // background render - { - globalEnvManager.switchToBackground(); - render(, __root); - lynx.getNativeApp().callLepusMethod.mockClear(); - } - - // hydrate - { - // LifecycleConstant.firstScreen - lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); - globalThis.__OnLifecycleEvent.mockClear(); - - // rLynxChange - globalEnvManager.switchToMainThread(); - const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; - globalThis[rLynxChange[0]](rLynxChange[1]); - } - // ref { + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); expect(ref1.mock.calls).toMatchInlineSnapshot(`[]`); expect(ref2).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - 2, - 1, - ], - "task": undefined, + "current": { + "selectUniqueID": [Function], + "uid": 38, }, } `); @@ -870,44 +1016,43 @@ describe('element ref in spread', () => { render(, __root); expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( - `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,0,{"ref":1}]}]}"`, + `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,0,{"ref":23}]}]}"`, ); } // rLynxChange { globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; globalThis[rLynxChange[0]](rLynxChange[1]); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); rLynxChange[2](); - expect(__root.__element_root).toMatchInlineSnapshot(` - - - - - - + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "rLynxRef", + { + "commitTaskId": 3, + "refPatch": "{"-2:0:ref":37}", + }, + ], + ], + ] `); } // ref { globalEnvManager.switchToBackground(); + await waitSchedule(); expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - -2, - 0, - ], - "task": undefined, + { + "selectUniqueID": [Function], + "uid": 37, }, ], ] @@ -915,7 +1060,7 @@ describe('element ref in spread', () => { } }); - it('should trigger ref when remove ref from spread', async function() { + it('remove', async function() { const ref1 = vi.fn(); const ref2 = createRef(); let spread1 = { ref: ref1 }; @@ -954,24 +1099,18 @@ describe('element ref in spread', () => { expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 3, - 0, - ], - "task": undefined, + { + "selectUniqueID": [Function], + "uid": 43, }, ], ] `); expect(ref2).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + "current": { + "selectUniqueID": [Function], + "uid": 42, }, } `); @@ -984,6 +1123,8 @@ describe('element ref in spread', () => { // ref { + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + globalThis.__OnLifecycleEvent.mockClear(); ref1.mockClear(); } @@ -1002,13 +1143,16 @@ describe('element ref in spread', () => { // rLynxChange { globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; globalThis[rLynxChange[0]](rLynxChange[1]); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); rLynxChange[2](); } // ref { + await waitSchedule(); expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ @@ -1020,7 +1164,7 @@ describe('element ref in spread', () => { } }); - it('should trigger ref when update ref in spread', async function() { + it('update', async function() { let ref1 = vi.fn(); let ref2 = createRef(); let ref3 = createRef(); @@ -1062,35 +1206,26 @@ describe('element ref in spread', () => { expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + { + "selectUniqueID": [Function], + "uid": 46, }, ], ] `); expect(ref2).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - 2, - 1, - ], - "task": undefined, + "current": { + "selectUniqueID": [Function], + "uid": 47, }, } `); expect(ref3).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - 2, - 2, - ], - "task": undefined, + "current": { + "selectUniqueID": [Function], + "uid": 48, }, } `); @@ -1107,6 +1242,8 @@ describe('element ref in spread', () => { // ref { globalEnvManager.switchToBackground(); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + globalThis.__OnLifecycleEvent.mockClear(); ref1.mockClear(); } @@ -1125,43 +1262,52 @@ describe('element ref in spread', () => { render(, __root); expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( - `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,2,{}]}]}"`, + `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,0,{"ref":29},3,-2,1,{"ref":30},3,-2,2,{}]}]}"`, ); } // rLynxChange { globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; globalThis[rLynxChange[0]](rLynxChange[1]); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); rLynxChange[2](); - lynx.getNativeApp().callLepusMethod.mockClear(); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "rLynxRef", + { + "commitTaskId": 3, + "refPatch": "{"-2:0:ref":46,"-2:1:ref":47}", + }, + ], + ], + ] + `); } // ref { globalEnvManager.switchToBackground(); + await waitSchedule(); expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - -2, - 0, - ], - "task": undefined, + { + "selectUniqueID": [Function], + "uid": 46, }, ], ] `); expect(ref2).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - -2, - 1, - ], - "task": undefined, + "current": { + "selectUniqueID": [Function], + "uid": 47, }, } `); @@ -1174,50 +1320,14 @@ describe('element ref in spread', () => { `); expect(oldRef2.current).toBeNull(); expect(oldRef3.current).toBeNull(); - ref1.mockClear(); - } - - // update ref - { - ref3 = createRef(); - spread3 = { ref: ref3 }; - render(, __root); - expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); - expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( - `"{"patchList":[{"id":4,"snapshotPatch":[3,-2,2,{"ref":1}]}]}"`, - ); - expect(ref1.mock.calls).toMatchInlineSnapshot(` - [ - [ - null, - ], - [ - RefProxy { - "refAttr": [ - -2, - 0, - ], - "task": undefined, - }, - ], - ] - `); - expect(ref3.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - -2, - 2, - ], - "task": undefined, - } - `); } }); }); describe('element ref in list', () => { - it('should trigger ref in list', async function() { + it('hydrate', async function() { const refs = [createRef(), createRef(), createRef()]; + const signs = [0, 0, 0]; class ListItem extends Component { render() { @@ -1257,17 +1367,17 @@ describe('element ref in list', () => { { "item-key": 0, "position": 0, - "type": "__Card__:__snapshot_a94a8_test_21", + "type": "__Card__:__snapshot_a94a8_test_19", }, { "item-key": 1, "position": 1, - "type": "__Card__:__snapshot_a94a8_test_21", + "type": "__Card__:__snapshot_a94a8_test_19", }, { "item-key": 2, "position": 2, - "type": "__Card__:__snapshot_a94a8_test_21", + "type": "__Card__:__snapshot_a94a8_test_19", }, ], "removeAction": [], @@ -1285,59 +1395,140 @@ describe('element ref in list', () => { globalEnvManager.switchToBackground(); render(, __root); lynx.getNativeApp().callLepusMethod.mockClear(); + } + + // hydrate + { + // LifecycleConstant.firstScreen + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + globalThis.__OnLifecycleEvent.mockClear(); + // rLynxChange + globalEnvManager.switchToMainThread(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`[]`); + } + + // list render item 1 & 2 + { + signs[0] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 0); + signs[1] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 1); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "rLynxRef", + { + "commitTaskId": undefined, + "refPatch": "{"-4:0:":52}", + }, + ], + ], + [ + [ + "rLynxRef", + { + "commitTaskId": undefined, + "refPatch": "{"-6:0:":54}", + }, + ], + ], + ] + `); + globalEnvManager.switchToBackground(); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[1]); + globalThis.__OnLifecycleEvent.mockClear(); expect(refs[0]).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - 4, - 0, - ], - "task": undefined, + "current": { + "selectUniqueID": [Function], + "uid": 52, }, } `); expect(refs[1]).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - 6, - 0, + "current": { + "selectUniqueID": [Function], + "uid": 54, + }, + } + `); + expect(refs[2].current).toBeNull(); + } + + // list enqueue item 1 & render item 3 + { + globalEnvManager.switchToMainThread(); + elementTree.triggerEnqueueComponent(__root.childNodes[0].__elements[0], signs[0]); + elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 2); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "rLynxRef", + { + "commitTaskId": undefined, + "refPatch": "{"-4:0:":null,"-8:0:":52}", + }, ], - "task": undefined, + ], + ] + `); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + globalThis.__OnLifecycleEvent.mockClear(); + expect(refs[0].current).toBeNull(); + expect(refs[1]).toMatchInlineSnapshot(` + { + "current": { + "selectUniqueID": [Function], + "uid": 54, }, } `); expect(refs[2]).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - 8, - 0, - ], - "task": undefined, + "current": { + "selectUniqueID": [Function], + "uid": 52, }, } `); } + + // list enqueue item 2 & render item 2 + { + globalEnvManager.switchToMainThread(); + elementTree.triggerEnqueueComponent(__root.childNodes[0].__elements[0], signs[1]); + elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 1); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`[]`); + } }); -}); -describe('ui operations', () => { - it('should delay until hydration finished', async function() { - const ref1 = vi.fn((ref) => { - ref.invoke({ - method: 'boundingClientRect', - }).exec(); - }); + it('continuously reuse', async function() { + const refs = [createRef(), createRef(), createRef()]; + const signs = [0, 0, 0]; + + class ListItem extends Component { + render() { + return ; + } + } class Comp extends Component { - x = 'x'; render() { return ( - - - + + {[0, 1, 2].map((index) => { + return ( + + + + ); + })} + ); } } @@ -1352,105 +1543,141 @@ describe('ui operations', () => { { globalEnvManager.switchToBackground(); render(, __root); - expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`[]`); + lynx.getNativeApp().callLepusMethod.mockClear(); } // hydrate { + // LifecycleConstant.firstScreen lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); globalThis.__OnLifecycleEvent.mockClear(); - expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(` + + // rLynxChange + globalEnvManager.switchToMainThread(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`[]`); + } + + // list render item 1 + { + signs[0] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 0); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` [ [ - "[react-ref--2-0]", - "invoke", [ + "rLynxRef", { - "method": "boundingClientRect", + "commitTaskId": undefined, + "refPatch": "{"-4:0:":58}", }, ], ], ] `); - lynx.createSelectorQuery().constructor.execLog.mockClear(); - } - }); - - it('should support more usages of ref 1', async function() { - const ref1 = vi.fn((ref) => { - ref.setNativeProps({ - 'background-color': 'blue', - }).exec(); - ref.path(vi.fn()).exec(); - }); - - class Comp extends Component { - x = 'x'; - render() { - return ( - - - - ); - } - } - - // main thread render - { - __root.__jsx = ; - renderPage(); - } - - // background render - { globalEnvManager.switchToBackground(); - render(, __root); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + globalThis.__OnLifecycleEvent.mockClear(); + expect(refs[0]).toMatchInlineSnapshot(` + { + "current": { + "selectUniqueID": [Function], + "uid": 58, + }, + } + `); + expect(refs[1].current).toBeNull(); + expect(refs[2].current).toBeNull(); } - // hydrate + // list enqueue item 1 & render item 2 { - lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); - globalThis.__OnLifecycleEvent.mockClear(); - expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(` + globalEnvManager.switchToMainThread(); + elementTree.triggerEnqueueComponent(__root.childNodes[0].__elements[0], signs[0]); + signs[1] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 1); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` [ [ - "[react-ref--2-0]", - "setNativeProps", [ + "rLynxRef", { - "background-color": "blue", + "commitTaskId": undefined, + "refPatch": "{"-4:0:":null,"-6:0:":58}", }, ], ], + ] + `); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + globalThis.__OnLifecycleEvent.mockClear(); + expect(refs[0].current).toBeNull(); + expect(refs[1]).toMatchInlineSnapshot(` + { + "current": { + "selectUniqueID": [Function], + "uid": 58, + }, + } + `); + expect(refs[2].current).toBeNull(); + } + + // list enqueue item 2 & render item 3 + { + globalEnvManager.switchToMainThread(); + elementTree.triggerEnqueueComponent(__root.childNodes[0].__elements[0], signs[1]); + signs[2] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 2); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` + [ [ - "[react-ref--2-0]", - "path", [ - [MockFunction spy], + "rLynxRef", + { + "commitTaskId": undefined, + "refPatch": "{"-6:0:":null,"-8:0:":58}", + }, ], ], ] `); - lynx.createSelectorQuery().constructor.execLog.mockClear(); + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + globalThis.__OnLifecycleEvent.mockClear(); + expect(refs[0].current).toBeNull(); + expect(refs[1].current).toBeNull(); + expect(refs[2]).toMatchInlineSnapshot(` + { + "current": { + "selectUniqueID": [Function], + "uid": 58, + }, + } + `); } }); - it('should support more usages of ref 2', async function() { - const ref1 = vi.fn((ref) => { - const fields = ref.fields({ - id: true, - }); - fields.exec(); - fields.exec(); - }); + it('when __FIRST_SCREEN_SYNC_TIMING__ is jsReady', async function() { + globalThis.__FIRST_SCREEN_SYNC_TIMING__ = 'jsReady'; + const refs = [createRef(), createRef(), createRef()]; + const signs = [0, 0, 0]; + + class ListItem extends Component { + render() { + return ; + } + } class Comp extends Component { - x = 'x'; render() { return ( - - - + + {[0, 1, 2].map((index) => { + return ( + + + + ); + })} + ); } } @@ -1461,159 +1688,71 @@ describe('ui operations', () => { renderPage(); } - // background render + // list render item 1 & 2 { - globalEnvManager.switchToBackground(); - render(, __root); - } + signs[0] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 0); + expect(globalThis.__OnLifecycleEvent).toHaveBeenCalledTimes(1); - // hydrate - { + globalEnvManager.switchToBackground(); lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); globalThis.__OnLifecycleEvent.mockClear(); - expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(` + expect(delayedLifecycleEvents).toMatchInlineSnapshot(` [ [ - "[react-ref--2-0]", - "fields", - [ - { - "id": true, - }, - ], - ], - [ - "[react-ref--2-0]", - "fields", - [ - { - "id": true, - }, - ], + "rLynxRef", + { + "commitTaskId": undefined, + "refPatch": "{"-4:0:":62}", + }, ], ] `); - lynx.createSelectorQuery().constructor.execLog.mockClear(); - } - }); - - it('should not delay after hydration', async function() { - const ref1 = createRef(); - - function Comp() { - return ( - - - - ); - } - - // main thread render - { - __root.__jsx = ; - renderPage(); } // background render { globalEnvManager.switchToBackground(); - render(, __root); - } - - // hydrate - { - lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); - globalThis.__OnLifecycleEvent.mockClear(); - expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`[]`); - - lynx.createSelectorQuery().constructor.execLog.mockClear(); + root.render(, __root); + expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); + expect(lynx.getNativeApp().callLepusMethod.mock.calls[0]).toMatchInlineSnapshot(` + [ + "rLynxJSReady", + {}, + ] + `); + globalEnvManager.switchToMainThread(); + const rLynxJSReady = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxJSReady[0]](rLynxJSReady[1]); lynx.getNativeApp().callLepusMethod.mockClear(); - } - - // call ref - { - ref1.current.invoke({ - method: 'boundingClientRect', - }).exec(); - expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(` + expect(globalThis.__OnLifecycleEvent).toHaveBeenCalledTimes(1); + expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(` [ [ - "[react-ref--2-0]", - "invoke", [ + "rLynxFirstScreen", { - "method": "boundingClientRect", + "jsReadyEventIdSwap": {}, + "refPatch": "{}", + "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_24","children":[{"id":-3,"type":"__Card__:__snapshot_a94a8_test_25","values":[{"item-key":0}],"children":[{"id":-4,"type":"__Card__:__snapshot_a94a8_test_23","values":["-4:0:"]}]},{"id":-5,"type":"__Card__:__snapshot_a94a8_test_25","values":[{"item-key":1}],"children":[{"id":-6,"type":"__Card__:__snapshot_a94a8_test_23","values":["-6:0:"]}]},{"id":-7,"type":"__Card__:__snapshot_a94a8_test_25","values":[{"item-key":2}],"children":[{"id":-8,"type":"__Card__:__snapshot_a94a8_test_23","values":["-8:0:"]}]}]}]}", }, ], ], ] `); } - }); - - it('should not delay after hydration', async function() { - const ref1 = vi.fn((ref) => { - ref.invoke({ - method: 'boundingClientRect', - }).exec(); - }); - let show = false; - function Child() { - return ; - } - - function Comp() { - return ( - - {show ? : null} - - ); - } - - // main thread render - { - __root.__jsx = ; - renderPage(); - } - - // background render { globalEnvManager.switchToBackground(); - render(, __root); - } - - // hydrate - { lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); globalThis.__OnLifecycleEvent.mockClear(); - expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`[]`); - - lynx.createSelectorQuery().constructor.execLog.mockClear(); - lynx.getNativeApp().callLepusMethod.mockClear(); - } - - // set show - { - show = true; - render(, __root); - expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot( - `"{"patchList":[{"id":3,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_26",3,4,3,[1],1,-2,3,null]}]}"`, - ); - expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(` - [ - [ - "[react-ref-3-0]", - "invoke", - [ - { - "method": "boundingClientRect", - }, - ], - ], - ] + expect(refs[0].current).toMatchInlineSnapshot(` + { + "selectUniqueID": [Function], + "uid": 62, + } `); } + globalThis.__FIRST_SCREEN_SYNC_TIMING__ = 'immediately'; }); }); diff --git a/packages/react/runtime/__test__/ssr.test.jsx b/packages/react/runtime/__test__/ssr.test.jsx index 3a3920d0d0..589ba928c7 100644 --- a/packages/react/runtime/__test__/ssr.test.jsx +++ b/packages/react/runtime/__test__/ssr.test.jsx @@ -1,13 +1,12 @@ /** @jsxImportSource ../lepus */ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { globalEnvManager } from './utils/envManager'; import { elementTree, options } from './utils/nativeMethod'; -import { __page as __internalPage } from '../src/internal'; -import { jsReadyEventIdSwap } from '../src/lifecycle/event/jsReady'; +import { globalEnvManager } from './utils/envManager'; import { __root } from '../src/root'; +import { __page as __internalPage } from '../src/internal'; import { clearPage } from '../src/snapshot'; +import { jsReadyEventIdSwap } from '../src/lifecycle/event/jsReady'; const ssrIDMap = new Map(); @@ -212,7 +211,7 @@ describe('ssr', () => { "color": "red", }, "-2:2:", - "react-ref--2-3", + "-2:3:", { "_lepusWorkletHash": "1", "_workletType": "main-thread", @@ -274,7 +273,7 @@ describe('ssr', () => { "main-thread:ref": { "_lepusWorkletHash": "2", }, - "ref": "react-ref--2-0", + "ref": "-2:0:ref", "style": { "color": "red", }, diff --git a/packages/react/runtime/__test__/utils/envManager.ts b/packages/react/runtime/__test__/utils/envManager.ts index bb73467090..21e6d6b261 100644 --- a/packages/react/runtime/__test__/utils/envManager.ts +++ b/packages/react/runtime/__test__/utils/envManager.ts @@ -3,15 +3,13 @@ // 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 { BackgroundSnapshotInstance } from '../../src/backgroundSnapshot.js'; import { setupBackgroundDocument, setupDocument } from '../../src/document.js'; +import { __root, setRoot } from '../../src/root.js'; +import { BackgroundSnapshotInstance } from '../../src/backgroundSnapshot.js'; +import { backgroundSnapshotInstanceManager, SnapshotInstance, snapshotInstanceManager } from '../../src/snapshot.js'; import { deinitGlobalSnapshotPatch } from '../../src/lifecycle/patch/snapshotPatch.js'; -import { shouldDelayUiOps } from '../../src/lifecycle/ref/delay.js'; -import { clearListGlobal } from '../../src/list.js'; import { globalPipelineOptions, setPipeline } from '../../src/lynx/performance.js'; -import { __root, setRoot } from '../../src/root.js'; -import { SnapshotInstance, backgroundSnapshotInstanceManager, snapshotInstanceManager } from '../../src/snapshot.js'; -import { hydrationMap } from '../../src/snapshotInstanceHydrationMap.js'; +import { clearListGlobal } from '../../src/list.js'; export class EnvManager { root: typeof __root | undefined; @@ -72,8 +70,6 @@ export class EnvManager { backgroundSnapshotInstanceManager.nextId = 0; snapshotInstanceManager.clear(); snapshotInstanceManager.nextId = 0; - hydrationMap.clear(); - shouldDelayUiOps.value = true; clearListGlobal(); deinitGlobalSnapshotPatch(); this.switchToBackground(); diff --git a/packages/react/runtime/__test__/utils/globals.js b/packages/react/runtime/__test__/utils/globals.js index c6604cbb3f..d2928a9df7 100644 --- a/packages/react/runtime/__test__/utils/globals.js +++ b/packages/react/runtime/__test__/utils/globals.js @@ -35,46 +35,6 @@ const performance = { }), }; -class SelectorQuery { - static execLog = vi.fn(); - id = ''; - method = ''; - params = undefined; - - select(id) { - this.id = id; - return this; - } - - invoke(...args) { - this.method = 'invoke'; - this.params = args; - return this; - } - - path(...args) { - this.method = 'path'; - this.params = args; - return this; - } - - fields(...args) { - this.method = 'fields'; - this.params = args; - return this; - } - - setNativeProps(...args) { - this.method = 'setNativeProps'; - this.params = args; - return this; - } - - exec() { - SelectorQuery.execLog(this.id, this.method, this.params); - } -} - function injectGlobals() { globalThis.__DEV__ = true; globalThis.__PROFILE__ = true; @@ -92,28 +52,11 @@ function injectGlobals() { globalThis.lynx = { getNativeApp: () => app, performance, - createSelectorQuery: () => { - return new SelectorQuery(); - }, - getElementByIdTasks: vi.fn(), - getElementById: vi.fn((id) => { + createSelectorQuery: vi.fn(() => { return { - animate: vi.fn(() => { - lynx.getElementByIdTasks('animate'); - return { - play: () => { - lynx.getElementByIdTasks('play'); - }, - pause: () => { - lynx.getElementByIdTasks('pause'); - }, - cancel: () => { - lynx.getElementByIdTasks('cancel'); - }, - }; - }), - setProperty: (property, value) => { - lynx.getElementByIdTasks('setProperty', property, value); + selectUniqueID: function(uid) { + this.uid = uid; + return this; }, }; }), diff --git a/packages/react/runtime/lazy/internal.js b/packages/react/runtime/lazy/internal.js index bac01f2d06..f26c184d94 100644 --- a/packages/react/runtime/lazy/internal.js +++ b/packages/react/runtime/lazy/internal.js @@ -16,7 +16,6 @@ export const { __page, __pageId, __root, - applyRefs, createSnapshot, loadDynamicJS, loadLazyBundle, diff --git a/packages/react/runtime/src/backgroundSnapshot.ts b/packages/react/runtime/src/backgroundSnapshot.ts index 02a1f8452f..cc27f82b48 100644 --- a/packages/react/runtime/src/backgroundSnapshot.ts +++ b/packages/react/runtime/src/backgroundSnapshot.ts @@ -22,7 +22,6 @@ import { takeGlobalSnapshotPatch, } from './lifecycle/patch/snapshotPatch.js'; import { globalPipelineOptions } from './lynx/performance.js'; -import { transformSpread } from './snapshot/spread.js'; import type { SerializedSnapshotInstance } from './snapshot.js'; import { DynamicPartType, @@ -30,7 +29,8 @@ import { snapshotManager, traverseSnapshotInstance, } from './snapshot.js'; -import { hydrationMap } from './snapshotInstanceHydrationMap.js'; +import { markRefToRemove } from './snapshot/ref.js'; +import { transformSpread } from './snapshot/spread.js'; import { isDirectOrDeepEqual } from './utils.js'; import { onPostWorkletCtx } from './worklet/ctx.js'; @@ -188,7 +188,7 @@ export class BackgroundSnapshotInstance { for (let index = 0; index < value.length; index++) { const { needUpdate, valueToCommit } = this.setAttributeImpl(value[index], oldValues[index], index); if (needUpdate) { - __globalSnapshotPatch!.push( + __globalSnapshotPatch?.push( SnapshotOperation.SetAttribute, this.__id, index, @@ -203,7 +203,7 @@ export class BackgroundSnapshotInstance { const { valueToCommit } = this.setAttributeImpl(value[index], null, index); patch[index] = valueToCommit; } - __globalSnapshotPatch!.push( + __globalSnapshotPatch?.push( SnapshotOperation.SetAttributes, this.__id, patch, @@ -238,6 +238,9 @@ export class BackgroundSnapshotInstance { valueToCommit: any; } { if (!newValue) { + if (oldValue && oldValue.__ref) { + markRefToRemove(`${this.__id}:${index}:`, oldValue); + } return { needUpdate: oldValue !== newValue, valueToCommit: newValue }; } @@ -250,7 +253,10 @@ export class BackgroundSnapshotInstance { // use __spread to cache the transform result for next diff newValue.__spread = newSpread; if (needUpdate) { - for (const key in newSpread) { + if (oldSpread && oldSpread.ref) { + markRefToRemove(`${this.__id}:${index}:ref`, oldValue.ref); + } + for (let key in newSpread) { const newSpreadValue = newSpread[key]; if (!newSpreadValue) { continue; @@ -259,15 +265,22 @@ export class BackgroundSnapshotInstance { newSpread[key] = onPostWorkletCtx(newSpreadValue as Worklet); } else if ((newSpreadValue as any).__isGesture) { processGestureBackground(newSpreadValue as GestureKind); - } else if (key == '__lynx_timing_flag' && oldSpread?.[key] != newSpreadValue && globalPipelineOptions) { - globalPipelineOptions.needTimestamps = true; + } else if (key == '__lynx_timing_flag' && oldSpread?.[key] != newSpreadValue) { + if (globalPipelineOptions) { + globalPipelineOptions.needTimestamps = true; + } } } } return { needUpdate, valueToCommit: newSpread }; } if (newValue.__ref) { - return { needUpdate: false, valueToCommit: 1 }; + // force update to update ref value + // TODO: ref: optimize this. The ref update maybe can be done on the background thread to reduce updating. + // The old ref must have a place to be stored because it needs to be cleared when the main thread returns. + markRefToRemove(`${this.__id}:${index}:`, oldValue); + // update ref. On the main thread, the ref id will be replaced with value's sign when updating. + return { needUpdate: true, valueToCommit: newValue.__ref }; } if (newValue._wkltId) { return { needUpdate: true, valueToCommit: onPostWorkletCtx(newValue) }; @@ -288,7 +301,8 @@ export class BackgroundSnapshotInstance { } if (newType === 'function') { if (newValue.__ref) { - return { needUpdate: false, valueToCommit: 1 }; + markRefToRemove(`${this.__id}:${index}:`, oldValue); + return { needUpdate: true, valueToCommit: newValue.__ref }; } /* event */ return { needUpdate: !oldValue, valueToCommit: 1 }; @@ -321,7 +335,6 @@ export function hydrate( before: SerializedSnapshotInstance, after: BackgroundSnapshotInstance, ) => { - hydrationMap.set(after.__id, before.id); backgroundSnapshotInstanceManager.updateId(after.__id, before.id); after.__values?.forEach((value, index) => { const old = before.values![index]; @@ -331,7 +344,7 @@ export function hydrate( // `value.__spread` my contain event ids using snapshot ids before hydration. Remove it. delete value.__spread; value = transformSpread(after, index, value); - for (const key in value) { + for (let key in value) { if (value[key] && value[key]._wkltId) { onPostWorkletCtx(value[key]); } else if (value[key] && value[key].__isGesture) { @@ -340,8 +353,12 @@ export function hydrate( } after.__values![index]!.__spread = value; } else if (value.__ref) { - // skip patch - value = old; + if (old) { + // skip patch + value = old; + } else { + value = value.__ref; + } } else if (typeof value === 'function') { value = `${after.__id}:${index}:`; } diff --git a/packages/react/runtime/src/hooks/react.ts b/packages/react/runtime/src/hooks/react.ts index 95d4665884..c582e96e40 100644 --- a/packages/react/runtime/src/hooks/react.ts +++ b/packages/react/runtime/src/hooks/react.ts @@ -9,7 +9,7 @@ import { useId, useImperativeHandle, useMemo, - useEffect as usePreactEffect, + useLayoutEffect as usePreactLayoutEffect, useReducer, useRef, useState, @@ -29,7 +29,7 @@ import type { DependencyList, EffectCallback } from 'react'; * @deprecated `useLayoutEffect` in the background thread cannot offer the precise timing for reading layout information and synchronously re-render, which is different from React. */ function useLayoutEffect(effect: EffectCallback, deps?: DependencyList): void { - return usePreactEffect(effect, deps); + return usePreactLayoutEffect(effect, deps); } /** @@ -42,7 +42,7 @@ function useLayoutEffect(effect: EffectCallback, deps?: DependencyList): void { * @public */ function useEffect(effect: EffectCallback, deps?: DependencyList): void { - return usePreactEffect(effect, deps); + return usePreactLayoutEffect(effect, deps); } export { diff --git a/packages/react/runtime/src/internal.ts b/packages/react/runtime/src/internal.ts index d66f23754b..40c8063496 100644 --- a/packages/react/runtime/src/internal.ts +++ b/packages/react/runtime/src/internal.ts @@ -26,7 +26,7 @@ export const __DynamicPartChildren_0: [DynamicPartType, number][] = [[DynamicPar export { updateSpread } from './snapshot/spread.js'; export { updateEvent } from './snapshot/event.js'; -export { updateRef, transformRef, applyRefs } from './snapshot/ref.js'; +export { updateRef, transformRef } from './snapshot/ref.js'; export { updateWorkletEvent } from './snapshot/workletEvent.js'; export { updateWorkletRef } from './snapshot/workletRef.js'; export { updateGesture } from './snapshot/gesture.js'; diff --git a/packages/react/runtime/src/lifecycle/event/delayLifecycleEvents.ts b/packages/react/runtime/src/lifecycle/event/delayLifecycleEvents.ts index 4df1b7bb80..1b0c61a6da 100644 --- a/packages/react/runtime/src/lifecycle/event/delayLifecycleEvents.ts +++ b/packages/react/runtime/src/lifecycle/event/delayLifecycleEvents.ts @@ -1,7 +1,18 @@ +import { LifecycleConstant } from '../../lifecycleConstant.js'; + const delayedLifecycleEvents: [type: string, data: any][] = []; function delayLifecycleEvent(type: string, data: any): void { - delayedLifecycleEvents.push([type, data]); + // We need to ensure that firstScreen events are executed before other events. + // This is because firstScreen events are used to initialize the dom tree, + // and other events depend on the dom tree being fully constructed. + // There might be some edge cases where ctx cannot be found in `ref` lifecycle event, + // and they should be ignored safely. + if (type === LifecycleConstant.firstScreen) { + delayedLifecycleEvents.unshift([type, data]); + } else { + delayedLifecycleEvents.push([type, data]); + } } /** diff --git a/packages/react/runtime/src/lifecycle/event/jsReady.ts b/packages/react/runtime/src/lifecycle/event/jsReady.ts index 93863d5e91..eaa1d23765 100644 --- a/packages/react/runtime/src/lifecycle/event/jsReady.ts +++ b/packages/react/runtime/src/lifecycle/event/jsReady.ts @@ -1,5 +1,6 @@ import { LifecycleConstant } from '../../lifecycleConstant.js'; import { __root } from '../../root.js'; +import { takeGlobalRefPatchMap } from '../../snapshot/ref.js'; let isJSReady: boolean; let jsReadyEventIdSwap: Record; @@ -10,6 +11,7 @@ function jsReady(): void { LifecycleConstant.firstScreen, /* FIRST_SCREEN */ { root: JSON.stringify(__root), + refPatch: JSON.stringify(takeGlobalRefPatchMap()), jsReadyEventIdSwap, }, ]); diff --git a/packages/react/runtime/src/lifecycle/patch/commit.ts b/packages/react/runtime/src/lifecycle/patch/commit.ts index 57d1b4cee2..ad3937e3f2 100644 --- a/packages/react/runtime/src/lifecycle/patch/commit.ts +++ b/packages/react/runtime/src/lifecycle/patch/commit.ts @@ -32,8 +32,8 @@ import { setPipeline, } from '../../lynx/performance.js'; import { CATCH_ERROR, COMMIT, RENDER_CALLBACKS, VNODE } from '../../renderToOpcodes/constants.js'; -import { applyDelayedRefs } from '../../snapshot/ref.js'; import { backgroundSnapshotInstanceManager } from '../../snapshot.js'; +import { updateBackgroundRefs } from '../../snapshot/ref.js'; import { isEmptyObject } from '../../utils.js'; import { takeWorkletRefInitValuePatch } from '../../worklet/workletRefPool.js'; import { runDelayedUnmounts, takeDelayedUnmounts } from '../delayUnmount.js'; @@ -43,7 +43,7 @@ import { takeGlobalSnapshotPatch } from './snapshotPatch.js'; let globalFlushOptions: FlushOptions = {}; -const globalCommitTaskMap: Map void> = /*@__PURE__*/ new Map void>(); +const globalCommitTaskMap: Map void> = /*@__PURE__*/ new Map(); let nextCommitTaskId = 1; let globalBackgroundSnapshotInstancesToRemove: number[] = []; @@ -52,7 +52,6 @@ let globalBackgroundSnapshotInstancesToRemove: number[] = []; * A single patch operation. */ interface Patch { - // TODO: ref: do we need `id`? id: number; snapshotPatch?: SnapshotPatch; workletRefInitValuePatch?: [id: number, value: unknown][]; @@ -113,6 +112,7 @@ function replaceCommitHook(): void { // Register the commit task globalCommitTaskMap.set(commitTaskId, () => { + updateBackgroundRefs(commitTaskId); runDelayedUnmounts(delayedUnmounts); originalPreactCommit?.(vnode, renderCallbacks); renderCallbacks.some(wrapper => { @@ -140,7 +140,6 @@ function replaceCommitHook(): void { globalFlushOptions = {}; if (!snapshotPatch && workletRefInitValuePatch.length === 0) { // before hydration, skip patch - applyDelayedRefs(); return; } @@ -170,8 +169,6 @@ function replaceCommitHook(): void { globalCommitTaskMap.delete(commitTaskId); } }); - - applyDelayedRefs(); }; options[COMMIT] = commit as ((...args: Parameters) => void); } @@ -246,6 +243,7 @@ export { globalBackgroundSnapshotInstancesToRemove, globalCommitTaskMap, globalFlushOptions, + nextCommitTaskId, replaceCommitHook, replaceRequestAnimationFrame, type PatchList, diff --git a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts index bf261a9884..da2001ba59 100644 --- a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts +++ b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts @@ -4,13 +4,15 @@ import { clearDelayedWorklets, updateWorkletRefInitValueChanges } from '@lynx-js/react/worklet-runtime/bindings'; +import type { PatchList, PatchOptions } from './commit.js'; +import { snapshotPatchApply } from './snapshotPatchApply.js'; import { LifecycleConstant } from '../../lifecycleConstant.js'; import { __pendingListUpdates } from '../../list.js'; import { PerformanceTimingKeys, markTiming, setPipeline } from '../../lynx/performance.js'; +import { takeGlobalRefPatchMap } from '../../snapshot/ref.js'; import { __page } from '../../snapshot.js'; +import { isEmptyObject } from '../../utils.js'; import { getReloadVersion } from '../pass.js'; -import type { PatchList, PatchOptions } from './commit.js'; -import { snapshotPatchApply } from './snapshotPatchApply.js'; function updateMainThread( { data, patchOptions }: { @@ -30,7 +32,7 @@ function updateMainThread( markTiming(PerformanceTimingKeys.parseChangesEnd); markTiming(PerformanceTimingKeys.patchChangesStart); - for (const { snapshotPatch, workletRefInitValuePatch } of patchList) { + for (const { snapshotPatch, workletRefInitValuePatch, id } of patchList) { updateWorkletRefInitValueChanges(workletRefInitValuePatch); __pendingListUpdates.clear(); if (snapshotPatch) { @@ -39,6 +41,8 @@ function updateMainThread( __pendingListUpdates.flush(); // console.debug('********** Lepus updatePatch:'); // printSnapshotInstance(snapshotInstanceManager.values.get(-1)!); + + commitMainThreadPatchUpdate(id); } markTiming(PerformanceTimingKeys.patchChangesEnd); markTiming(PerformanceTimingKeys.mtsRenderEnd); @@ -56,7 +60,14 @@ function injectUpdateMainThread(): void { Object.assign(globalThis, { [LifecycleConstant.patchUpdate]: updateMainThread }); } +function commitMainThreadPatchUpdate(commitTaskId?: number): void { + const refPatch = takeGlobalRefPatchMap(); + if (!isEmptyObject(refPatch)) { + __OnLifecycleEvent([LifecycleConstant.ref, { commitTaskId, refPatch: JSON.stringify(refPatch) }]); + } +} + /** * @internal */ -export { injectUpdateMainThread }; +export { commitMainThreadPatchUpdate, injectUpdateMainThread }; diff --git a/packages/react/runtime/src/lifecycle/ref/delay.ts b/packages/react/runtime/src/lifecycle/ref/delay.ts deleted file mode 100644 index 8cea860c47..0000000000 --- a/packages/react/runtime/src/lifecycle/ref/delay.ts +++ /dev/null @@ -1,99 +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. - -import type { NodesRef, SelectorQuery } from '@lynx-js/types'; - -import { hydrationMap } from '../../snapshotInstanceHydrationMap.js'; - -type RefTask = (nodesRef: NodesRef) => SelectorQuery; - -/** - * A flag to indicate whether UI operations should be delayed. - * When set to true, UI operations will be queued in the `delayedUiOps` array - * and executed later when `runDelayedUiOps` is called. - * This is used before hydration to ensure UI operations are batched - * and executed at the appropriate time. - */ -const shouldDelayUiOps = { value: true }; - -/** - * An array of functions that will be executed later when `runDelayedUiOps` is called. - * These functions contain UI operations that need to be delayed. - */ -const delayedUiOps: (() => void)[] = []; - -/** - * Runs a task either immediately or delays it based on the `shouldDelayUiOps` flag. - * @param task - The function to execute. - */ -function runOrDelay(task: () => void): void { - if (shouldDelayUiOps.value) { - delayedUiOps.push(task); - } else { - task(); - } -} - -/** - * Executes all delayed UI operations. - */ -function runDelayedUiOps(): void { - for (const task of delayedUiOps) { - task(); - } - shouldDelayUiOps.value = false; - delayedUiOps.length = 0; -} - -/** - * A proxy class designed for managing and executing reference-based tasks. - * It delays the execution of tasks until hydration is complete. - */ -class RefProxy { - private readonly refAttr: [snapshotInstanceId: number, expIndex: number]; - private task: RefTask | undefined; - - constructor(refAttr: [snapshotInstanceId: number, expIndex: number]) { - this.refAttr = refAttr; - } - - private setTask( - method: K, - args: Parameters, - ): this { - this.task = (nodesRef) => { - return (nodesRef[method] as unknown as (...args: any[]) => SelectorQuery)(...args); - }; - return this; - } - - invoke(...args: Parameters): RefProxy { - return new RefProxy(this.refAttr).setTask('invoke', args); - } - - path(...args: Parameters): RefProxy { - return new RefProxy(this.refAttr).setTask('path', args); - } - - fields(...args: Parameters): RefProxy { - return new RefProxy(this.refAttr).setTask('fields', args); - } - - setNativeProps(...args: Parameters): RefProxy { - return new RefProxy(this.refAttr).setTask('setNativeProps', args); - } - - exec(): void { - runOrDelay(() => { - const realRefId = hydrationMap.get(this.refAttr[0]) ?? this.refAttr[0]; - const refSelector = `[react-ref-${realRefId}-${this.refAttr[1]}]`; - this.task!(lynx.createSelectorQuery().select(refSelector)).exec(); - }); - } -} - -/** - * @internal - */ -export { RefProxy, runDelayedUiOps, shouldDelayUiOps }; diff --git a/packages/react/runtime/src/lifecycle/reload.ts b/packages/react/runtime/src/lifecycle/reload.ts index 32bf5cf29d..7ffd950e0b 100644 --- a/packages/react/runtime/src/lifecycle/reload.ts +++ b/packages/react/runtime/src/lifecycle/reload.ts @@ -14,13 +14,13 @@ import { LifecycleConstant } from '../lifecycleConstant.js'; import { __pendingListUpdates } from '../list.js'; import { __root, setRoot } from '../root.js'; import { SnapshotInstance, __page, snapshotInstanceManager } from '../snapshot.js'; +import { takeGlobalRefPatchMap } from '../snapshot/ref.js'; import { isEmptyObject } from '../utils.js'; -import { destroyBackground } from './destroy.js'; import { destroyWorklet } from '../worklet/destroy.js'; +import { destroyBackground } from './destroy.js'; import { clearJSReadyEventIdSwap, isJSReady } from './event/jsReady.js'; import { increaseReloadVersion } from './pass.js'; import { deinitGlobalSnapshotPatch } from './patch/snapshotPatch.js'; -import { shouldDelayUiOps } from './ref/delay.js'; import { renderMainThread } from './render.js'; function reloadMainThread(data: any, options: UpdatePageOption): void { @@ -55,6 +55,7 @@ function reloadMainThread(data: any, options: UpdatePageOption): void { LifecycleConstant.firstScreen, /* FIRST_SCREEN */ { root: JSON.stringify(__root), + refPatch: JSON.stringify(takeGlobalRefPatchMap()), }, ]); } @@ -81,7 +82,6 @@ function reloadBackground(updateData: Record): void { // COW when modify `lynx.__initData` to make sure Provider & Consumer works lynx.__initData = Object.assign({}, lynx.__initData, updateData); - shouldDelayUiOps.value = true; render(__root.__jsx, __root as any); if (__PROFILE__) { diff --git a/packages/react/runtime/src/lifecycleConstant.ts b/packages/react/runtime/src/lifecycleConstant.ts index de5d956ce1..aaf25f033d 100644 --- a/packages/react/runtime/src/lifecycleConstant.ts +++ b/packages/react/runtime/src/lifecycleConstant.ts @@ -5,6 +5,7 @@ export class LifecycleConstant { public static readonly firstScreen = 'rLynxFirstScreen'; public static readonly updateFromRoot = 'updateFromRoot'; public static readonly globalEventFromLepus = 'globalEventFromLepus'; + public static readonly ref = 'rLynxRef'; public static readonly jsReady = 'rLynxJSReady'; public static readonly patchUpdate = 'rLynxChange'; } diff --git a/packages/react/runtime/src/list.ts b/packages/react/runtime/src/list.ts index 973f1469bc..9049146d75 100644 --- a/packages/react/runtime/src/list.ts +++ b/packages/react/runtime/src/list.ts @@ -2,6 +2,7 @@ // 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 { hydrate } from './hydrate.js'; +import { commitMainThreadPatchUpdate } from './lifecycle/patch/updateMainThread.js'; import type { SnapshotInstance } from './snapshot.js'; export interface ListUpdateInfo { @@ -338,6 +339,7 @@ export function componentAtIndexFactory( __FlushElementTree(root, flushOptions); } signMap.set(sign, childCtx); + commitMainThreadPatchUpdate(undefined); return sign; } @@ -358,6 +360,7 @@ export function componentAtIndexFactory( }); } signMap.set(sign, childCtx); + commitMainThreadPatchUpdate(undefined); return sign; }; diff --git a/packages/react/runtime/src/lynx/calledByNative.ts b/packages/react/runtime/src/lynx/calledByNative.ts index 46a451e8a8..ecf29588a0 100644 --- a/packages/react/runtime/src/lynx/calledByNative.ts +++ b/packages/react/runtime/src/lynx/calledByNative.ts @@ -9,6 +9,7 @@ import { LifecycleConstant } from '../lifecycleConstant.js'; import { __pendingListUpdates } from '../list.js'; import { ssrHydrateByOpcodes } from '../opcodes.js'; import { __root, setRoot } from '../root.js'; +import { takeGlobalRefPatchMap } from '../snapshot/ref.js'; import { SnapshotInstance, __page, setupPage } from '../snapshot.js'; import { isEmptyObject } from '../utils.js'; import { PerformanceTimingKeys, markTiming, setPipeline } from './performance.js'; @@ -117,6 +118,9 @@ function updatePage(data: any, options?: UpdatePageOption): void { markTiming(PerformanceTimingKeys.updateDiffVdomStart); { __pendingListUpdates.clear(); + + // ignore ref & unref before jsReady + takeGlobalRefPatchMap(); renderMainThread(); // As said by codename `jsReadyEventIdSwap`, this swap will only be used for event remap, // because ref & unref cause by previous render will be ignored diff --git a/packages/react/runtime/src/lynx/runWithForce.ts b/packages/react/runtime/src/lynx/runWithForce.ts index 53d429b5ba..88b864f0d0 100644 --- a/packages/react/runtime/src/lynx/runWithForce.ts +++ b/packages/react/runtime/src/lynx/runWithForce.ts @@ -2,13 +2,17 @@ import { options } from 'preact'; import type { VNode } from 'preact'; import { COMPONENT, DIFF, DIFFED, FORCE } from '../renderToOpcodes/constants.js'; +const sForcedVNode = Symbol('FORCE'); + +type PatchedVNode = VNode & { [sForcedVNode]?: true }; + export function runWithForce(cb: () => void): void { // save vnode and its `_component` in WeakMap const m = new WeakMap(); const oldDiff = options[DIFF]; - options[DIFF] = (vnode: VNode) => { + options[DIFF] = (vnode: PatchedVNode) => { if (oldDiff) { oldDiff(vnode); } @@ -28,19 +32,26 @@ export function runWithForce(cb: () => void): void { return m.get(vnode); }, }); + vnode[sForcedVNode] = true; }; const oldDiffed = options[DIFFED]; - options[DIFFED] = (vnode: VNode) => { + options[DIFFED] = (vnode: PatchedVNode) => { if (oldDiffed) { oldDiffed(vnode); } - // delete is a reverse operation of previous `Object.defineProperty` - delete vnode[COMPONENT]; - // restore - vnode[COMPONENT] = m.get(vnode); + // There would be cases when `options[DIFF]` has been reset while options[DIFFED] is not, + // so we need to check if `vnode` is patched by `options[DIFF]`. + // We only want to change the patched vnode + if (vnode[sForcedVNode]) { + // delete is a reverse operation of previous `Object.defineProperty` + delete vnode[COMPONENT]; + delete vnode[sForcedVNode]; + // restore + vnode[COMPONENT] = m.get(vnode); + } }; try { diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index dc8a2a2f97..22679d9302 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -18,10 +18,10 @@ import { delayedEvents, delayedPublishEvent } from '../lifecycle/event/delayEven import { delayLifecycleEvent, delayedLifecycleEvents } from '../lifecycle/event/delayLifecycleEvents.js'; import { commitPatchUpdate, genCommitTaskId, globalCommitTaskMap } from '../lifecycle/patch/commit.js'; import type { PatchList } from '../lifecycle/patch/commit.js'; -import { runDelayedUiOps } from '../lifecycle/ref/delay.js'; import { reloadBackground } from '../lifecycle/reload.js'; import { CHILDREN } from '../renderToOpcodes/constants.js'; import { __root } from '../root.js'; +import { globalRefsToSet, updateBackgroundRefs } from '../snapshot/ref.js'; import { backgroundSnapshotInstanceManager } from '../snapshot.js'; import { destroyWorklet } from '../worklet/destroy.js'; @@ -72,7 +72,7 @@ function onLifecycleEvent([type, data]: [string, any]) { function onLifecycleEventImpl(type: string, data: any): void { switch (type) { case LifecycleConstant.firstScreen: { - const { root: lepusSide, jsReadyEventIdSwap } = data; + const { root: lepusSide, refPatch, jsReadyEventIdSwap } = data; if (__PROFILE__) { console.profile('hydrate'); } @@ -109,6 +109,16 @@ function onLifecycleEventImpl(type: string, data: any): void { lynxCoreInject.tt.publishEvent = publishEvent; lynxCoreInject.tt.publicComponentEvent = publicComponentEvent; + if (__PROFILE__) { + console.profile('patchRef'); + } + if (refPatch) { + globalRefsToSet.set(0, JSON.parse(refPatch)); + updateBackgroundRefs(0); + } + if (__PROFILE__) { + console.profileEnd(); + } // console.debug("********** After hydration:"); // printSnapshotInstance(__root as BackgroundSnapshotInstance); if (__PROFILE__) { @@ -121,6 +131,7 @@ function onLifecycleEventImpl(type: string, data: any): void { const obj = commitPatchUpdate(patchList, { isHydration: true }); lynx.getNativeApp().callLepusMethod(LifecycleConstant.patchUpdate, obj, () => { + updateBackgroundRefs(commitTaskId); globalCommitTaskMap.forEach((commitTask, id) => { if (id > commitTaskId) { return; @@ -129,7 +140,6 @@ function onLifecycleEventImpl(type: string, data: any): void { globalCommitTaskMap.delete(id); }); }); - runDelayedUiOps(); break; } case LifecycleConstant.globalEventFromLepus: { @@ -137,6 +147,16 @@ function onLifecycleEventImpl(type: string, data: any): void { lynx.getJSModule('GlobalEventEmitter').trigger(eventName, params); break; } + case LifecycleConstant.ref: { + const { refPatch, commitTaskId } = data; + if (commitTaskId) { + globalRefsToSet.set(commitTaskId, JSON.parse(refPatch)); + } else { + globalRefsToSet.set(0, JSON.parse(refPatch)); + updateBackgroundRefs(0); + } + break; + } } } diff --git a/packages/react/runtime/src/snapshot.ts b/packages/react/runtime/src/snapshot.ts index 846eef8b4d..30ece552d1 100644 --- a/packages/react/runtime/src/snapshot.ts +++ b/packages/react/runtime/src/snapshot.ts @@ -166,7 +166,7 @@ export const backgroundSnapshotInstanceManager: { if (!res || (res.length != 2 && res.length != 3)) { throw new Error('Invalid ctx format: ' + str); } - const id = Number(res[0]); + let id = Number(res[0]); const expIndex = Number(res[1]); const ctx = this.values.get(id); if (!ctx) { @@ -266,6 +266,7 @@ export class SnapshotInstance { __element_root?: FiberElement | undefined; __values?: any[] | undefined; __current_slot_index = 0; + __ref_set?: Set; __worklet_ref_set?: Set | Worklet>; __listItemPlatformInfo?: any; @@ -288,15 +289,15 @@ export class SnapshotInstance { // CSS Scope is removed(We only need to call `__SetCSSId` when there is `entryName`) // Or an old bundle(`__SetCSSId` is called in `create`), we skip calling `__SetCSSId` if (entryName !== DEFAULT_ENTRY_NAME && entryName !== undefined) { - __SetCSSId(this.__elements, DEFAULT_CSS_ID, entryName); + __SetCSSId(this.__elements!, DEFAULT_CSS_ID, entryName); } } else { // cssId !== undefined if (entryName !== DEFAULT_ENTRY_NAME && entryName !== undefined) { // For lazy bundle, we need add `entryName` to the third params - __SetCSSId(this.__elements, cssId, entryName); + __SetCSSId(this.__elements!, cssId, entryName); } else { - __SetCSSId(this.__elements, cssId); + __SetCSSId(this.__elements!, cssId); } } @@ -553,6 +554,7 @@ export class SnapshotInstance { return; } + // TODO: ref: can this be done on the background thread? unref(child, true); r(); if (this.__elements) { diff --git a/packages/react/runtime/src/snapshot/ref.ts b/packages/react/runtime/src/snapshot/ref.ts index 26b31230f4..248be086af 100644 --- a/packages/react/runtime/src/snapshot/ref.ts +++ b/packages/react/runtime/src/snapshot/ref.ts @@ -3,18 +3,21 @@ // LICENSE file in the root directory of this source tree. import type { Worklet, WorkletRef } from '@lynx-js/react/worklet-runtime/bindings'; -import type { SnapshotInstance } from '../snapshot.js'; +import { nextCommitTaskId } from '../lifecycle/patch/commit.js'; +import { SnapshotInstance, backgroundSnapshotInstanceManager } from '../snapshot.js'; import { workletUnRef } from './workletRef.js'; -import type { BackgroundSnapshotInstance } from '../backgroundSnapshot.js'; -import { RefProxy } from '../lifecycle/ref/delay.js'; -const refsToApply: (Ref[] | BackgroundSnapshotInstance)[] = []; - -type Ref = (((ref: RefProxy) => () => void) | { current: RefProxy | null }) & { - _unmount?: () => void; -}; +let globalRefPatch: Record = {}; +const globalRefsToRemove: Map> = /* @__PURE__ */ new Map(); +const globalRefsToSet: Map> = /* @__PURE__ */ new Map(); +let nextRefId = 1; function unref(snapshot: SnapshotInstance, recursive: boolean): void { + snapshot.__ref_set?.forEach(v => { + globalRefPatch[v] = null; + }); + snapshot.__ref_set?.clear(); + snapshot.__worklet_ref_set?.forEach(v => { if (v) { workletUnRef(v as Worklet | WorkletRef); @@ -29,60 +32,91 @@ function unref(snapshot: SnapshotInstance, recursive: boolean): void { } } -// This function is modified from preact source code. -function applyRef(ref: Ref, value: null | [snapshotInstanceId: number, expIndex: number]): void { - const newRef = value && new RefProxy(value); +function applyRef(ref: any, value: any) { + // TODO: ref: exceptions thrown in user functions should be able to be caught by an Error Boundary + if (typeof ref == 'function') { + const hasRefUnmount = typeof ref._unmount == 'function'; + if (hasRefUnmount) { + // @ts-ignore TS doesn't like moving narrowing checks into variables + ref._unmount(); + } - try { - if (typeof ref == 'function') { - const hasRefUnmount = typeof ref._unmount == 'function'; - if (hasRefUnmount) { - ref._unmount!(); - } + if (!hasRefUnmount || value != null) { + // Store the cleanup function on the function + // instance object itself to avoid shape + // transitioning vnode + ref._unmount = ref(value); + } + } else ref.current = value; +} - if (!hasRefUnmount || newRef != null) { - // Store the cleanup function on the function - // instance object itself to avoid shape - // transitioning vnode - ref._unmount = ref(newRef!); +function updateBackgroundRefs(commitId: number): void { + const oldRefMap = globalRefsToRemove.get(commitId); + if (oldRefMap) { + globalRefsToRemove.delete(commitId); + for (const ref of oldRefMap.values()) { + applyRef(ref, null); + } + } + const newRefMap = globalRefsToSet.get(commitId); + if (newRefMap) { + globalRefsToSet.delete(commitId); + for (const sign in newRefMap) { + const ref = backgroundSnapshotInstanceManager.getValueBySign(sign); + if (ref) { + // TODO: ref: support __REF_FIRE_IMMEDIATELY__ + const v = newRefMap[sign] && lynx.createSelectorQuery().selectUniqueID(newRefMap[sign]); + applyRef(ref, v); } - } else ref.current = newRef; - /* v8 ignore start */ - } catch (e) { - lynx.reportError(e as Error); + } } - /* v8 ignore stop */ } function updateRef( snapshot: SnapshotInstance, expIndex: number, - _oldValue: any, + oldValue: any, elementIndex: number, + spreadKey: string, ): void { - const value: unknown = snapshot.__values![expIndex]; + const value = snapshot.__values![expIndex]; let ref; - if (typeof value === 'string') { + if (!value) { + ref = undefined; + } else if (typeof value === 'string') { ref = value; } else { - ref = `react-ref-${snapshot.__id}-${expIndex}`; + ref = `${snapshot.__id}:${expIndex}:${spreadKey}`; } snapshot.__values![expIndex] = ref; if (snapshot.__elements && ref) { - __SetAttribute(snapshot.__elements[elementIndex]!, ref, 1); + __SetAttribute(snapshot.__elements[elementIndex]!, 'has-react-ref', true); + const uid = __GetElementUniqueID(snapshot.__elements[elementIndex]!); + globalRefPatch[ref] = uid; + snapshot.__ref_set ??= new Set(); + snapshot.__ref_set.add(ref); + } + if (oldValue !== ref) { + snapshot.__ref_set?.delete(oldValue); } } -function transformRef(ref: unknown): Ref | null | undefined { +function takeGlobalRefPatchMap(): Record { + const patch = globalRefPatch; + globalRefPatch = {}; + return patch; +} + +function transformRef(ref: unknown): Function | (object & Record<'current', unknown>) | null | undefined { if (ref === undefined || ref === null) { return ref; } if (typeof ref === 'function' || (typeof ref === 'object' && 'current' in ref)) { if ('__ref' in ref) { - return ref as Ref; + return ref; } - return Object.defineProperty(ref, '__ref', { value: 1 }) as Ref; + return Object.defineProperty(ref, '__ref', { value: nextRefId++ }); } throw new Error( `Elements' "ref" property should be a function, or an object created ` @@ -90,68 +124,25 @@ function transformRef(ref: unknown): Ref | null | undefined { ); } -/** - * Applies refs from a snapshot instance to their corresponding DOM elements. - * - * This function is called directly by preact with a `this` context of a Ref array that collects all - * refs that are applied during the process. - * - * @param snapshotInstance - The snapshot instance containing refs to apply - * - * If snapshotInstance is null, all previously collected refs are cleared. - * Otherwise, it iterates through the snapshot values, finds refs (either direct or in spreads), - * and applies them to their corresponding elements. - */ -function applyRefs(this: Ref[], snapshotInstance: BackgroundSnapshotInstance): void { - if (__LEPUS__) { - // for testing environment only +function markRefToRemove(sign: string, ref: unknown): void { + if (!ref) { return; } - refsToApply.push(this, snapshotInstance); -} - -function applyDelayedRefs(): void { - try { - for (let i = 0; i < refsToApply.length; i += 2) { - const refs = refsToApply[i] as Ref[]; - const snapshotInstance = refsToApply[i + 1] as BackgroundSnapshotInstance; - - if (snapshotInstance == null) { - try { - refs.forEach(ref => { - applyRef(ref, null); - }); - } finally { - refs.length = 0; - } - continue; - } - - for (let i = 0; i < snapshotInstance.__values!.length; i++) { - const value: unknown = snapshotInstance.__values![i]; - if (!value || (typeof value !== 'function' && typeof value !== 'object')) { - continue; - } - - let ref: Ref | undefined; - if ('__ref' in value) { - ref = value as Ref; - } else if ('__spread' in value) { - ref = (value as { ref?: Ref | undefined }).ref; - } - - if (ref) { - applyRef(ref, [snapshotInstance.__id, i]); - refs.push(ref); - } - } - } - } finally { - refsToApply.length = 0; + let oldRefs = globalRefsToRemove.get(nextCommitTaskId); + if (!oldRefs) { + oldRefs = new Map(); + globalRefsToRemove.set(nextCommitTaskId, oldRefs); } + oldRefs.set(sign, ref); } -/** - * @internal - */ -export { updateRef, unref, transformRef, applyRef, applyRefs, applyDelayedRefs }; +export { + globalRefsToRemove, + globalRefsToSet, + markRefToRemove, + takeGlobalRefPatchMap, + transformRef, + unref, + updateBackgroundRefs, + updateRef, +}; diff --git a/packages/react/runtime/src/snapshot/spread.ts b/packages/react/runtime/src/snapshot/spread.ts index 403b84f056..678c39ea55 100644 --- a/packages/react/runtime/src/snapshot/spread.ts +++ b/packages/react/runtime/src/snapshot/spread.ts @@ -91,6 +91,7 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any, } else if (key.startsWith('data-')) { // collected below } else if (key === 'ref') { + snapshot.__ref_set ??= new Set(); const fakeSnapshot = { __values: { get [index]() { @@ -103,8 +104,9 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any, }, __id: snapshot.__id, __elements: snapshot.__elements, + __ref_set: snapshot.__ref_set, } as SnapshotInstance; - updateRef(fakeSnapshot, index, oldValue[key], elementIndex); + updateRef(fakeSnapshot, index, oldValue[key], elementIndex, key); } else if (key.endsWith(':ref')) { snapshot.__worklet_ref_set ??= new Set(); const fakeSnapshot = { @@ -130,7 +132,7 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any, __elements: snapshot.__elements, } as SnapshotInstance; updateGesture(fakeSnapshot, index, oldValue[key], elementIndex, workletType); - } else if ((match = eventRegExp.exec(key))) { + } else if ((match = key.match(eventRegExp))) { const workletType = match[2]; const eventType = eventTypeMap[match[3]!]!; const eventName = match[4]!; @@ -177,6 +179,7 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any, } else if (key.startsWith('data-')) { // collected below } else if (key === 'ref') { + snapshot.__ref_set ??= new Set(); const fakeSnapshot = { __values: { get [index]() { @@ -189,8 +192,9 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any, }, __id: snapshot.__id, __elements: snapshot.__elements, + __ref_set: snapshot.__ref_set, } as SnapshotInstance; - updateRef(fakeSnapshot, index, oldValue[key], elementIndex); + updateRef(fakeSnapshot, index, oldValue[key], elementIndex, key); } else if (key.endsWith(':ref')) { snapshot.__worklet_ref_set ??= new Set(); const fakeSnapshot = { @@ -216,7 +220,7 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any, __elements: snapshot.__elements, } as SnapshotInstance; updateGesture(fakeSnapshot, index, oldValue[key], elementIndex, workletType); - } else if ((match = eventRegExp.exec(key))) { + } else if ((match = key.match(eventRegExp))) { const workletType = match[2]; const eventType = eventTypeMap[match[3]!]!; const eventName = match[4]!; @@ -270,12 +274,8 @@ function transformSpread( value ??= ''; result['className'] = value; } else if (key === 'ref') { - if (__LEPUS__) { - result[key] = value ? 1 : undefined; - } else { - // @ts-ignore - result[key] = transformRef(value)?.__ref; - } + // @ts-ignore + result[key] = transformRef(value)?.__ref; } else if (typeof value === 'function') { result[key] = `${snapshot.__id}:${index}:${key}`; } else { diff --git a/packages/react/runtime/src/snapshot/workletRef.ts b/packages/react/runtime/src/snapshot/workletRef.ts index 5f55b84937..6628ef7dc1 100644 --- a/packages/react/runtime/src/snapshot/workletRef.ts +++ b/packages/react/runtime/src/snapshot/workletRef.ts @@ -1,10 +1,14 @@ // 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 { runWorkletCtx, updateWorkletRef as update } from '@lynx-js/react/worklet-runtime/bindings'; -import type { Worklet, WorkletRef } from '@lynx-js/react/worklet-runtime/bindings'; +import { + type Worklet, + type WorkletRef, + runWorkletCtx, + updateWorkletRef as update, +} from '@lynx-js/react/worklet-runtime/bindings'; -import type { SnapshotInstance } from '../snapshot.js'; +import { SnapshotInstance } from '../snapshot.js'; function workletUnRef(value: Worklet | WorkletRef): void { if ('_wvid' in value) { @@ -38,10 +42,10 @@ function updateWorkletRef( if (value === null || value === undefined) { // do nothing } else if (value._wvid) { - update(value, snapshot.__elements[elementIndex]!); + update(value as any, snapshot.__elements[elementIndex]!); } else if (value._wkltId) { // @ts-ignore - value._unmount = runWorkletCtx(value, [{ elementRefptr: snapshot.__elements[elementIndex]! }]); + value._unmount = runWorkletCtx(value as any, [{ elementRefptr: snapshot.__elements[elementIndex]! }]); } else if (value._type === '__LEPUS__' || value._lepusWorkletHash) { // During the initial render, we will not update the WorkletRef because the background thread is not ready yet. } else { diff --git a/packages/react/runtime/src/snapshotInstanceHydrationMap.ts b/packages/react/runtime/src/snapshotInstanceHydrationMap.ts deleted file mode 100644 index 9cddf69b3e..0000000000 --- a/packages/react/runtime/src/snapshotInstanceHydrationMap.ts +++ /dev/null @@ -1,17 +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. - -/** - * A map to store hydration states between snapshot instances. - * K->V: main thread snapshotInstance IDs -> background snapshotInstance IDs. - * - * The map is used by the ref system to translate between snapshot instance IDs when - * operations need to cross the thread boundary during the commit phase. - */ -const hydrationMap: Map = new Map(); - -/** - * @internal - */ -export { hydrationMap }; diff --git a/packages/react/testing-library/etc/react-lynx-testing-library.api.md b/packages/react/testing-library/etc/react-lynx-testing-library.api.md index 53f6f051e1..2918a26696 100644 --- a/packages/react/testing-library/etc/react-lynx-testing-library.api.md +++ b/packages/react/testing-library/etc/react-lynx-testing-library.api.md @@ -1,4 +1,4 @@ -## API Report File for "@lynx-js/react-lynx-testing-library" +## API Report File for "@lynx-js/reactlynx-testing-library" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/packages/react/testing-library/src/__tests__/act.test.jsx b/packages/react/testing-library/src/__tests__/act.test.jsx index ad324fc170..e1bf4aec5e 100644 --- a/packages/react/testing-library/src/__tests__/act.test.jsx +++ b/packages/react/testing-library/src/__tests__/act.test.jsx @@ -77,7 +77,7 @@ test('findByTestId returns the element', async () => { Hello world! @@ -88,7 +88,7 @@ test('findByTestId returns the element', async () => { expect(await findByTestId('foo')).toMatchInlineSnapshot(` Hello world! @@ -96,12 +96,12 @@ test('findByTestId returns the element', async () => { `); expect(ref.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "1", + "type": 2, + }, + "_selectorQuery": {}, } `); }); diff --git a/packages/react/testing-library/src/__tests__/events.test.jsx b/packages/react/testing-library/src/__tests__/events.test.jsx index 4dba02ef82..679c6f48f6 100644 --- a/packages/react/testing-library/src/__tests__/events.test.jsx +++ b/packages/react/testing-library/src/__tests__/events.test.jsx @@ -1,10 +1,9 @@ // cspell:disable import '@testing-library/jest-dom'; -import { expect, test, vi } from 'vitest'; - -import { createRef } from '@lynx-js/react'; - +import { test } from 'vitest'; import { fireEvent, render } from '..'; +import { createRef } from '@lynx-js/react'; +import { expect, vi } from 'vitest'; const eventTypes = [ { @@ -75,28 +74,29 @@ eventTypes.forEach(({ type, events, elementType, init }, eventTypeIdx) => { if (eventTypeIdx === 0 && eventIdx === 0) { expect(ref).toMatchInlineSnapshot(` { - "current": RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + "current": NodesRef { + "_nodeSelectToken": { + "identifier": "1", + "type": 2, + }, + "_selectorQuery": {}, }, } `); expect(ref.current.constructor.name).toMatchInlineSnapshot( - `"RefProxy"`, + `"NodesRef"`, + ); + const element = __GetElementByUniqueId( + Number(ref.current._nodeSelectToken.identifier), ); - const refId = `react-ref-${ref.current.refAttr[0]}-${ref.current.refAttr[1]}`; - const element = document.querySelector(`[${refId}]`); expect(element).toMatchInlineSnapshot(` `); expect(element.attributes).toMatchInlineSnapshot(` NamedNodeMap { - "react-ref-2-0": "1", + "has-react-ref": "true", } `); expect(element.eventMap).toMatchInlineSnapshot(` diff --git a/packages/react/testing-library/src/__tests__/ref.test.jsx b/packages/react/testing-library/src/__tests__/ref.test.jsx index 51664bf58a..f618a0030e 100644 --- a/packages/react/testing-library/src/__tests__/ref.test.jsx +++ b/packages/react/testing-library/src/__tests__/ref.test.jsx @@ -51,10 +51,10 @@ describe('component ref', () => { @@ -66,23 +66,23 @@ describe('component ref', () => { expect(ref3.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "3", + "type": 2, + }, + "_selectorQuery": {}, }, ], ] `); expect(ref4.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - 2, - 1, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "4", + "type": 2, + }, + "_selectorQuery": {}, } `); expect(cleanup).toBeCalledTimes(0); @@ -121,10 +121,10 @@ describe('element ref', () => { @@ -132,23 +132,23 @@ describe('element ref', () => { expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "2", + "type": 2, + }, + "_selectorQuery": {}, }, ], ] `); expect(ref2.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - 2, - 1, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "3", + "type": 2, + }, + "_selectorQuery": {}, } `); }); @@ -185,10 +185,10 @@ describe('element ref', () => { @@ -196,23 +196,23 @@ describe('element ref', () => { expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "2", + "type": 2, + }, + "_selectorQuery": {}, }, ], ] `); expect(ref2.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - 2, - 1, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "3", + "type": 2, + }, + "_selectorQuery": {}, } `); }); @@ -244,10 +244,10 @@ describe('element ref', () => { @@ -255,23 +255,23 @@ describe('element ref', () => { expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "2", + "type": 2, + }, + "_selectorQuery": {}, }, ], ] `); expect(ref2.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - 2, - 1, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "3", + "type": 2, + }, + "_selectorQuery": {}, } `); act(() => { @@ -281,12 +281,12 @@ describe('element ref', () => { expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "2", + "type": 2, + }, + "_selectorQuery": {}, }, ], [ @@ -328,7 +328,7 @@ describe('element ref', () => { @@ -336,12 +336,12 @@ describe('element ref', () => { expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "2", + "type": 2, + }, + "_selectorQuery": {}, }, ], ] @@ -355,12 +355,12 @@ describe('element ref', () => { expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "2", + "type": 2, + }, + "_selectorQuery": {}, }, ], ] @@ -406,10 +406,10 @@ describe('element ref', () => { @@ -417,23 +417,23 @@ describe('element ref', () => { expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "2", + "type": 2, + }, + "_selectorQuery": {}, }, ], ] `); expect(ref2.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - 2, - 1, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "3", + "type": 2, + }, + "_selectorQuery": {}, } `); expect(lynx.getNativeApp().callLepusMethod).toBeCalledTimes(1); @@ -441,12 +441,12 @@ describe('element ref', () => { expect(ref1.mock.calls).toMatchInlineSnapshot(` [ [ - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "2", + "type": 2, + }, + "_selectorQuery": {}, }, ], ] diff --git a/packages/react/testing-library/src/__tests__/render.test.jsx b/packages/react/testing-library/src/__tests__/render.test.jsx index 60fcef559e..cfd62ca70a 100644 --- a/packages/react/testing-library/src/__tests__/render.test.jsx +++ b/packages/react/testing-library/src/__tests__/render.test.jsx @@ -17,12 +17,12 @@ test('renders view into page', async () => { }; render(); expect(ref.current).toMatchInlineSnapshot(` - RefProxy { - "refAttr": [ - 2, - 0, - ], - "task": undefined, + NodesRef { + "_nodeSelectToken": { + "identifier": "1", + "type": 2, + }, + "_selectorQuery": {}, } `); }); diff --git a/packages/react/testing-library/src/fire-event.ts b/packages/react/testing-library/src/fire-event.ts index 52f784d89d..602c9e8e7d 100644 --- a/packages/react/testing-library/src/fire-event.ts +++ b/packages/react/testing-library/src/fire-event.ts @@ -1,14 +1,12 @@ // @ts-nocheck -import { createEvent, fireEvent as domFireEvent } from '@testing-library/dom'; +import { fireEvent as domFireEvent, createEvent } from '@testing-library/dom'; -const NodesRef = lynx.createSelectorQuery().selectUniqueID(-1).constructor; +let NodesRef = lynx.createSelectorQuery().selectUniqueID(-1).constructor; function getElement(elemOrNodesRef) { if (elemOrNodesRef instanceof NodesRef) { return __GetElementByUniqueId( Number(elemOrNodesRef._nodeSelectToken.identifier), ); - } else if ('refAttr' in elemOrNodesRef) { - return document.querySelector(`[react-ref-${elemOrNodesRef.refAttr[0]}-${elemOrNodesRef.refAttr[1]}]`); } else if (elemOrNodesRef?.constructor?.name === 'HTMLUnknownElement') { return elemOrNodesRef; } else { @@ -27,7 +25,7 @@ export const fireEvent: any = (elemOrNodesRef, ...args) => { const elem = getElement(elemOrNodesRef); - const ans = domFireEvent(elem, ...args); + let ans = domFireEvent(elem, ...args); if (isMainThread) { // switch back to main thread @@ -158,7 +156,7 @@ Object.keys(eventMap).forEach((key) => { lynxTestingEnv.switchToBackgroundThread(); const elem = getElement(elemOrNodesRef); - const eventType = init?.eventType || 'bindEvent'; + const eventType = init?.['eventType'] || 'bindEvent'; init = { eventType, eventName: key, diff --git a/packages/react/transform/src/swc_plugin_snapshot/mod.rs b/packages/react/transform/src/swc_plugin_snapshot/mod.rs index ab4523d62c..86f80ae751 100644 --- a/packages/react/transform/src/swc_plugin_snapshot/mod.rs +++ b/packages/react/transform/src/swc_plugin_snapshot/mod.rs @@ -226,7 +226,7 @@ impl DynamicPart { element_index: Expr = i32_to_expr(element_index), ), AttrName::Ref => quote!( - "(snapshot, index, oldValue) => $runtime_id.updateRef(snapshot, index, oldValue, $element_index)" as Expr, + "(snapshot, index, oldValue) => $runtime_id.updateRef(snapshot, index, oldValue, $element_index, '')" as Expr, runtime_id: Expr = runtime_id.clone(), element_index: Expr = i32_to_expr(element_index), ), @@ -1306,8 +1306,6 @@ where let mut snapshot_values: Vec> = vec![]; let mut snapshot_values_has_attr = false; - let mut snapshot_values_has_ref = false; - let mut snapshot_values_has_spread = false; let mut snapshot_attrs: Vec = vec![]; let mut snapshot_children: Vec = vec![]; let mut snapshot_dynamic_part_def: Vec> = vec![]; @@ -1396,16 +1394,11 @@ where value } } else if let AttrName::Ref = attr_name { - snapshot_values_has_ref = true; - if target == TransformTarget::LEPUS { - quote!("1" as Expr) - } else { - quote!( - "$runtime_id.transformRef($value)" as Expr, - runtime_id: Expr = runtime_id.clone(), - value: Expr = value, - ) - } + quote!( + "$runtime_id.transformRef($value)" as Expr, + runtime_id: Expr = runtime_id.clone(), + value: Expr = value, + ) } else { value }), @@ -1434,7 +1427,6 @@ where expr: Box::new(value), })); snapshot_values_has_attr = true; - snapshot_values_has_spread = true; // snapshot_attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr { // span: DUMMY_SP, // name, @@ -1592,21 +1584,6 @@ where name: JSXElementName::Ident(snapshot_id.clone()), span: node.span, attrs: { - if target != TransformTarget::LEPUS - && (snapshot_values_has_ref || snapshot_values_has_spread) - { - snapshot_attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr { - span: DUMMY_SP, - name: JSXAttrName::Ident(IdentName::new("ref".into(), DUMMY_SP)), - value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer { - span: DUMMY_SP, - expr: JSXExpr::Expr(Box::new(quote!( - r#"$runtime_id.applyRefs.bind([])"# as Expr, - runtime_id: Expr = self.runtime_id.clone(), - ))), - })), - })) - }; if snapshot_values_has_attr { snapshot_attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr { span: DUMMY_SP, diff --git a/packages/react/transform/tests/__swc_snapshots__/src/swc_plugin_snapshot/mod.rs/basic_ref.js b/packages/react/transform/tests/__swc_snapshots__/src/swc_plugin_snapshot/mod.rs/basic_ref.js index 4086075f3f..d96d9a7aba 100644 --- a/packages/react/transform/tests/__swc_snapshots__/src/swc_plugin_snapshot/mod.rs/basic_ref.js +++ b/packages/react/transform/tests/__swc_snapshots__/src/swc_plugin_snapshot/mod.rs/basic_ref.js @@ -12,12 +12,11 @@ const __snapshot_da39a_test_1 = require('@lynx-js/react/internal').createSnapsho el2 ]; }, [ - (snapshot, index, oldValue)=>require('@lynx-js/react/internal').updateRef(snapshot, index, oldValue, 1) + (snapshot, index, oldValue)=>require('@lynx-js/react/internal').updateRef(snapshot, index, oldValue, 1, '') ], null, undefined, globDynamicComponentEntry); function Comp() { const handleRef = ()=>{}; return _jsx(__snapshot_da39a_test_1, { - ref: require('@lynx-js/react/internal').applyRefs.bind([]), values: [ require('@lynx-js/react/internal').transformRef(handleRef) ] diff --git a/packages/rspeedy/core/CHANGELOG.md b/packages/rspeedy/core/CHANGELOG.md index 470550e096..8f8bfc98af 100644 --- a/packages/rspeedy/core/CHANGELOG.md +++ b/packages/rspeedy/core/CHANGELOG.md @@ -1,5 +1,37 @@ # @lynx-js/rspeedy +## 0.9.4 + +### Patch Changes + +- Bump Rsbuild v1.3.17 with Rspack v1.3.9. ([#708](https://github.com/lynx-family/lynx-stack/pull/708)) + +- Support `performance.profile`. ([#691](https://github.com/lynx-family/lynx-stack/pull/691)) + +- Support CLI flag `--mode` to specify the build mode. ([#723](https://github.com/lynx-family/lynx-stack/pull/723)) + +- Enable native Rsdoctor plugin by default. ([#688](https://github.com/lynx-family/lynx-stack/pull/688)) + + Set `tools.rsdoctor.experiments.enableNativePlugin` to `false` to use the old JS plugin. + + ```js + import { defineConfig } from '@lynx-js/rspeedy' + + export default defineConfig({ + tools: { + rsdoctor: { + experiments: { + enableNativePlugin: false, + }, + }, + }, + }) + ``` + + See [Rsdoctor - 1.0](https://rsdoctor.dev/blog/release/release-note-1_0#-faster-analysis) for more details. + +- Bump Rsbuild v1.3.14 with Rspack v1.3.8. ([#630](https://github.com/lynx-family/lynx-stack/pull/630)) + ## 0.9.3 ### Patch Changes diff --git a/packages/rspeedy/core/package.json b/packages/rspeedy/core/package.json index 089addb782..a50c839cb3 100644 --- a/packages/rspeedy/core/package.json +++ b/packages/rspeedy/core/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/rspeedy", - "version": "0.9.3", + "version": "0.9.4", "description": "A webpack/rspack-based frontend toolchain for Lynx", "keywords": [ "webpack", diff --git a/packages/rspeedy/create-rspeedy/CHANGELOG.md b/packages/rspeedy/create-rspeedy/CHANGELOG.md index f81cabf9e6..50ba0c4967 100644 --- a/packages/rspeedy/create-rspeedy/CHANGELOG.md +++ b/packages/rspeedy/create-rspeedy/CHANGELOG.md @@ -1,5 +1,7 @@ # create-rspeedy +## 0.9.4 + ## 0.9.3 ### Patch Changes diff --git a/packages/rspeedy/create-rspeedy/package.json b/packages/rspeedy/create-rspeedy/package.json index d8a3095a2e..63abac6733 100644 --- a/packages/rspeedy/create-rspeedy/package.json +++ b/packages/rspeedy/create-rspeedy/package.json @@ -1,6 +1,6 @@ { "name": "create-rspeedy", - "version": "0.9.3", + "version": "0.9.4", "description": "Create Rspeedy-powered ReactLynx apps with one command", "keywords": [ "webpack", diff --git a/packages/rspeedy/plugin-react-alias/CHANGELOG.md b/packages/rspeedy/plugin-react-alias/CHANGELOG.md index 2adda4da15..7f613300c7 100644 --- a/packages/rspeedy/plugin-react-alias/CHANGELOG.md +++ b/packages/rspeedy/plugin-react-alias/CHANGELOG.md @@ -1,5 +1,7 @@ # @lynx-js/react-alias-rsbuild-plugin +## 0.9.9 + ## 0.9.8 ### Patch Changes diff --git a/packages/rspeedy/plugin-react-alias/package.json b/packages/rspeedy/plugin-react-alias/package.json index a90b19e34d..b3efda1dba 100644 --- a/packages/rspeedy/plugin-react-alias/package.json +++ b/packages/rspeedy/plugin-react-alias/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/react-alias-rsbuild-plugin", - "version": "0.9.8", + "version": "0.9.9", "description": "A rsbuild plugin for making alias in ReactLynx", "keywords": [ "rsbuild", diff --git a/packages/rspeedy/plugin-react/CHANGELOG.md b/packages/rspeedy/plugin-react/CHANGELOG.md index fc05ca00b4..952df4e5d1 100644 --- a/packages/rspeedy/plugin-react/CHANGELOG.md +++ b/packages/rspeedy/plugin-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @lynx-js/react-rsbuild-plugin +## 0.9.9 + +### Patch Changes + +- Fix runtime error: "SyntaxError: Identifier 'i' has already been declared". ([#651](https://github.com/lynx-family/lynx-stack/pull/651)) + +- Enable runtime profiling when `performance.profile` is set to true. ([#722](https://github.com/lynx-family/lynx-stack/pull/722)) + +- fix: resolve page crash on development mode when enabling `experimental_isLazyBundle: true` ([#653](https://github.com/lynx-family/lynx-stack/pull/653)) + +- Support `@lynx-js/react` v0.108.0. ([#649](https://github.com/lynx-family/lynx-stack/pull/649)) + +- Updated dependencies [[`ea4da1a`](https://github.com/lynx-family/lynx-stack/commit/ea4da1af0ff14e2480e49f7004a3a2616594968d), [`ca15dda`](https://github.com/lynx-family/lynx-stack/commit/ca15dda4122c5eedc1fd82cefb0cd9af7fdaa47e), [`f8d369d`](https://github.com/lynx-family/lynx-stack/commit/f8d369ded802f8d7b9b859b1f150015d65773b0f), [`ea4da1a`](https://github.com/lynx-family/lynx-stack/commit/ea4da1af0ff14e2480e49f7004a3a2616594968d)]: + - @lynx-js/react-webpack-plugin@0.6.13 + - @lynx-js/runtime-wrapper-webpack-plugin@0.0.10 + - @lynx-js/react-alias-rsbuild-plugin@0.9.9 + - @lynx-js/react-refresh-webpack-plugin@0.3.2 + ## 0.9.8 ### Patch Changes diff --git a/packages/rspeedy/plugin-react/package.json b/packages/rspeedy/plugin-react/package.json index 9e5f9546b1..f57c17eb58 100644 --- a/packages/rspeedy/plugin-react/package.json +++ b/packages/rspeedy/plugin-react/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/react-rsbuild-plugin", - "version": "0.9.8", + "version": "0.9.9", "description": "A rsbuild plugin for ReactLynx", "keywords": [ "rsbuild", diff --git a/packages/rspeedy/upgrade-rspeedy/CHANGELOG.md b/packages/rspeedy/upgrade-rspeedy/CHANGELOG.md index e7f2ebf21b..3e186efa16 100644 --- a/packages/rspeedy/upgrade-rspeedy/CHANGELOG.md +++ b/packages/rspeedy/upgrade-rspeedy/CHANGELOG.md @@ -1,5 +1,7 @@ # upgrade-rspeedy +## 0.9.4 + ## 0.9.3 ## 0.9.2 diff --git a/packages/rspeedy/upgrade-rspeedy/package.json b/packages/rspeedy/upgrade-rspeedy/package.json index eb082d16df..567f1838d1 100644 --- a/packages/rspeedy/upgrade-rspeedy/package.json +++ b/packages/rspeedy/upgrade-rspeedy/package.json @@ -1,6 +1,6 @@ { "name": "upgrade-rspeedy", - "version": "0.9.3", + "version": "0.9.4", "description": "Upgrade Rspeedy-related packages", "keywords": [ "webpack", diff --git a/packages/testing-library/README.md b/packages/testing-library/README.md index a47064aca3..83450f763c 100644 --- a/packages/testing-library/README.md +++ b/packages/testing-library/README.md @@ -11,7 +11,7 @@ Unit testing library for lynx, same as https://github.com/testing-library. ## Documentation -Find the complete documentation for ReactLynx Testing Library on [lynxjs.org](https://lynxjs.org/react/react-lynx-testing-library.html). +Find the complete documentation for ReactLynx Testing Library on [lynxjs.org](https://lynxjs.org/react/reactlynx-testing-library.html). ## Credits diff --git a/packages/testing-library/testing-environment/CHANGELOG.md b/packages/testing-library/testing-environment/CHANGELOG.md index e9e81dac5a..75e09573fa 100644 --- a/packages/testing-library/testing-environment/CHANGELOG.md +++ b/packages/testing-library/testing-environment/CHANGELOG.md @@ -1,5 +1,15 @@ # @lynx-js/testing-environment +## 0.1.0 + +### Minor Changes + +- Switch to ESM package format by setting `"type": "module"`. ([#703](https://github.com/lynx-family/lynx-stack/pull/703)) + +### Patch Changes + +- rename @lynx-js/test-environment to @lynx-js/testing-environment ([#704](https://github.com/lynx-family/lynx-stack/pull/704)) + ## 0.0.1 ### Patch Changes diff --git a/packages/testing-library/testing-environment/README.md b/packages/testing-library/testing-environment/README.md index bfac35ad99..e8ca15ba5b 100644 --- a/packages/testing-library/testing-environment/README.md +++ b/packages/testing-library/testing-environment/README.md @@ -63,7 +63,7 @@ After configuration, you can directly access the `lynxTestingEnv` object globall If you want to use `@lynx-js/testing-environment` for unit testing in ReactLynx, you usually don't need to specify this configuration manually. -Please refer to [ReactLynx Testing Library](https://lynxjs.org/react/react-lynx-testing-library.html) to inherit the configuration from `@lynx-js/react/testing-library`. +Please refer to [ReactLynx Testing Library](https://lynxjs.org/react/reactlynx-testing-library.html) to inherit the configuration from `@lynx-js/react/testing-library`. ## Credits diff --git a/packages/testing-library/testing-environment/etc/testing-environment.api.md b/packages/testing-library/testing-environment/etc/testing-environment.api.md new file mode 100644 index 0000000000..8b5a0ee238 --- /dev/null +++ b/packages/testing-library/testing-environment/etc/testing-environment.api.md @@ -0,0 +1,111 @@ +## API Report File for "@lynx-js/testing-environment" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { JSDOM } from 'jsdom'; + +// @public +export type ElementTree = ReturnType; + +// @public +export type ElementTreeGlobals = PickUnderscoreKeys; + +// @public (undocumented) +export type FilterUnderscoreKeys = { + [K in keyof T]: K extends `__${string}` ? K : never; +}[keyof T]; + +// @public (undocumented) +export const initElementTree: () => { + root: LynxElement | undefined; + countElement(element: LynxElement, parentComponentUniqueId: number): void; + __CreatePage(_tag: string, parentComponentUniqueId: number): LynxElement; + __CreateRawText(text: string): LynxElement; + __GetElementUniqueID(e: LynxElement): number; + __SetClasses(e: LynxElement, cls: string): void; + __CreateElement(tag: string, parentComponentUniqueId: number): LynxElement; + __CreateView(parentComponentUniqueId: number): LynxElement; + __CreateScrollView(parentComponentUniqueId: number): LynxElement; + __FirstElement(e: LynxElement): LynxElement; + __CreateText(parentComponentUniqueId: number): LynxElement; + __CreateImage(parentComponentUniqueId: number): LynxElement; + __CreateWrapperElement(parentComponentUniqueId: number): LynxElement; + __AddInlineStyle(e: HTMLElement, key: number, value: string): void; + __AppendElement(parent: LynxElement, child: LynxElement): void; + __SetCSSId(e: LynxElement | LynxElement[], id: string, entryName?: string): void; + __SetAttribute(e: LynxElement, key: string, value: any): void; + __AddEvent(e: LynxElement, eventType: string, eventName: string, eventHandler: string | Record): void; + __GetEvent(e: LynxElement, eventType: string, eventName: string): { + type: string; + name: string; + jsFunction: any; + } | undefined; + __SetID(e: LynxElement, id: string): void; + __SetInlineStyles(e: LynxElement, styles: string | Record): void; + __AddDataset(e: LynxElement, key: string, value: string): void; + __SetDataset(e: LynxElement, dataset: any): void; + __SetGestureDetector(e: LynxElement, id: number, type: number, config: any, relationMap: Record): void; + __GetDataset(e: LynxElement): DOMStringMap; + __RemoveElement(parent: LynxElement, child: LynxElement): void; + __InsertElementBefore(parent: LynxElement, child: LynxElement, ref?: LynxElement): void; + __ReplaceElement(newElement: LynxElement, oldElement: LynxElement): void; + __FlushElementTree(): void; + __UpdateListComponents(_list: LynxElement, _components: string[]): void; + __UpdateListCallbacks(list: LynxElement, componentAtIndex: (list: LynxElement, listID: number, cellIndex: number, operationID: number, enable_reuse_notification: boolean) => void, enqueueComponent: (list: LynxElement, listID: number, sign: number) => void): void; + __CreateList(parentComponentUniqueId: number, componentAtIndex: any, enqueueComponent: any): LynxElement; + __GetTag(ele: LynxElement): string; + __GetAttributeByName(ele: LynxElement, name: string): string | null; + clear(): void; + toTree(): LynxElement | undefined; + enterListItemAtIndex(e: LynxElement, index: number, ...args: any[]): number; + leaveListItem(e: LynxElement, uiSign: number): void; + toJSON(): LynxElement | undefined; + __GetElementByUniqueId(uniqueId: number): LynxElement | undefined; +}; + +// @public +export interface LynxElement extends HTMLElement { + cssId?: string; + eventMap?: { + [key: string]: any; + }; + firstChild: LynxElement; + gesture?: { + [key: string]: any; + }; + nextSibling: LynxElement; + parentNode: LynxElement; +} + +// @public +export interface LynxGlobalThis { + // (undocumented) + [key: string]: any; + globalThis: LynxGlobalThis; +} + +// @public +export class LynxTestingEnv { + constructor(); + backgroundThread: LynxGlobalThis; + // (undocumented) + clearGlobal(): void; + // (undocumented) + injectGlobals(): void; + // (undocumented) + jsdom: JSDOM; + mainThread: LynxGlobalThis & ElementTreeGlobals; + // (undocumented) + reset(): void; + // (undocumented) + switchToBackgroundThread(): void; + // (undocumented) + switchToMainThread(): void; +} + +// @public (undocumented) +export type PickUnderscoreKeys = Pick>; + +``` diff --git a/packages/testing-library/testing-environment/package.json b/packages/testing-library/testing-environment/package.json index fbadd1de32..aa957b7a0d 100644 --- a/packages/testing-library/testing-environment/package.json +++ b/packages/testing-library/testing-environment/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/testing-environment", - "version": "0.0.1", + "version": "0.1.0", "description": "A subset of a Lynx environment to be useful for testing", "keywords": [ "Lynx", diff --git a/packages/third-party/tailwind-preset/CHANGELOG.md b/packages/third-party/tailwind-preset/CHANGELOG.md index d2bc560ef4..f94cfdef1c 100644 --- a/packages/third-party/tailwind-preset/CHANGELOG.md +++ b/packages/third-party/tailwind-preset/CHANGELOG.md @@ -1,5 +1,11 @@ # @lynx-js/tailwind-preset +## 0.0.3 + +### Patch Changes + +- Support `hidden`, `no-underline` and `line-through` utilities. ([#745](https://github.com/lynx-family/lynx-stack/pull/745)) + ## 0.0.2 ### Patch Changes diff --git a/packages/third-party/tailwind-preset/package.json b/packages/third-party/tailwind-preset/package.json index b56c7bc62d..d72d9f63cb 100644 --- a/packages/third-party/tailwind-preset/package.json +++ b/packages/third-party/tailwind-preset/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/tailwind-preset", - "version": "0.0.2", + "version": "0.0.3", "description": "A tailwindcss preset for ReactLynx", "keywords": [ "Lynx", diff --git a/packages/web-platform/offscreen-document/CHANGELOG.md b/packages/web-platform/offscreen-document/CHANGELOG.md index 1de4dc2503..a2ce95a646 100644 --- a/packages/web-platform/offscreen-document/CHANGELOG.md +++ b/packages/web-platform/offscreen-document/CHANGELOG.md @@ -1,5 +1,11 @@ # @lynx-js/offscreen-document +## 0.0.2 + +### Patch Changes + +- feat: support touch events ([#641](https://github.com/lynx-family/lynx-stack/pull/641)) + ## 0.0.1 ### Patch Changes diff --git a/packages/web-platform/offscreen-document/package.json b/packages/web-platform/offscreen-document/package.json index e897f662db..5b20b3af96 100644 --- a/packages/web-platform/offscreen-document/package.json +++ b/packages/web-platform/offscreen-document/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/offscreen-document", - "version": "0.0.1", + "version": "0.0.2", "private": false, "description": "Offscreen Document allows developers to use partical DOM in WebWorker", "keywords": [ diff --git a/packages/web-platform/web-constants/CHANGELOG.md b/packages/web-platform/web-constants/CHANGELOG.md index b43e534308..7665a272e6 100644 --- a/packages/web-platform/web-constants/CHANGELOG.md +++ b/packages/web-platform/web-constants/CHANGELOG.md @@ -1,5 +1,21 @@ # @lynx-js/web-constants +## 0.13.1 + +### Patch Changes + +- feat: support touch events for MTS ([#641](https://github.com/lynx-family/lynx-stack/pull/641)) + + now we support + + - main-thread:bindtouchstart + - main-thread:bindtouchend + - main-thread:bindtouchmove + - main-thread:bindtouchcancel + +- Updated dependencies []: + - @lynx-js/web-worker-rpc@0.13.1 + ## 0.13.0 ### Patch Changes diff --git a/packages/web-platform/web-constants/package.json b/packages/web-platform/web-constants/package.json index 30cb9a91f1..d0e005463c 100644 --- a/packages/web-platform/web-constants/package.json +++ b/packages/web-platform/web-constants/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/web-constants", - "version": "0.13.0", + "version": "0.13.1", "private": false, "description": "", "keywords": [], diff --git a/packages/web-platform/web-core/CHANGELOG.md b/packages/web-platform/web-core/CHANGELOG.md index 7c6a403773..eba4ae127a 100644 --- a/packages/web-platform/web-core/CHANGELOG.md +++ b/packages/web-platform/web-core/CHANGELOG.md @@ -1,5 +1,45 @@ # @lynx-js/web-core +## 0.13.1 + +### Patch Changes + +- fix: some inline style properties cause crash ([#647](https://github.com/lynx-family/lynx-stack/pull/647)) + + add support for the following css properties + + - mask + - mask-repeat + - mask-position + - mask-clip + - mask-origin + - mask-size + - gap + - column-gap + - row-gap + - image-rendering + - hyphens + - offset-path + - offset-distance + +- feat: support touch events for MTS ([#641](https://github.com/lynx-family/lynx-stack/pull/641)) + + now we support + + - main-thread:bindtouchstart + - main-thread:bindtouchend + - main-thread:bindtouchmove + - main-thread:bindtouchcancel + +- feat: add SystemInfo.screenWidth and SystemInfo.screenHeight ([#641](https://github.com/lynx-family/lynx-stack/pull/641)) + +- Updated dependencies [[`c9ccad6`](https://github.com/lynx-family/lynx-stack/commit/c9ccad6b574c98121149d3e9d4a9a7e97af63d91), [`9ad394e`](https://github.com/lynx-family/lynx-stack/commit/9ad394ea9ef28688a3b810b4051868b2a28eb7de), [`f4cfb70`](https://github.com/lynx-family/lynx-stack/commit/f4cfb70606d46cd4017254c326095432f9c6bcb8), [`c9ccad6`](https://github.com/lynx-family/lynx-stack/commit/c9ccad6b574c98121149d3e9d4a9a7e97af63d91), [`839d61c`](https://github.com/lynx-family/lynx-stack/commit/839d61c8a329ed1e265fe2edc12a702e9592f743)]: + - @lynx-js/offscreen-document@0.0.2 + - @lynx-js/web-mainthread-apis@0.13.1 + - @lynx-js/web-worker-runtime@0.13.1 + - @lynx-js/web-constants@0.13.1 + - @lynx-js/web-worker-rpc@0.13.1 + ## 0.13.0 ### Patch Changes diff --git a/packages/web-platform/web-core/package.json b/packages/web-platform/web-core/package.json index 34e94c9851..c86a2784a7 100644 --- a/packages/web-platform/web-core/package.json +++ b/packages/web-platform/web-core/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/web-core", - "version": "0.13.0", + "version": "0.13.1", "private": false, "description": "", "keywords": [], diff --git a/packages/web-platform/web-elements/CHANGELOG.md b/packages/web-platform/web-elements/CHANGELOG.md index 25806da931..fc25bf2a9c 100644 --- a/packages/web-platform/web-elements/CHANGELOG.md +++ b/packages/web-platform/web-elements/CHANGELOG.md @@ -1,5 +1,13 @@ # @lynx-js/web-elements +## 0.7.1 + +### Patch Changes + +- fix(web): x-swiper-item threshold updated to 20 ([#639](https://github.com/lynx-family/lynx-stack/pull/639)) + +- fix: In React19, setter and getter functions are treated as properties, making it impossible to retrieve the current value via attributes. ([#639](https://github.com/lynx-family/lynx-stack/pull/639)) + ## 0.7.0 ### Minor Changes diff --git a/packages/web-platform/web-elements/package.json b/packages/web-platform/web-elements/package.json index d6dd328b7f..e751476d00 100644 --- a/packages/web-platform/web-elements/package.json +++ b/packages/web-platform/web-elements/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/web-elements", - "version": "0.7.0", + "version": "0.7.1", "private": false, "repository": { "type": "git", diff --git a/packages/web-platform/web-explorer/CHANGELOG.md b/packages/web-platform/web-explorer/CHANGELOG.md index 8ecd26c2c4..e70de1e06d 100644 --- a/packages/web-platform/web-explorer/CHANGELOG.md +++ b/packages/web-platform/web-explorer/CHANGELOG.md @@ -1,5 +1,17 @@ # @lynx-js/web-explorer +## 0.0.7 + +### Patch Changes + +- feat: use nativeModulesPath instead of nativeModulesMap to lynx-view. ([#668](https://github.com/lynx-family/lynx-stack/pull/668)) + +- fix: fork @vant/touch-emulator and make it work with shadowroot ([#662](https://github.com/lynx-family/lynx-stack/pull/662)) + +- fix: loading errors caused by script import order ([#665](https://github.com/lynx-family/lynx-stack/pull/665)) + +- chore: update homepage ([#645](https://github.com/lynx-family/lynx-stack/pull/645)) + ## 0.0.6 ### Patch Changes diff --git a/packages/web-platform/web-explorer/package.json b/packages/web-platform/web-explorer/package.json index 893b92caa7..534c320da7 100644 --- a/packages/web-platform/web-explorer/package.json +++ b/packages/web-platform/web-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/web-explorer", - "version": "0.0.6", + "version": "0.0.7", "private": false, "repository": { "type": "git", @@ -20,8 +20,8 @@ ], "scripts": { "build": "rsbuild build", - "dev": "rsbuild dev", - "build:rsdoctor": "RSDOCTOR=true rsbuild build" + "build:rsdoctor": "RSDOCTOR=true rsbuild build", + "dev": "rsbuild dev" }, "dependencies": { "qr-scanner": "^1.4.2" diff --git a/packages/web-platform/web-mainthread-apis/CHANGELOG.md b/packages/web-platform/web-mainthread-apis/CHANGELOG.md index 3679a77636..d7427224dc 100644 --- a/packages/web-platform/web-mainthread-apis/CHANGELOG.md +++ b/packages/web-platform/web-mainthread-apis/CHANGELOG.md @@ -1,5 +1,39 @@ # @lynx-js/web-mainthread-apis +## 0.13.1 + +### Patch Changes + +- fix: some inline style properties cause crash ([#647](https://github.com/lynx-family/lynx-stack/pull/647)) + + add support for the following css properties + + - mask + - mask-repeat + - mask-position + - mask-clip + - mask-origin + - mask-size + - gap + - column-gap + - row-gap + - image-rendering + - hyphens + - offset-path + - offset-distance + +- feat: support touch events for MTS ([#641](https://github.com/lynx-family/lynx-stack/pull/641)) + + now we support + + - main-thread:bindtouchstart + - main-thread:bindtouchend + - main-thread:bindtouchmove + - main-thread:bindtouchcancel + +- Updated dependencies [[`c9ccad6`](https://github.com/lynx-family/lynx-stack/commit/c9ccad6b574c98121149d3e9d4a9a7e97af63d91)]: + - @lynx-js/web-constants@0.13.1 + ## 0.13.0 ### Minor Changes diff --git a/packages/web-platform/web-mainthread-apis/package.json b/packages/web-platform/web-mainthread-apis/package.json index 241906eb9e..60a5aa2b9a 100644 --- a/packages/web-platform/web-mainthread-apis/package.json +++ b/packages/web-platform/web-mainthread-apis/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/web-mainthread-apis", - "version": "0.13.0", + "version": "0.13.1", "private": false, "description": "", "keywords": [], diff --git a/packages/web-platform/web-rsbuild-plugin/CHANGELOG.md b/packages/web-platform/web-rsbuild-plugin/CHANGELOG.md new file mode 100644 index 0000000000..03c43737fc --- /dev/null +++ b/packages/web-platform/web-rsbuild-plugin/CHANGELOG.md @@ -0,0 +1,46 @@ +# @lynx-js/web-platform-rsbuild-plugin + +## 0.1.0 + +### Minor Changes + +- feat: add new parameter `nativeModulesPath` to `pluginWebPlatform({})`. ([#668](https://github.com/lynx-family/lynx-stack/pull/668)) + + After this commit, you can use `nativeModulesPath` to package custom nativeModules directly into the worker, and no longer need to pass `nativeModulesMap` to lynx-view. + + Here is an example: + + - `native-modules.ts`: + + ```ts + // index.native-modules.ts + export default { + CustomModule: function(NativeModules, NativeModulesCall) { + return { + async getColor(data, callback) { + const color = await NativeModulesCall('getColor', data); + callback(color); + }, + }; + }, + }; + ``` + + - plugin config: + + ```ts + // rsbuild.config.ts + import { pluginWebPlatform } from '@lynx-js/web-platform-rsbuild-plugin'; + import { defineConfig } from '@rsbuild/core'; + + export default defineConfig({ + plugins: [ + pluginWebPlatform({ + // replace with your actual native-modules file path + nativeModulesPath: path.resolve(__dirname, './index.native-modules.ts'), + }), + ], + }); + ``` + +- feat: Provides Rsbuild plugin for Web projects in Lynx Web Platform, currently supports polyfill about lynx. ([#606](https://github.com/lynx-family/lynx-stack/pull/606)) diff --git a/packages/web-platform/web-rsbuild-plugin/package.json b/packages/web-platform/web-rsbuild-plugin/package.json index 001f9dd74e..947071abdb 100644 --- a/packages/web-platform/web-rsbuild-plugin/package.json +++ b/packages/web-platform/web-rsbuild-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/web-platform-rsbuild-plugin", - "version": "0.0.0", + "version": "0.1.0", "private": false, "description": "A rsbuild plugin for Web projects in Lynx Web Platform", "keywords": [ diff --git a/packages/web-platform/web-worker-rpc/CHANGELOG.md b/packages/web-platform/web-worker-rpc/CHANGELOG.md index d069b690cc..67ea1664bb 100644 --- a/packages/web-platform/web-worker-rpc/CHANGELOG.md +++ b/packages/web-platform/web-worker-rpc/CHANGELOG.md @@ -1,5 +1,7 @@ # @lynx-js/web-worker-rpc +## 0.13.1 + ## 0.13.0 ## 0.12.0 diff --git a/packages/web-platform/web-worker-rpc/package.json b/packages/web-platform/web-worker-rpc/package.json index c91b3b5d50..165c7b1cea 100644 --- a/packages/web-platform/web-worker-rpc/package.json +++ b/packages/web-platform/web-worker-rpc/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/web-worker-rpc", - "version": "0.13.0", + "version": "0.13.1", "private": false, "description": "", "keywords": [], diff --git a/packages/web-platform/web-worker-runtime/CHANGELOG.md b/packages/web-platform/web-worker-runtime/CHANGELOG.md index 21322e1739..e758d2bcbc 100644 --- a/packages/web-platform/web-worker-runtime/CHANGELOG.md +++ b/packages/web-platform/web-worker-runtime/CHANGELOG.md @@ -1,5 +1,28 @@ # @lynx-js/web-worker-runtime +## 0.13.1 + +### Patch Changes + +- feat: support for using `lynx.queueMicrotask`. ([#702](https://github.com/lynx-family/lynx-stack/pull/702)) + +- feat: support touch events for MTS ([#641](https://github.com/lynx-family/lynx-stack/pull/641)) + + now we support + + - main-thread:bindtouchstart + - main-thread:bindtouchend + - main-thread:bindtouchmove + - main-thread:bindtouchcancel + +- feat: provide comments for `@lynx-js/web-platform-rsbuild-plugin`. ([#668](https://github.com/lynx-family/lynx-stack/pull/668)) + +- Updated dependencies [[`c9ccad6`](https://github.com/lynx-family/lynx-stack/commit/c9ccad6b574c98121149d3e9d4a9a7e97af63d91), [`9ad394e`](https://github.com/lynx-family/lynx-stack/commit/9ad394ea9ef28688a3b810b4051868b2a28eb7de), [`c9ccad6`](https://github.com/lynx-family/lynx-stack/commit/c9ccad6b574c98121149d3e9d4a9a7e97af63d91)]: + - @lynx-js/offscreen-document@0.0.2 + - @lynx-js/web-mainthread-apis@0.13.1 + - @lynx-js/web-constants@0.13.1 + - @lynx-js/web-worker-rpc@0.13.1 + ## 0.13.0 ### Patch Changes diff --git a/packages/web-platform/web-worker-runtime/package.json b/packages/web-platform/web-worker-runtime/package.json index b6634957f2..45b86fac90 100644 --- a/packages/web-platform/web-worker-runtime/package.json +++ b/packages/web-platform/web-worker-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/web-worker-runtime", - "version": "0.13.0", + "version": "0.13.1", "private": false, "description": "", "keywords": [], diff --git a/packages/webpack/react-webpack-plugin/CHANGELOG.md b/packages/webpack/react-webpack-plugin/CHANGELOG.md index 76ecabcf0e..d58c33d872 100644 --- a/packages/webpack/react-webpack-plugin/CHANGELOG.md +++ b/packages/webpack/react-webpack-plugin/CHANGELOG.md @@ -1,5 +1,15 @@ # @lynx-js/react-webpack-plugin +## 0.6.13 + +### Patch Changes + +- feat: add `experimental_isLazyBundle` option, it will disable snapshot HMR for standalone lazy bundle ([#653](https://github.com/lynx-family/lynx-stack/pull/653)) + +- Add the `profile` option to control whether `__PROFILE__` is enabled. ([#722](https://github.com/lynx-family/lynx-stack/pull/722)) + +- Support `@lynx-js/react` v0.108.0. ([#649](https://github.com/lynx-family/lynx-stack/pull/649)) + ## 0.6.12 ### Patch Changes diff --git a/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md b/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md index 4dbda5e090..f527a43608 100644 --- a/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md +++ b/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md @@ -51,6 +51,7 @@ export interface ReactWebpackPluginOptions { extractStr?: Partial | boolean; firstScreenSyncTiming?: 'immediately' | 'jsReady'; mainThreadChunks?: string[] | undefined; + profile?: boolean | undefined; } ``` diff --git a/packages/webpack/react-webpack-plugin/package.json b/packages/webpack/react-webpack-plugin/package.json index 24f5fc3df8..0d18005f87 100644 --- a/packages/webpack/react-webpack-plugin/package.json +++ b/packages/webpack/react-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/react-webpack-plugin", - "version": "0.6.12", + "version": "0.6.13", "description": "A webpack plugin for ReactLynx", "keywords": [ "webpack", diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/CHANGELOG.md b/packages/webpack/runtime-wrapper-webpack-plugin/CHANGELOG.md index 7970e26d69..9cb4b414b1 100644 --- a/packages/webpack/runtime-wrapper-webpack-plugin/CHANGELOG.md +++ b/packages/webpack/runtime-wrapper-webpack-plugin/CHANGELOG.md @@ -1,5 +1,11 @@ # @lynx-js/runtime-wrapper-webpack-plugin +## 0.0.10 + +### Patch Changes + +- feat: add `experimental_isLazyBundle` option, it will disable lynxChunkEntries for standalone lazy bundle ([#653](https://github.com/lynx-family/lynx-stack/pull/653)) + ## 0.0.9 ### Patch Changes diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/package.json b/packages/webpack/runtime-wrapper-webpack-plugin/package.json index c2a0746525..6773dd8e9b 100644 --- a/packages/webpack/runtime-wrapper-webpack-plugin/package.json +++ b/packages/webpack/runtime-wrapper-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@lynx-js/runtime-wrapper-webpack-plugin", - "version": "0.0.9", + "version": "0.0.10", "description": "Use runtime wrapper which allow JavaScript to be load by Lynx.", "keywords": [ "webpack",