diff --git a/.changeset/itchy-candies-smoke.md b/.changeset/itchy-candies-smoke.md new file mode 100644 index 0000000000..26a34d11a7 --- /dev/null +++ b/.changeset/itchy-candies-smoke.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/react": patch +--- + +Fix the issue that lazy bundle HMR will lost CSS. diff --git a/examples/react-lazy-bundle/src/App.css b/examples/react-lazy-bundle/src/App.css index fc28b87906..2362b071a3 100644 --- a/examples/react-lazy-bundle/src/App.css +++ b/examples/react-lazy-bundle/src/App.css @@ -32,3 +32,7 @@ text { color: var(--color-text); } + +.Suspense { + margin-top: 21px; +} diff --git a/examples/react-lazy-bundle/src/App.tsx b/examples/react-lazy-bundle/src/App.tsx index 732b018af6..de023703f5 100644 --- a/examples/react-lazy-bundle/src/App.tsx +++ b/examples/react-lazy-bundle/src/App.tsx @@ -1,7 +1,9 @@ -import { useEffect } from '@lynx-js/react'; +import { Suspense, lazy, useEffect } from '@lynx-js/react'; import './App.css'; +const LazyComponent = lazy(() => import('./LazyComponent.js')); + export function App() { useEffect(() => { console.info('Hello, ReactLynx'); @@ -24,6 +26,11 @@ export function App() { React on Lynx + + Loading...}> + + + ); diff --git a/examples/react-lazy-bundle/src/LazyComponent.css b/examples/react-lazy-bundle/src/LazyComponent.css new file mode 100644 index 0000000000..a7aafb7083 --- /dev/null +++ b/examples/react-lazy-bundle/src/LazyComponent.css @@ -0,0 +1,4 @@ +.LazyComponent { + font-weight: 700; + color: yellow; +} diff --git a/examples/react-lazy-bundle/src/LazyComponent.tsx b/examples/react-lazy-bundle/src/LazyComponent.tsx new file mode 100644 index 0000000000..bfc3e63537 --- /dev/null +++ b/examples/react-lazy-bundle/src/LazyComponent.tsx @@ -0,0 +1,9 @@ +import './LazyComponent.css'; + +export default function LazyComponent() { + return ( + + LazyComponent + + ); +} diff --git a/packages/react/runtime/__test__/snapshotPatch.test.jsx b/packages/react/runtime/__test__/snapshotPatch.test.jsx index 898369a32a..0c0e570eeb 100644 --- a/packages/react/runtime/__test__/snapshotPatch.test.jsx +++ b/packages/react/runtime/__test__/snapshotPatch.test.jsx @@ -811,6 +811,118 @@ describe('DEV_ONLY_addSnapshot', () => { expect(snapshot).toHaveProperty('slot', null); }); + it('with non-standalone lazy bundle', () => { + const uniqID1 = 'basic-1'; + // We have to use `snapshotCreatorMap[uniqID1] =` so that it can be created after `initGlobalSnapshotPatch` + snapshotCreatorMap[uniqID1] = (uniqID1) => { + globalThis.createSnapshot( + uniqID1, + // The `create` function is stringified and called by `new Function()` + /* v8 ignore start */ + () => { + const pageId = 0; + const el = __CreateView(pageId); + const el1 = __CreateText(pageId); + __AppendElement(el, el1); + const el2 = __CreateRawText('Hello, ReactLynx x Fast Refresh'); + __AppendElement(el1, el2); + return [ + el, + el1, + el2, + ]; + }, + /* v8 ignore stop */ + null, + null, + undefined, + globDynamicComponentEntry, + null, + true, + ); + }; + const patch = takeGlobalSnapshotPatch(); + + expect(patch).toMatchInlineSnapshot(` + [ + 100, + "basic-1", + "(uniqID12) => { + globalThis.createSnapshot( + uniqID12, + // The \`create\` function is stringified and called by \`new Function()\` + /* v8 ignore start */ + () => { + const pageId = 0; + const el = __CreateView(pageId); + const el1 = __CreateText(pageId); + __AppendElement(el, el1); + const el2 = __CreateRawText("Hello, ReactLynx x Fast Refresh"); + __AppendElement(el1, el2); + return [ + el, + el1, + el2 + ]; + }, + /* v8 ignore stop */ + null, + null, + void 0, + globDynamicComponentEntry, + null, + true + ); + }", + ] + `); + + const oldDynamicComponentEntry = global.globDynamicComponentEntry; + global.globDynamicComponentEntry = 'https://example.com/lazy-bundle.js'; + new SnapshotInstance(uniqID1); + const originalSize = snapshotManager.values.size; + + { + const patch1 = takeGlobalSnapshotPatch(); + expect(patch1).toMatchInlineSnapshot(` + [ + 102, + "basic-1", + "https://example.com/lazy-bundle.js", + ] + `); + global.globDynamicComponentEntry = oldDynamicComponentEntry; + patch.push(...patch1); + } + + // Remove the old definition + snapshotManager.values.delete(uniqID1); + delete snapshotCreatorMap[uniqID1]; + + const fn = vi.fn(); + vi.stubGlobal('__SetCSSId', fn); + // Apply patches in main thread + snapshotPatchApply(patch); + new SnapshotInstance(uniqID1); + + expect(snapshotManager.values.size).toBe(originalSize); + expect(snapshotManager.values.has(uniqID1)).toBeTruthy(); + const snapshot = snapshotManager.values.get(uniqID1); + expect(snapshot).toHaveProperty('create', expect.any(Function)); + const si = new SnapshotInstance(uniqID1); + expect(si.ensureElements()); + expect(si.__element_root).not.toBeUndefined(); + expect(snapshot).toHaveProperty('update', null); + expect(snapshot).toHaveProperty('slot', null); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn.mock.calls[0].slice(1)).toMatchInlineSnapshot(` + [ + 0, + "https://example.com/lazy-bundle.js", + ] + `); + }); + it('with update', () => { const uniqID1 = 'with-update-0'; // We have to use `snapshotCreatorMap[uniqID1] =` so that it can be created after `initGlobalSnapshotPatch` diff --git a/packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts b/packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts index c45448fb18..00ac03e2b8 100644 --- a/packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts +++ b/packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts @@ -17,6 +17,7 @@ export const SnapshotOperation = { DEV_ONLY_AddSnapshot: 100, DEV_ONLY_RegisterWorklet: 101, + DEV_ONLY_SetSnapshotEntryName: 102, } as const; export const SnapshotOperationParams: Record = /* @__PURE__ */ { @@ -42,6 +43,10 @@ export const SnapshotOperationParams: Record string>(snapshotCreator); } break; + } + case SnapshotOperation.DEV_ONLY_SetSnapshotEntryName: { + if (__DEV__) { + const uniqID = snapshotPatch[++i] as string; + const entryName = snapshotPatch[++i] as string; + + // HMR-related + // Update the evaluated snapshot entryName from JS. + snapshotCreatorMap[uniqID] = evaluate<(uniqId: string) => string>( + snapshotCreatorMap[uniqID]!.toString().replaceAll('globDynamicComponentEntry', JSON.stringify(entryName)), + ); + } + break; } // case SnapshotOperation.DEV_ONLY_RegisterWorklet: { // // HMR-related diff --git a/packages/react/runtime/src/snapshot.ts b/packages/react/runtime/src/snapshot.ts index 52092c16f4..f12b4d7c38 100644 --- a/packages/react/runtime/src/snapshot.ts +++ b/packages/react/runtime/src/snapshot.ts @@ -236,6 +236,15 @@ export function createSnapshot( if (!isLazySnapshotSupported) { uniqID = entryUniqID(uniqID, entryName); } + // For Lazy Bundle, their entryName is not DEFAULT_ENTRY_NAME. + // We need to set the entryName correctly for HMR + if (__DEV__ && __JS__ && __globalSnapshotPatch && entryName && entryName !== DEFAULT_ENTRY_NAME) { + __globalSnapshotPatch.push( + SnapshotOperation.DEV_ONLY_SetSnapshotEntryName, + uniqID, + entryName, + ); + } const s: Snapshot = { create, update, slot, cssId, entryName, refAndSpreadIndexes }; snapshotManager.values.set(uniqID, s);