Skip to content

Commit 87de745

Browse files
committed
fix(modules): scope SSR markup by module
1 parent 982b557 commit 87de745

3 files changed

Lines changed: 76 additions & 6 deletions

File tree

packages/arui-scripts-modules/src/module-loader/utils/get-embedded-module-resources.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export const MODULE_DATA_HREF_ATTRIBUTE = 'data-href';
3232
* снять с неё снапшот при гидрации.
3333
*/
3434
export const MODULE_SSR_ROOT_ATTRIBUTE = 'data-module-ssr-root';
35+
/**
36+
* Атрибут SSR-обёртки, различающий разные модули с одинаковым `instanceId`.
37+
* Вместе с `data-module-ssr-root` образует уникальный ключ серверной разметки.
38+
*/
39+
export const MODULE_SSR_MODULE_ATTRIBUTE = 'data-module-ssr-module';
3540
/**
3641
* Атрибут outlet-элемента (внутри обёртки), в который смонтирован/гидрирован модуль.
3742
* Значение — `instanceId`.

packages/arui-scripts-modules/src/ssr/__tests__/create-ssr-mounter.tests.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,52 @@ describe('createSsrMounter', () => {
329329
]);
330330
});
331331

332+
it('does not use another module SSR markup when instance ids match', async () => {
333+
const firstModuleId = 'FirstModule';
334+
const secondModuleId = 'SecondModule';
335+
const firstModule = { hydrate: jest.fn(), mount: jest.fn(), unmount: jest.fn() };
336+
const secondModule = { hydrate: jest.fn(), mount: jest.fn(), unmount: jest.fn() };
337+
338+
(window as unknown as Record<string, unknown>)[firstModuleId] = firstModule;
339+
(window as unknown as Record<string, unknown>)[secondModuleId] = secondModule;
340+
341+
const { ModuleComponent: FirstModuleComponent } = createSsrMounter<RunParams>({
342+
moduleId: firstModuleId,
343+
hostAppId: 'host',
344+
getModuleResources: jest
345+
.fn()
346+
.mockResolvedValue(buildResources({ appName: firstModuleId })),
347+
});
348+
const { ModuleComponent: SecondModuleComponent } = createSsrMounter<RunParams>({
349+
moduleId: secondModuleId,
350+
hostAppId: 'host',
351+
getModuleResources: jest.fn().mockResolvedValue(
352+
buildResources({
353+
appName: secondModuleId,
354+
html: undefined,
355+
}),
356+
),
357+
});
358+
const element = (
359+
<Suspense fallback={<span>loading</span>}>
360+
<FirstModuleComponent instanceId='shared' />
361+
<SecondModuleComponent instanceId='shared' />
362+
</Suspense>
363+
);
364+
365+
container.innerHTML = await renderServerHtml(element);
366+
367+
await act(async () => {
368+
hydrateRoot(container, element);
369+
});
370+
371+
await waitFor(() => {
372+
expect(secondModule.mount).toHaveBeenCalledTimes(1);
373+
});
374+
375+
expect(secondModule.hydrate).not.toHaveBeenCalled();
376+
});
377+
332378
it('falls back to mount (clearing the outlet) when the module has no hydrate', async () => {
333379
jest.spyOn(console, 'warn').mockImplementation(() => {});
334380
const getModuleResources = jest.fn().mockResolvedValue(buildResources());

packages/arui-scripts-modules/src/ssr/create-ssr-mounter.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
MODULE_DATA_HREF_ATTRIBUTE,
1313
MODULE_SSR_HREF_ATTRIBUTE,
1414
MODULE_SSR_INSTANCE_ATTRIBUTE,
15+
MODULE_SSR_MODULE_ATTRIBUTE,
1516
MODULE_SSR_MOUNT_ID_ATTRIBUTE,
1617
MODULE_SSR_PAYLOAD_ATTRIBUTE,
1718
MODULE_SSR_ROOT_ATTRIBUTE,
@@ -209,7 +210,12 @@ export function createSsrMounter<
209210

210211
/* eslint-disable react/no-danger -- мы получаем много разметки не из реакта при работе с модулями */
211212
return (
212-
<div {...{ [MODULE_SSR_ROOT_ATTRIBUTE]: instanceId }}>
213+
<div
214+
{...{
215+
[MODULE_SSR_ROOT_ATTRIBUTE]: instanceId,
216+
[MODULE_SSR_MODULE_ATTRIBUTE]: moduleId,
217+
}}
218+
>
213219
{stylesMode === 'inline'
214220
? inlineStyles.map((style) => (
215221
<style
@@ -275,7 +281,7 @@ export function createSsrMounter<
275281
const hadServerHtmlRef = useRef(false);
276282

277283
if (snapshotRef.current === null) {
278-
const { snapshot, hadServerHtml } = readServerMarkup(instanceId);
284+
const { snapshot, hadServerHtml } = readServerMarkup(moduleId, instanceId);
279285

280286
snapshotRef.current = snapshot;
281287
hadServerHtmlRef.current = hadServerHtml;
@@ -378,7 +384,10 @@ export function createSsrMounter<
378384
return (
379385
<div
380386
ref={rootRef}
381-
{...{ [MODULE_SSR_ROOT_ATTRIBUTE]: instanceId }}
387+
{...{
388+
[MODULE_SSR_ROOT_ATTRIBUTE]: instanceId,
389+
[MODULE_SSR_MODULE_ATTRIBUTE]: moduleId,
390+
}}
382391
dangerouslySetInnerHTML={{ __html: snapshotRef.current }}
383392
suppressHydrationWarning={true}
384393
/>
@@ -388,7 +397,13 @@ export function createSsrMounter<
388397
// Нет серверной разметки (например, клиентский SPA-переход) — рендерим пустой outlet
389398
// для обычного монтирования.
390399
return (
391-
<div ref={rootRef} {...{ [MODULE_SSR_ROOT_ATTRIBUTE]: instanceId }}>
400+
<div
401+
ref={rootRef}
402+
{...{
403+
[MODULE_SSR_ROOT_ATTRIBUTE]: instanceId,
404+
[MODULE_SSR_MODULE_ATTRIBUTE]: moduleId,
405+
}}
406+
>
392407
<div {...{ [MODULE_SSR_MOUNT_ID_ATTRIBUTE]: instanceId }} />
393408
</div>
394409
);
@@ -415,13 +430,17 @@ function safeStringify(value: unknown): string {
415430
}
416431
}
417432

418-
function readServerMarkup(instanceId: string): { snapshot: string; hadServerHtml: boolean } {
433+
function readServerMarkup(
434+
moduleId: string,
435+
instanceId: string,
436+
): { snapshot: string; hadServerHtml: boolean } {
419437
if (typeof document === 'undefined') {
420438
return { snapshot: '', hadServerHtml: false };
421439
}
422440

423441
const root = document.querySelector(
424-
`[${MODULE_SSR_ROOT_ATTRIBUTE}="${cssEscape(instanceId)}"]`,
442+
`[${MODULE_SSR_ROOT_ATTRIBUTE}="${cssEscape(instanceId)}"]` +
443+
`[${MODULE_SSR_MODULE_ATTRIBUTE}="${cssEscape(moduleId)}"]`,
425444
);
426445

427446
if (!root) {

0 commit comments

Comments
 (0)