diff --git a/.changeset/afraid-places-act.md b/.changeset/afraid-places-act.md new file mode 100644 index 0000000000..8872906118 --- /dev/null +++ b/.changeset/afraid-places-act.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/react": minor +--- + +refactor: create SnapshotInstance in renderToString directly diff --git a/packages/react/runtime/__test__/lifecycle/reload.test.jsx b/packages/react/runtime/__test__/lifecycle/reload.test.jsx index c9e5cfbb7c..22bb645e80 100644 --- a/packages/react/runtime/__test__/lifecycle/reload.test.jsx +++ b/packages/react/runtime/__test__/lifecycle/reload.test.jsx @@ -123,7 +123,7 @@ describe('reload', () => { expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1]).toMatchInlineSnapshot(` { - "data": "{"patchList":[{"id":3,"snapshotPatch":[3,-5,0,{"dataX2":"WorldX2"},3,-8,0,"update",3,-6,0,{"attr":{"dataX2":"WorldX2"}}]}]}", + "data": "{"patchList":[{"id":3,"snapshotPatch":[3,-5,0,{"dataX2":"WorldX2"},3,-7,0,"update",3,-8,0,{"attr":{"dataX2":"WorldX2"}}]}]}", "patchOptions": { "flowIds": [ 666, @@ -234,7 +234,7 @@ describe('reload', () => { expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1]).toMatchInlineSnapshot(` { - "data": "{"patchList":[{"id":4,"snapshotPatch":[3,-8,0,"???"]}]}", + "data": "{"patchList":[{"id":4,"snapshotPatch":[3,-7,0,"???"]}]}", "patchOptions": { "flowIds": [ 666, @@ -305,7 +305,7 @@ describe('reload', () => { [ "rLynxFirstScreen", { - "root": "{"id":-9,"type":"root","children":[{"id":-13,"type":"__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-10,"type":"__snapshot_a94a8_test_3","children":[{"id":-15,"type":null,"values":["Enjoy"]}]},{"id":-11,"type":"__snapshot_a94a8_test_4","children":[{"id":-16,"type":null,"values":["World"]}]},{"id":-12,"type":"wrapper","children":[{"id":-14,"type":"__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}", + "root": "{"id":-9,"type":"root","children":[{"id":-13,"type":"__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-10,"type":"__snapshot_a94a8_test_3","children":[{"id":-14,"type":null,"values":["Enjoy"]}]},{"id":-11,"type":"__snapshot_a94a8_test_4","children":[{"id":-15,"type":null,"values":["World"]}]},{"id":-12,"type":"wrapper","children":[{"id":-16,"type":"__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}", }, ], ], @@ -386,7 +386,7 @@ describe('reload', () => { expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1]).toMatchInlineSnapshot(` { - "data": "{"patchList":[{"id":8,"snapshotPatch":[3,-16,0,"update"]}]}", + "data": "{"patchList":[{"id":8,"snapshotPatch":[3,-15,0,"update"]}]}", "patchOptions": { "flowIds": [ 666, @@ -535,7 +535,7 @@ describe('reload', () => { expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1]).toMatchInlineSnapshot(` { - "data": "{"patchList":[{"id":11,"snapshotPatch":[3,-5,0,{"dataX2":"WorldX2"},3,-8,0,"update",3,-6,0,{"attr":{"dataX2":"WorldX2"}}]}]}", + "data": "{"patchList":[{"id":11,"snapshotPatch":[3,-5,0,{"dataX2":"WorldX2"},3,-7,0,"update",3,-8,0,{"attr":{"dataX2":"WorldX2"}}]}]}", "patchOptions": { "flowIds": [ 666, @@ -650,7 +650,7 @@ describe('reload', () => { expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1]).toMatchInlineSnapshot(` { - "data": "{"patchList":[{"id":12,"snapshotPatch":[3,-8,0,"???"]}]}", + "data": "{"patchList":[{"id":12,"snapshotPatch":[3,-7,0,"???"]}]}", "patchOptions": { "flowIds": [ 666, @@ -723,7 +723,7 @@ describe('reload', () => { [ "rLynxFirstScreen", { - "root": "{"id":-9,"type":"root","children":[{"id":-15,"type":"__snapshot_a94a8_test_5","children":[{"id":-13,"type":"__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-10,"type":"__snapshot_a94a8_test_3","children":[{"id":-16,"type":null,"values":["Enjoy"]}]},{"id":-11,"type":"__snapshot_a94a8_test_4","children":[{"id":-17,"type":null,"values":["World"]}]},{"id":-12,"type":"wrapper","children":[{"id":-14,"type":"__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}]}", + "root": "{"id":-9,"type":"root","children":[{"id":-10,"type":"__snapshot_a94a8_test_5","children":[{"id":-14,"type":"__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-11,"type":"__snapshot_a94a8_test_3","children":[{"id":-15,"type":null,"values":["Enjoy"]}]},{"id":-12,"type":"__snapshot_a94a8_test_4","children":[{"id":-16,"type":null,"values":["World"]}]},{"id":-13,"type":"wrapper","children":[{"id":-17,"type":"__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}]}", }, ], ], @@ -806,7 +806,7 @@ describe('reload', () => { expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1); expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1]).toMatchInlineSnapshot(` { - "data": "{"patchList":[{"id":16,"snapshotPatch":[3,-17,0,"update"]}]}", + "data": "{"patchList":[{"id":16,"snapshotPatch":[3,-16,0,"update"]}]}", "patchOptions": { "flowIds": [ 666, @@ -1314,7 +1314,7 @@ describe('firstScreenSyncTiming - jsReady', () => { "-8": -16, "-9": -17, }, - "root": "{"id":-17,"type":"root","children":[{"id":-21,"type":"__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-18,"type":"__snapshot_a94a8_test_3","children":[{"id":-23,"type":null,"values":["Hello 2"]}]},{"id":-19,"type":"__snapshot_a94a8_test_4","children":[{"id":-24,"type":null,"values":["World"]}]},{"id":-20,"type":"wrapper","children":[{"id":-22,"type":"__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}", + "root": "{"id":-17,"type":"root","children":[{"id":-21,"type":"__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-18,"type":"__snapshot_a94a8_test_3","children":[{"id":-22,"type":null,"values":["Hello 2"]}]},{"id":-19,"type":"__snapshot_a94a8_test_4","children":[{"id":-23,"type":null,"values":["World"]}]},{"id":-20,"type":"wrapper","children":[{"id":-24,"type":"__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}", }, ], ] @@ -1874,7 +1874,7 @@ describe('firstScreenSyncTiming - jsReady', () => { "rLynxFirstScreen", { "jsReadyEventIdSwap": {}, - "root": "{"id":-17,"type":"root","children":[{"id":-21,"type":"__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-18,"type":"__snapshot_a94a8_test_3","children":[{"id":-23,"type":null,"values":["Hello 2"]}]},{"id":-19,"type":"__snapshot_a94a8_test_4","children":[{"id":-24,"type":null,"values":["World"]}]},{"id":-20,"type":"wrapper","children":[{"id":-22,"type":"__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}", + "root": "{"id":-17,"type":"root","children":[{"id":-21,"type":"__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-18,"type":"__snapshot_a94a8_test_3","children":[{"id":-22,"type":null,"values":["Hello 2"]}]},{"id":-19,"type":"__snapshot_a94a8_test_4","children":[{"id":-23,"type":null,"values":["World"]}]},{"id":-20,"type":"wrapper","children":[{"id":-24,"type":"__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}", }, ], ] diff --git a/packages/react/runtime/__test__/lifecycle/updateData.test.jsx b/packages/react/runtime/__test__/lifecycle/updateData.test.jsx index 6e5f9cd022..d83897fd43 100644 --- a/packages/react/runtime/__test__/lifecycle/updateData.test.jsx +++ b/packages/react/runtime/__test__/lifecycle/updateData.test.jsx @@ -292,7 +292,7 @@ describe('triggerDataUpdated', () => { [ "rLynxChange", { - "data": "{"patchList":[{"id":7,"snapshotPatch":[3,-7,0,"update"]}]}", + "data": "{"patchList":[{"id":7,"snapshotPatch":[3,-6,0,"update"]}]}", "patchOptions": { "flowIds": [ 666, @@ -528,7 +528,7 @@ describe('triggerDataUpdated', () => { [ "rLynxChange", { - "data": "{"patchList":[{"id":15,"snapshotPatch":[3,-6,0,"update"]}]}", + "data": "{"patchList":[{"id":15,"snapshotPatch":[3,-5,0,"update"]}]}", "patchOptions": { "reloadVersion": 0, }, diff --git a/packages/react/runtime/__test__/lynx/suspense.test.jsx b/packages/react/runtime/__test__/lynx/suspense.test.jsx index 3d7d2d9619..0ec90aff46 100644 --- a/packages/react/runtime/__test__/lynx/suspense.test.jsx +++ b/packages/react/runtime/__test__/lynx/suspense.test.jsx @@ -384,9 +384,8 @@ describe('suspense', () => { expect([...snapshotInstanceManager.values.keys()]).toMatchInlineSnapshot(` [ -1, - -2, -3, - -4, + -5, ] `); } @@ -486,7 +485,6 @@ describe('suspense', () => { expect([...snapshotInstanceManager.values.keys()]).toMatchInlineSnapshot(` [ -1, - -2, ] `); @@ -1590,7 +1588,7 @@ describe('suspense', () => { .toMatchInlineSnapshot(` [ { - "childId": -4, + "childId": -2, "op": "RemoveChild", "parentId": -1, }, diff --git a/packages/react/runtime/__test__/renderToOpcodes.test.jsx b/packages/react/runtime/__test__/renderToOpcodes.test.jsx index 99702fe5d9..7de60a1ace 100644 --- a/packages/react/runtime/__test__/renderToOpcodes.test.jsx +++ b/packages/react/runtime/__test__/renderToOpcodes.test.jsx @@ -7,21 +7,33 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vite import { elementTree, waitSchedule } from './utils/nativeMethod'; import { globalEnvManager } from './utils/envManager'; import { setupDocument } from '../src/document'; -import { renderOpcodesInto } from '../src/opcodes'; -import renderToString from '../src/renderToOpcodes'; +// import { renderOpcodesInto } from '../src/opcodes'; +import renderToStringBase from '../src/renderToOpcodes'; import { setupPage, SnapshotInstance, snapshotInstanceManager } from '../src/snapshot'; import { createElement, cloneElement } from '../lepus'; import { Suspense } from 'preact/compat'; import { createSuspender } from './createSuspender'; import { __root } from '../src/root'; -describe('renderToOpcodes', () => { +const renderToString = (element, root = __root) => { + return renderToStringBase(element, null, root); +}; + +describe('renderToString', () => { beforeAll(() => { globalEnvManager.switchToMainThread(); }); + beforeEach(() => { + setupPage(__CreatePage('0', 0)); + }); + afterEach(() => { vi.clearAllMocks(); + + globalEnvManager.resetEnv(); + elementTree.clear(); + snapshotInstanceManager.clear(); }); it('should render hello world', () => { @@ -39,11 +51,23 @@ describe('renderToOpcodes', () => { [ 0, { - "children": undefined, + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -4, + "type": null, + "values": [ + "hello world", + ], + }, + ], "extraProps": undefined, "id": -3, "type": "__snapshot_a94a8_test_1", - "values": undefined, + "values": [ + "a", + ], }, 2, "values", @@ -51,7 +75,18 @@ describe('renderToOpcodes', () => { "a", ], 3, - "hello world", + [ + { + "children": undefined, + "extraProps": undefined, + "id": -4, + "type": null, + "values": [ + "hello world", + ], + }, + "hello world", + ], 1, ] `); @@ -77,22 +112,61 @@ describe('renderToOpcodes', () => { [ 0, { - "children": undefined, + "children": [ + { + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -5, + "type": null, + "values": [ + 1000, + ], + }, + ], + "extraProps": undefined, + "id": -4, + "type": "__snapshot_a94a8_test_2", + "values": undefined, + }, + ], "extraProps": undefined, - "id": -4, + "id": -2, "type": "__snapshot_a94a8_test_3", "values": undefined, }, 0, { - "children": undefined, + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -5, + "type": null, + "values": [ + 1000, + ], + }, + ], "extraProps": undefined, - "id": -6, + "id": -4, "type": "__snapshot_a94a8_test_2", "values": undefined, }, 3, - 1000, + [ + { + "children": undefined, + "extraProps": undefined, + "id": -5, + "type": null, + "values": [ + 1000, + ], + }, + 1000, + ], 1, 1, ] @@ -121,22 +195,61 @@ describe('renderToOpcodes', () => { [ 0, { - "children": undefined, + "children": [ + { + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -4, + "type": null, + "values": [ + 1, + ], + }, + ], + "extraProps": undefined, + "id": -3, + "type": "__snapshot_a94a8_test_4", + "values": undefined, + }, + ], "extraProps": undefined, - "id": -7, + "id": -2, "type": "__snapshot_a94a8_test_5", "values": undefined, }, 0, { - "children": undefined, + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -4, + "type": null, + "values": [ + 1, + ], + }, + ], "extraProps": undefined, - "id": -8, + "id": -3, "type": "__snapshot_a94a8_test_4", "values": undefined, }, 3, - 1, + [ + { + "children": undefined, + "extraProps": undefined, + "id": -4, + "type": null, + "values": [ + 1, + ], + }, + 1, + ], 1, 1, ] @@ -161,9 +274,12 @@ describe('renderToOpcodes', () => { { "children": undefined, "extraProps": undefined, - "id": -9, + "id": -2, "type": "__snapshot_a94a8_test_6", - "values": undefined, + "values": [ + ${random}, + "hello world", + ], }, 2, "values", @@ -190,14 +306,35 @@ describe('renderToOpcodes', () => { [ 0, { - "children": undefined, + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -3, + "type": null, + "values": [ + "111", + ], + }, + ], "extraProps": undefined, - "id": -10, + "id": -2, "type": "__snapshot_a94a8_test_7", "values": undefined, }, 3, - "111", + [ + { + "children": undefined, + "extraProps": undefined, + "id": -3, + "type": null, + "values": [ + "111", + ], + }, + "111", + ], 1, ] `); @@ -219,14 +356,35 @@ describe('renderToOpcodes', () => { [ 0, { - "children": undefined, + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -3, + "type": null, + "values": [ + "111", + ], + }, + ], "extraProps": undefined, - "id": -11, + "id": -2, "type": "__snapshot_a94a8_test_8", "values": undefined, }, 3, - "111", + [ + { + "children": undefined, + "extraProps": undefined, + "id": -3, + "type": null, + "values": [ + "111", + ], + }, + "111", + ], 1, ] `); @@ -246,14 +404,35 @@ describe('renderToOpcodes', () => { [ 0, { - "children": undefined, + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -3, + "type": null, + "values": [ + "111", + ], + }, + ], "extraProps": undefined, - "id": -12, + "id": -2, "type": "__snapshot_a94a8_test_9", "values": undefined, }, 3, - "111", + [ + { + "children": undefined, + "extraProps": undefined, + "id": -3, + "type": null, + "values": [ + "111", + ], + }, + "111", + ], 1, ] `); @@ -272,22 +451,61 @@ describe('renderToOpcodes', () => { [ 0, { - "children": undefined, + "children": [ + { + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -4, + "type": null, + "values": [ + 11111, + ], + }, + ], + "extraProps": undefined, + "id": -3, + "type": "__snapshot_a94a8_test_11", + "values": undefined, + }, + ], "extraProps": undefined, - "id": -13, + "id": -2, "type": "__snapshot_a94a8_test_10", "values": undefined, }, 0, { - "children": undefined, + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -4, + "type": null, + "values": [ + 11111, + ], + }, + ], "extraProps": undefined, - "id": -14, + "id": -3, "type": "__snapshot_a94a8_test_11", "values": undefined, }, 3, - 11111, + [ + { + "children": undefined, + "extraProps": undefined, + "id": -4, + "type": null, + "values": [ + 11111, + ], + }, + 11111, + ], 1, 1, ] @@ -306,22 +524,61 @@ describe('renderToOpcodes', () => { [ 0, { - "children": undefined, + "children": [ + { + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -7, + "type": null, + "values": [ + 12345, + ], + }, + ], + "extraProps": undefined, + "id": -6, + "type": "__snapshot_a94a8_test_13", + "values": undefined, + }, + ], "extraProps": undefined, - "id": -15, + "id": -5, "type": "__snapshot_a94a8_test_12", "values": undefined, }, 0, { - "children": undefined, + "children": [ + { + "children": undefined, + "extraProps": undefined, + "id": -7, + "type": null, + "values": [ + 12345, + ], + }, + ], "extraProps": undefined, - "id": -16, + "id": -6, "type": "__snapshot_a94a8_test_13", "values": undefined, }, 3, - 12345, + [ + { + "children": undefined, + "extraProps": undefined, + "id": -7, + "type": null, + "values": [ + 12345, + ], + }, + 12345, + ], 1, 1, ] @@ -618,13 +875,14 @@ describe('renderOpcodesInto', () => { it('should render hello world', () => { scratch.ensureElements(); - const opcodes = renderToString( + renderToString( Hello World , + scratch, ); - renderOpcodesInto(opcodes, scratch); + // renderOpcodesInto(opcodes, scratch); expect(scratch.__element_root).toMatchInlineSnapshot(` { it('should render attr', () => { scratch.ensureElements(); - const opcodes = renderToString( + renderToString( Hello World , + scratch, ); - renderOpcodesInto(opcodes, scratch); + // renderOpcodesInto(opcodes, scratch); expect(scratch.__element_root).toMatchInlineSnapshot(` { it('should render string', () => { scratch.ensureElements(); - const opcodes = renderToString( + renderToString( Hello World {'aaaa'.toUpperCase()} , + scratch, ); - renderOpcodesInto(opcodes, scratch); + // renderOpcodesInto(opcodes, scratch); expect(scratch.__element_root).toMatchInlineSnapshot(` { it('should render with multi-children', () => { scratch.ensureElements(); - const opcodes = renderToString( + renderToString( Hello World {[A, B, C]} , + scratch, ); - renderOpcodesInto(opcodes, scratch); + // renderOpcodesInto(opcodes, scratch); expect(scratch.__element_root).toMatchInlineSnapshot(` { ); - const opcodes = renderToString( + renderToString( Hello World {[reuse, reuse]} , + scratch, ); - renderOpcodesInto(opcodes, scratch); + // renderOpcodesInto(opcodes, scratch); expect(scratch.__element_root).toMatchInlineSnapshot(` { return ; } - const opcodes = renderToString(); - renderOpcodesInto(opcodes, scratch); + renderToString(, scratch); + // renderOpcodesInto(opcodes, scratch); expect(scratch.__element_root).toMatchInlineSnapshot(` { Counter.defaultProps = { count: 1 }; - const opcodes = renderToString(); - renderOpcodesInto(opcodes, scratch); + renderToString(, scratch); + // renderOpcodesInto(opcodes, scratch); expect(scratch.__element_root).toMatchInlineSnapshot(` { it('should render empty array', () => { scratch.ensureElements(); - renderOpcodesInto([], scratch); + renderToString([], scratch); expect(scratch.__element_root).toMatchInlineSnapshot(` { return children; } - const opcodes = renderToString( + renderToString( Hello World {[ @@ -891,9 +1153,10 @@ describe('renderOpcodesInto', () => { D{[D1, D2, D3, D4]}, ]} , + scratch, ); - renderOpcodesInto(opcodes, scratch); + // renderOpcodesInto(opcodes, scratch); scratch.__firstChild.removeChild(scratch.__firstChild.__firstChild); scratch.__firstChild.removeChild(scratch.__firstChild.__lastChild); @@ -976,7 +1239,7 @@ describe('createElement', () => { { "children": undefined, "extraProps": undefined, - "id": -89, + "id": -41, "type": "__snapshot_a94a8_test_74", "values": undefined, }, @@ -990,7 +1253,7 @@ describe('createElement', () => { { "children": undefined, "extraProps": undefined, - "id": -90, + "id": -42, "type": "__snapshot_a94a8_test_74", "values": undefined, }, @@ -1012,7 +1275,7 @@ describe('createElement', () => { { "children": undefined, "extraProps": undefined, - "id": -91, + "id": -43, "type": "__snapshot_a94a8_test_75", "values": undefined, }, @@ -1026,7 +1289,7 @@ describe('createElement', () => { { "children": undefined, "extraProps": undefined, - "id": -92, + "id": -44, "type": "__snapshot_a94a8_test_75", "values": undefined, }, diff --git a/packages/react/runtime/__test__/snapshot/workletRef.test.jsx b/packages/react/runtime/__test__/snapshot/workletRef.test.jsx index c40847d0a2..8fe5e3b0e6 100644 --- a/packages/react/runtime/__test__/snapshot/workletRef.test.jsx +++ b/packages/react/runtime/__test__/snapshot/workletRef.test.jsx @@ -591,13 +591,18 @@ describe('WorkletRef', () => { }); it('invalid ref type', async function() { + const reportError = lynx.reportError; + lynx.reportError = vi.fn(); // main thread render { __root.__jsx = createCompMT1('number'); - expect(() => renderPage()).toThrowError( - 'MainThreadRef: main-thread:ref must be of type MainThreadRef or main-thread function.', - ); + renderPage(); } + expect(lynx.reportError).toHaveBeenCalledTimes(1); + expect(lynx.reportError).toHaveBeenCalledWith( + new Error('MainThreadRef: main-thread:ref must be of type MainThreadRef or main-thread function.'), + ); + lynx.reportError = reportError; }); }); diff --git a/packages/react/runtime/src/lifecycle/render.ts b/packages/react/runtime/src/lifecycle/render.ts index 153ea55d1d..1382cf1752 100644 --- a/packages/react/runtime/src/lifecycle/render.ts +++ b/packages/react/runtime/src/lifecycle/render.ts @@ -6,10 +6,7 @@ * Implements the IFR (Instant First-Frame Rendering) on main thread. */ -import { isValidElement } from 'preact'; - import { profileEnd, profileStart } from '../debug/profile.js'; -import { renderOpcodesInto } from '../opcodes.js'; import { render as renderToString } from '../renderToOpcodes/index.js'; import { __root } from '../root.js'; import { SnapshotInstance } from '../snapshot/snapshot.js'; @@ -20,31 +17,20 @@ function renderMainThread(): void { if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) { profileStart('ReactLynx::renderMainThread'); } - opcodes = renderToString(__root.__jsx, undefined); + opcodes = renderToString(__root.__jsx, undefined, __root as SnapshotInstance); } catch (e) { lynx.reportError(e as Error); opcodes = []; + (__root as SnapshotInstance).removeChildren(); } finally { if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) { profileEnd(); } } - if (process.env['NODE_ENV'] === 'test') { - opcodes = opcodes.map((opcode) => { - if (isValidElement(opcode) && typeof opcode.type === 'string') { - return Object.assign(new SnapshotInstance(opcode.type), opcode, { $$typeof: undefined }); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return opcode; - }); - } - if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) { profileStart('ReactLynx::renderOpcodes'); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - renderOpcodesInto(opcodes, __root as any); if (__ENABLE_SSR__) { __root.__opcodes = opcodes; } diff --git a/packages/react/runtime/src/opcodes.ts b/packages/react/runtime/src/opcodes.ts index 352488e1f9..881bc6548a 100644 --- a/packages/react/runtime/src/opcodes.ts +++ b/packages/react/runtime/src/opcodes.ts @@ -95,59 +95,59 @@ export function ssrHydrateByOpcodes( } } -export function renderOpcodesInto(opcodes: any[], into: SnapshotInstance): void { - let top: SnapshotInstance = into; - const stack: SnapshotInstance[] = [into]; - for (let i = 0; i < opcodes.length;) { - const opcode = opcodes[i]; - switch (opcode) { - case OpcodeBegin: { - const p = top; - top = opcodes[i + 1]; - // @ts-ignore - if (top.__parent) { - // already inserted - top = new SnapshotInstance(top.type); - opcodes[i + 1] = top; - } - p.insertBefore(top); - stack.push(top); +// export function renderOpcodesInto(opcodes: any[], into: SnapshotInstance): void { +// let top: SnapshotInstance = into; +// const stack: SnapshotInstance[] = [into]; +// for (let i = 0; i < opcodes.length;) { +// const opcode = opcodes[i]; +// switch (opcode) { +// case OpcodeBegin: { +// const p = top; +// top = opcodes[i + 1]; +// // @ts-ignore +// if (top.__parent) { +// // already inserted +// top = new SnapshotInstance(top.type); +// opcodes[i + 1] = top; +// } +// p.insertBefore(top); +// stack.push(top); - i += 2; - break; - } - case OpcodeEnd: { - // @ts-ignore - top[CHILDREN] = undefined; +// i += 2; +// break; +// } +// case OpcodeEnd: { +// // @ts-ignore +// top[CHILDREN] = undefined; - stack.pop(); - const p = stack[stack.length - 1]; - top = p!; +// stack.pop(); +// const p = stack[stack.length - 1]; +// top = p!; - i += 1; - break; - } - case OpcodeAttr: { - const key = opcodes[i + 1]; - const value = opcodes[i + 2]; - top.setAttribute(key, value); +// i += 1; +// break; +// } +// case OpcodeAttr: { +// const key = opcodes[i + 1]; +// const value = opcodes[i + 2]; +// top.setAttribute(key, value); - i += 3; - break; - } - case OpcodeText: { - const text = opcodes[i + 1]; - const s = new SnapshotInstance(null as unknown as string); - if (__ENABLE_SSR__) { - // We need store the just created SnapshotInstance, or it will be lost when we leave the function - opcodes[i + 1] = [s, text]; - } - s.setAttribute(0, text); - top.insertBefore(s); +// i += 3; +// break; +// } +// case OpcodeText: { +// const text = opcodes[i + 1]; +// const s = new SnapshotInstance(null as unknown as string); +// if (__ENABLE_SSR__) { +// // We need store the just created SnapshotInstance, or it will be lost when we leave the function +// opcodes[i + 1] = [s, text]; +// } +// s.setAttribute(0, text); +// top.insertBefore(s); - i += 2; - break; - } - } - } -} +// i += 2; +// break; +// } +// } +// } +// } diff --git a/packages/react/runtime/src/renderToOpcodes/index.ts b/packages/react/runtime/src/renderToOpcodes/index.ts index 7de5d6589a..475d595fd7 100644 --- a/packages/react/runtime/src/renderToOpcodes/index.ts +++ b/packages/react/runtime/src/renderToOpcodes/index.ts @@ -10,7 +10,8 @@ // @ts-nocheck -import { Fragment, h, options } from 'preact'; +import { Fragment, h, options, isValidElement } from 'preact'; +import { SnapshotInstance } from '../snapshot/snapshot.js'; import { CHILDREN, @@ -41,7 +42,7 @@ let beforeDiff, beforeDiff2, afterDiff, renderHook, ummountHook; * @param {VNode} vnode JSX Element / VNode to render * @param {object} [context] Initial root context object */ -export function renderToString(vnode: any, context: any): any[] { +export function renderToString(vnode: any, context: any, into: SnapshotInstance): any[] { // Performance optimization: `renderToString` is synchronous and we // therefore don't execute any effects. To do that we pass an empty // array to `options._commit` (`__c`). But we can go one step further @@ -71,6 +72,7 @@ export function renderToString(vnode: any, context: any): any[] { parent, opcodes, 0, + into, ); } finally { // options._commit, we don't schedule any effects in this library right now, @@ -144,7 +146,9 @@ function renderClassComponent(vnode, context) { * @param {boolean} isSvgMode * @param {any} selectValue * @param {VNode} parent - * @param opcodes + * @param {any[]} opcodes + * @param {number} opcodesLength + * @param {SnapshotInstance} into */ function _renderToString( vnode, @@ -154,6 +158,7 @@ function _renderToString( parent, opcodes, opcodesLength, + into, ) { // Ignore non-rendered VNodes/values if (vnode == null || vnode === true || vnode === false || vnode === '') { @@ -163,8 +168,7 @@ function _renderToString( // Text VNodes: escape as HTML if (typeof vnode !== 'object') { if (typeof vnode === 'function') return; - - opcodes.push(__OpText, vnode + ''); + renderToTextNode(into, vnode + '', opcodes); return; } @@ -175,7 +179,18 @@ function _renderToString( const child = vnode[i]; if (child == null || typeof child === 'boolean') continue; - _renderToString(child, context, isSvgMode, selectValue, parent, opcodes, opcodes.length); + _renderToString( + child, + context, + isSvgMode, + selectValue, + parent, + opcodes, + /* v8 ignore start */ + __ENABLE_SSR__ ? opcodes.length : 0, + /* v8 ignore end */ + into, + ); } return; } @@ -251,10 +266,28 @@ function _renderToString( && rendered.key == null; rendered = isTopLevelFragment ? rendered.props.children : rendered; + let lastChild = into.__lastChild; // Recurse into children before invoking the after-diff hook try { - _renderToString(rendered, context, isSvgMode, selectValue, vnode, opcodes, opcodes.length); + _renderToString( + rendered, + context, + isSvgMode, + selectValue, + vnode, + opcodes, + /* v8 ignore start */ + __ENABLE_SSR__ ? opcodes.length : 0, + /* v8 ignore end */ + into, + ); } catch (e) { + // clear existing children + into.removeChildren( + lastChild + ? lastChild.__nextSibling + : into.__firstChild, + ); if (e && typeof e === 'object' && e.then && component && /* _childDidSuspend */ component.__c) { component.setState({ /* _suspended */ __a: true }); @@ -262,8 +295,21 @@ function _renderToString( rendered = renderClassComponent(vnode, context); component = vnode[COMPONENT]; - opcodes.length = opcodesLength; - _renderToString(rendered, context, isSvgMode, selectValue, vnode, opcodes, opcodes.length); + if (__ENABLE_SSR__) { + opcodes.length = opcodesLength; + } + _renderToString( + rendered, + context, + isSvgMode, + selectValue, + vnode, + opcodes, + /* v8 ignore start */ + __ENABLE_SSR__ ? opcodes.length : 0, + /* v8 ignore end */ + into, + ); } } else { throw e; @@ -280,7 +326,18 @@ function _renderToString( let children; - opcodes.push(__OpBegin, vnode); + // hack for runtime test + if (process.env['NODE_ENV'] === 'test' && isValidElement(vnode) && typeof vnode.type === 'string') { + vnode = Object.assign(new SnapshotInstance(type), vnode, { $$typeof: undefined }); + } + // already inserted + if (vnode.__parent) { + vnode = new SnapshotInstance(type); + } + if (__ENABLE_SSR__) { + opcodes.push(__OpBegin, vnode); + } + into.insertBefore(vnode); for (const name in props) { const v = props[name]; @@ -303,23 +360,40 @@ function _renderToString( // write this attribute to the buffer if (v != null && v !== false && typeof v !== 'function') { - opcodes.push(__OpAttr, name, v); + if (__ENABLE_SSR__) { + opcodes.push(__OpAttr, name, v); + } + vnode.setAttribute(name, v); } } if (typeof children === 'string' || typeof children === 'number') { // single text child - opcodes.push(__OpText, children); + renderToTextNode(vnode, children, opcodes); } else if (children != null && children !== false && children !== true) { // recurse into this element VNode's children - _renderToString(children, context, false, selectValue, vnode, opcodes, opcodes.length); + _renderToString( + children, + context, + false, + selectValue, + vnode, + opcodes, + /* v8 ignore start */ + __ENABLE_SSR__ ? opcodes.length : 0, + /* v8 ignore end */ + vnode, + ); } if (afterDiff) afterDiff(vnode); vnode[PARENT] = undefined; if (ummountHook) ummountHook(vnode); - opcodes.push(__OpEnd); + if (__ENABLE_SSR__) { + opcodes.push(__OpEnd); + } + vnode[CHILDREN] = undefined; return; } @@ -329,6 +403,17 @@ function doRender(props, state, context) { return this.constructor(props, context); } +function renderToTextNode(into: SnapshotInstance, text: string | number, opcodes: Opcode[]) { + const textNode = new SnapshotInstance(null); + textNode.setAttribute(0, text); + into.insertBefore(textNode); + if (__ENABLE_SSR__) { + // We need store the just created SnapshotInstance, or it will be lost when we leave the function + text = [textNode, text]; + opcodes.push(__OpText, text); + } +} + export default renderToString; export const render: typeof renderToString = renderToString; export const renderToStaticMarkup: typeof renderToString = renderToString; diff --git a/packages/react/runtime/src/snapshot/snapshot.ts b/packages/react/runtime/src/snapshot/snapshot.ts index 62ea06d52a..1464690583 100644 --- a/packages/react/runtime/src/snapshot/snapshot.ts +++ b/packages/react/runtime/src/snapshot/snapshot.ts @@ -431,6 +431,16 @@ export class SnapshotInstance { }); } + // remove all children from start or this.__firstChild + removeChildren(start: SnapshotInstance | null = this.__firstChild): void { + let nodeToRemove = start; + while (nodeToRemove) { + const next = nodeToRemove.__nextSibling; + this.removeChild(nodeToRemove); + nodeToRemove = next; + } + } + setAttribute(key: string | number, value: any): void { if (key === 'values') { const oldValues = this.__values; diff --git a/packages/react/testing-library/src/__tests__/alog.test.jsx b/packages/react/testing-library/src/__tests__/alog.test.jsx index 00a34ac41f..2ebadd6119 100644 --- a/packages/react/testing-library/src/__tests__/alog.test.jsx +++ b/packages/react/testing-library/src/__tests__/alog.test.jsx @@ -60,15 +60,6 @@ describe('alog', () => { [ "[ReactLynxDebug] FiberElement API call #3: __SetCSSId([PAGE#0], 0)", ], - [ - "[MainThread Component Render] name: ClassComponent", - ], - [ - "[MainThread Component Render] name: FunctionComponent", - ], - [ - "[MainThread Component Render] name: App", - ], [ "[ReactLynxDebug] FiberElement API call #4: __CreateView(0) => VIEW#1", ], @@ -141,6 +132,9 @@ describe('alog', () => { [ "[ReactLynxDebug] FiberElement API call #27: __AppendElement(WRAPPER#8, VIEW#9)", ], + [ + "[MainThread Component Render] name: ClassComponent", + ], [ "[ReactLynxDebug] FiberElement API call #28: __CreateView(0) => VIEW#11", ], @@ -154,7 +148,13 @@ describe('alog', () => { "[ReactLynxDebug] FiberElement API call #31: __AppendElement(WRAPPER#8, VIEW#11)", ], [ - "[ReactLynxDebug] FiberElement API call #32: __OnLifecycleEvent(["rLynxFirstScreen", {"root":"{\\"id\\":-1,\\"type\\":\\"root\\",\\"children\\":[{\\"id\\":-2,\\"type\\":\\"__snapshot_426db_test_1\\",\\"values\\":[\\"-2:0:\\",\\"-2:1:\\"],\\"children\\":[{\\"id\\":-3,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-7,\\"type\\":null,\\"values\\":[0]}]},{\\"id\\":-4,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-5,\\"type\\":\\"__snapshot_426db_test_2\\"},{\\"id\\":-6,\\"type\\":\\"__snapshot_426db_test_3\\"}]}]}]}","jsReadyEventIdSwap":{}}])", + "[MainThread Component Render] name: FunctionComponent", + ], + [ + "[MainThread Component Render] name: App", + ], + [ + "[ReactLynxDebug] FiberElement API call #32: __OnLifecycleEvent(["rLynxFirstScreen", {"root":"{\\"id\\":-1,\\"type\\":\\"root\\",\\"children\\":[{\\"id\\":-2,\\"type\\":\\"__snapshot_426db_test_1\\",\\"values\\":[\\"-2:0:\\",\\"-2:1:\\"],\\"children\\":[{\\"id\\":-3,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-4,\\"type\\":null,\\"values\\":[0]}]},{\\"id\\":-5,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-6,\\"type\\":\\"__snapshot_426db_test_2\\"},{\\"id\\":-7,\\"type\\":\\"__snapshot_426db_test_3\\"}]}]}]}","jsReadyEventIdSwap":{}}])", ], [ "[ReactLynxDebug] BTS -> MTS updateMainThread: @@ -193,7 +193,7 @@ describe('alog', () => { "snapshotPatch": [ { "op": "SetAttribute", - "id": -7, + "id": -4, "dynamicPartIndex": 0, "value": 1 } @@ -255,7 +255,7 @@ describe('alog', () => { "type": "wrapper", "children": [ { - "id": -7, + "id": -4, "type": null, "values": [ 0 @@ -264,15 +264,15 @@ describe('alog', () => { ] }, { - "id": -4, + "id": -5, "type": "wrapper", "children": [ { - "id": -5, + "id": -6, "type": "__snapshot_426db_test_2" }, { - "id": -6, + "id": -7, "type": "__snapshot_426db_test_3" } ] @@ -289,10 +289,10 @@ describe('alog', () => { | -1(root): undefined | -2(__snapshot_426db_test_1): ["-2:0:","-2:1:"] | -3(wrapper): undefined - | -7(null): [0] - | -4(wrapper): undefined - | -5(__snapshot_426db_test_2): undefined - | -6(__snapshot_426db_test_3): undefined", + | -4(null): [0] + | -5(wrapper): undefined + | -6(__snapshot_426db_test_2): undefined + | -7(__snapshot_426db_test_3): undefined", ], [ "[ReactLynxDebug] BackgroundSnapshotInstance tree before hydration: @@ -309,10 +309,10 @@ describe('alog', () => { | -1(root): undefined | -2(__snapshot_426db_test_1): [null,null] | -3(wrapper): undefined - | -7(null): [0] - | -4(wrapper): undefined - | -5(__snapshot_426db_test_2): undefined - | -6(__snapshot_426db_test_3): undefined", + | -4(null): [0] + | -5(wrapper): undefined + | -6(__snapshot_426db_test_2): undefined + | -7(__snapshot_426db_test_3): undefined", ], [ "[ReactLynxDebug] BTS received event: @@ -333,10 +333,10 @@ describe('alog', () => { }", ], [ - "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_426db_test_2, __id: -5", + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_426db_test_2, __id: -6", ], [ - "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_426db_test_3, __id: -6", + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_426db_test_3, __id: -7", ], [ "[BackgroundThread Component Render] name: App, uniqID: __snapshot_426db_test_1, __id: -2", @@ -363,7 +363,7 @@ describe('alog', () => { "snapshotPatch": [ { "op": "SetAttribute", - "id": -7, + "id": -4, "dynamicPartIndex": 0, "value": 0 } @@ -394,10 +394,10 @@ describe('alog', () => { expect(lynxTestingEnv.backgroundThread.console.alog.mock.calls).toMatchInlineSnapshot(` [ [ - "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_426db_test_2, __id: -5", + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_426db_test_2, __id: -6", ], [ - "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_426db_test_3, __id: -6", + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_426db_test_3, __id: -7", ], [ "[BackgroundThread Component Render] name: App, uniqID: __snapshot_426db_test_1, __id: -2", @@ -424,7 +424,7 @@ describe('alog', () => { "snapshotPatch": [ { "op": "SetAttribute", - "id": -7, + "id": -4, "dynamicPartIndex": 0, "value": 1 } @@ -455,10 +455,10 @@ describe('alog', () => { expect(lynxTestingEnv.backgroundThread.console.alog.mock.calls).toMatchInlineSnapshot(` [ [ - "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_426db_test_2, __id: -5", + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_426db_test_2, __id: -6", ], [ - "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_426db_test_3, __id: -6", + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_426db_test_3, __id: -7", ], [ "[BackgroundThread Component Render] name: App, uniqID: __snapshot_426db_test_1, __id: -2", diff --git a/packages/react/testing-library/src/__tests__/list.test.jsx b/packages/react/testing-library/src/__tests__/list.test.jsx index 1bdfef64f8..775a16cab7 100644 --- a/packages/react/testing-library/src/__tests__/list.test.jsx +++ b/packages/react/testing-library/src/__tests__/list.test.jsx @@ -890,7 +890,7 @@ describe('list - deferred should render as normal', () => { "rLynxFirstScreen", { "jsReadyEventIdSwap": {}, - "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__snapshot_a9e46_test_30","children":[{"id":-3,"type":"__snapshot_a9e46_test_31","values":[{"item-key":0}],"children":[{"id":-4,"type":"__snapshot_a9e46_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-5,"type":"__snapshot_a9e46_test_32","children":[{"id":-12,"type":null,"values":[0]}]}]}]},{"id":-6,"type":"__snapshot_a9e46_test_31","values":[{"item-key":1}],"children":[{"id":-7,"type":"__snapshot_a9e46_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-8,"type":"__snapshot_a9e46_test_32","children":[{"id":-13,"type":null,"values":[1]}]}]}]},{"id":-9,"type":"__snapshot_a9e46_test_31","values":[{"item-key":2}],"children":[{"id":-10,"type":"__snapshot_a9e46_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-11,"type":"__snapshot_a9e46_test_32","children":[{"id":-14,"type":null,"values":[2]}]}]}]}]}]}", + "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__snapshot_a9e46_test_30","children":[{"id":-3,"type":"__snapshot_a9e46_test_31","values":[{"item-key":0}],"children":[{"id":-4,"type":"__snapshot_a9e46_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-5,"type":"__snapshot_a9e46_test_32","children":[{"id":-6,"type":null,"values":[0]}]}]}]},{"id":-7,"type":"__snapshot_a9e46_test_31","values":[{"item-key":1}],"children":[{"id":-8,"type":"__snapshot_a9e46_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-9,"type":"__snapshot_a9e46_test_32","children":[{"id":-10,"type":null,"values":[1]}]}]}]},{"id":-11,"type":"__snapshot_a9e46_test_31","values":[{"item-key":2}],"children":[{"id":-12,"type":"__snapshot_a9e46_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-13,"type":"__snapshot_a9e46_test_32","children":[{"id":-14,"type":null,"values":[2]}]}]}]}]}]}", }, ], ],