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);