@quark-fw/plugin-react 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/ssr/createPreloadTagWriter.ts +36 -0
- package/src/ssr/createRendererLoader.ts +29 -0
- package/src/ssr/createRequestTrpcClient.ts +23 -0
- package/src/ssr/createStreamingStore.ts +47 -0
- package/src/ssr/getRenderMiddleware.ts +21 -154
- package/src/ssr/helpers.ts +26 -0
- package/src/ssr/resolveClientAssets.ts +31 -0
- package/src/utils/vite.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quark-fw/plugin-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"types": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@quark-fw/atom": "0.1.
|
|
26
|
-
"@quark-fw/plugin": "0.1.
|
|
25
|
+
"@quark-fw/atom": "0.1.10",
|
|
26
|
+
"@quark-fw/plugin": "0.1.10",
|
|
27
27
|
"@trpc/client": "^11.0.0",
|
|
28
28
|
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"express": "^5.2.1",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"@mantine/dates": "^8.3.18",
|
|
37
37
|
"@mantine/modals": "^8.3.18",
|
|
38
38
|
"@mantine/notifications": "^8.3.18",
|
|
39
|
-
"@quark-fw/core": "0.1.
|
|
40
|
-
"@quark-fw/store": "0.1.
|
|
39
|
+
"@quark-fw/core": "0.1.10",
|
|
40
|
+
"@quark-fw/store": "0.1.10",
|
|
41
41
|
"@vitejs/plugin-react": "^5.1.0",
|
|
42
42
|
"react": "^19.2.4",
|
|
43
43
|
"react-dom": "^19.2.4",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Manifest } from "vite";
|
|
2
|
+
import type { Response } from "express";
|
|
3
|
+
import { UglyModule } from "../Router/UglyModule.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Пишет в ответ preload-теги (modulepreload + css) для модулей страницы,
|
|
7
|
+
* которые регистрирует роутер по мере рендера. Каждый css — один раз.
|
|
8
|
+
*/
|
|
9
|
+
export function createPreloadTagWriter(manifest: Manifest, res: Response) {
|
|
10
|
+
const requiredModules: UglyModule[] = [];
|
|
11
|
+
const written: Record<string, boolean> = {};
|
|
12
|
+
|
|
13
|
+
const extractTags = (link: string): void => {
|
|
14
|
+
const chunk = manifest[link];
|
|
15
|
+
if (chunk && chunk.name !== "entry-client") {
|
|
16
|
+
res.write(
|
|
17
|
+
`<link rel="modulepreload" as="script" crossorigin href="/${chunk.file}">`,
|
|
18
|
+
);
|
|
19
|
+
(chunk.css ?? []).forEach((cssFile) => {
|
|
20
|
+
if (!written[cssFile]) {
|
|
21
|
+
written[cssFile] = true;
|
|
22
|
+
res.write(`<link rel="stylesheet" href="/${cssFile}"/>`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
(chunk.imports ?? []).map((childLink) => extractTags(childLink));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return function registerModule(uglyModule: UglyModule) {
|
|
30
|
+
if (!requiredModules.includes(uglyModule)) {
|
|
31
|
+
requiredModules.push(uglyModule);
|
|
32
|
+
|
|
33
|
+
extractTags(uglyModule.modulePath.substring(1));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ViteDevServer } from "vite";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { buildPaths } from "../utils/viteConfig.js";
|
|
5
|
+
import { isProd } from "./helpers.js";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
type RendererModule = typeof import("../frontend/entry-server.js");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Загрузчик SSR-бандла: в prod импортируется один раз из сборки,
|
|
13
|
+
* в dev — через vite (с HMR).
|
|
14
|
+
*/
|
|
15
|
+
export function createRendererLoader(vite?: ViteDevServer) {
|
|
16
|
+
let prodRenderer: Promise<RendererModule>;
|
|
17
|
+
|
|
18
|
+
return function loadRenderer(): Promise<RendererModule> {
|
|
19
|
+
if (isProd) {
|
|
20
|
+
prodRenderer ??= import(
|
|
21
|
+
pathToFileURL(buildPaths().serverEntry).href
|
|
22
|
+
) as Promise<RendererModule>;
|
|
23
|
+
return prodRenderer;
|
|
24
|
+
}
|
|
25
|
+
return vite!.ssrLoadModule(
|
|
26
|
+
path.join(__dirname, "../frontend/entry-server.js"),
|
|
27
|
+
) as Promise<RendererModule>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createTRPCClient, httpLink } from "@trpc/client";
|
|
2
|
+
import { jsonSafeFetch, type TRPCClient } from "@quark-fw/store";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* tRPC-клиент КОНКРЕТНОГО запроса: SSR ходит к собственному серверу по
|
|
6
|
+
* HTTP и пробрасывает cookie, чтобы страница рендерилась от имени
|
|
7
|
+
* залогиненного пользователя. Клиент собирается здесь, а роутер объявляет
|
|
8
|
+
* приложение — тот же шов аугментации, что и в сторе.
|
|
9
|
+
*/
|
|
10
|
+
export function createRequestTrpcClient(
|
|
11
|
+
port: number,
|
|
12
|
+
cookie: string | undefined,
|
|
13
|
+
): TRPCClient {
|
|
14
|
+
return createTRPCClient({
|
|
15
|
+
links: [
|
|
16
|
+
httpLink({
|
|
17
|
+
url: `http://127.0.0.1:${port}/api/trpc`,
|
|
18
|
+
headers: cookie ? { cookie } : {},
|
|
19
|
+
fetch: jsonSafeFetch,
|
|
20
|
+
}),
|
|
21
|
+
],
|
|
22
|
+
}) as TRPCClient;
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Response } from "express";
|
|
2
|
+
import Store, { NOT_READY_STATE, type TRPCClient } from "@quark-fw/store";
|
|
3
|
+
import serializeForScript from "../utils/serializeForScript.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Store, стримящий резолв данных в ответ по мере рендера: клиентский Store
|
|
7
|
+
* гидратируется из window.cache без повторных запросов, NOT_READY_STATE
|
|
8
|
+
* будит зависшие Suspense-чтения.
|
|
9
|
+
*
|
|
10
|
+
* Мета страницы резолвится ДО записи <head>, а её tRPC-вызовы дёргают
|
|
11
|
+
* callback store — писать их сразу нельзя: заголовки ушли бы раньше
|
|
12
|
+
* res.status(), а скрипты — раньше <!doctype>. Поэтому записи кэша ждут
|
|
13
|
+
* releaseCacheWrites(), который вызывается после записи shell.
|
|
14
|
+
*/
|
|
15
|
+
export function createStreamingStore(trpcClient: TRPCClient, res: Response) {
|
|
16
|
+
let htmlReadyResolve!: () => void;
|
|
17
|
+
const htmlReady = new Promise<void>((resolve) => {
|
|
18
|
+
htmlReadyResolve = resolve;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const store = new Store({
|
|
22
|
+
trpcClient,
|
|
23
|
+
callback: (key, value) => {
|
|
24
|
+
const safeKey = serializeForScript(key);
|
|
25
|
+
void htmlReady.then(() => {
|
|
26
|
+
if (value === NOT_READY_STATE) {
|
|
27
|
+
res.write(
|
|
28
|
+
`<script>window.cache=window.cache||{};window.cache[${safeKey}]=${serializeForScript(NOT_READY_STATE)};</script>`,
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
// undefined сериализуем как null: literal undefined в
|
|
32
|
+
// window.cache читается клиентом как cache miss и
|
|
33
|
+
// приводит к повторному suspense при гидратации
|
|
34
|
+
res.write(
|
|
35
|
+
`<script>window.cache[${safeKey}]=${
|
|
36
|
+
typeof value === "undefined"
|
|
37
|
+
? "null"
|
|
38
|
+
: serializeForScript(value)
|
|
39
|
+
};window.store?.emit(${safeKey},window.cache[${safeKey}]);</script>`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return { store, releaseCacheWrites: htmlReadyResolve };
|
|
47
|
+
}
|
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ViteDevServer } from "vite";
|
|
2
2
|
import { Request, Response } from "express";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import * as fs from "node:fs";
|
|
5
|
-
import { createTRPCClient, httpLink } from "@trpc/client";
|
|
6
|
-
import Store, {
|
|
7
|
-
jsonSafeFetch,
|
|
8
|
-
NOT_READY_STATE,
|
|
9
|
-
type TRPCClient,
|
|
10
|
-
} from "@quark-fw/store";
|
|
11
|
-
import { UglyModule } from "../Router/gatherPages.js";
|
|
12
|
-
import removeSearchParamsFromUrl from "../utils/removeSearchParamsFromUrl.js";
|
|
13
|
-
import serializeForScript from "../utils/serializeForScript.js";
|
|
14
3
|
import { matchPath } from "react-router-dom";
|
|
4
|
+
import { UglyModule } from "../Router/UglyModule.js";
|
|
5
|
+
import removeSearchParamsFromUrl from "../utils/removeSearchParamsFromUrl.js";
|
|
15
6
|
import { resolveMeta, type PageMeta } from "../seo/createPageMeta.js";
|
|
16
7
|
import {
|
|
17
8
|
buildLocalePath,
|
|
@@ -19,63 +10,21 @@ import {
|
|
|
19
10
|
parseLocalePath,
|
|
20
11
|
type I18nConfig,
|
|
21
12
|
} from "../i18n/config.js";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.map((part) => part.trim().split("="))
|
|
29
|
-
.filter((pair) => pair.length === 2)
|
|
30
|
-
.map(([k, v]) => [k!, decodeURIComponent(v!)]),
|
|
31
|
-
);
|
|
32
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
33
|
-
import { buildPaths } from "../utils/viteConfig.js";
|
|
13
|
+
import { getReactAtoms, parseCookies } from "./helpers.js";
|
|
14
|
+
import { resolveClientAssets } from "./resolveClientAssets.js";
|
|
15
|
+
import { createRendererLoader } from "./createRendererLoader.js";
|
|
16
|
+
import { createPreloadTagWriter } from "./createPreloadTagWriter.js";
|
|
17
|
+
import { createRequestTrpcClient } from "./createRequestTrpcClient.js";
|
|
18
|
+
import { createStreamingStore } from "./createStreamingStore.js";
|
|
34
19
|
|
|
35
20
|
export type GetRenderMiddlewareArgs = {
|
|
36
21
|
/** dev: модули грузятся через vite; в prod — из собранного бандла */
|
|
37
22
|
vite?: ViteDevServer;
|
|
38
23
|
};
|
|
39
24
|
|
|
40
|
-
const isProd = process.env.NODE_ENV === "production";
|
|
41
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
-
|
|
43
25
|
export default function getRenderMiddleware({ vite }: GetRenderMiddlewareArgs) {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const manifest = (
|
|
47
|
-
isProd ? JSON.parse(fs.readFileSync(paths.manifest, "utf8")) : {}
|
|
48
|
-
) as Manifest;
|
|
49
|
-
|
|
50
|
-
const findManifestEntry = (fragment: string) =>
|
|
51
|
-
Object.entries(manifest).find(([key]) => key.includes(fragment))?.[1];
|
|
52
|
-
|
|
53
|
-
let bootstrapModules: string[];
|
|
54
|
-
if (isProd) {
|
|
55
|
-
const entry = findManifestEntry("entry-client");
|
|
56
|
-
if (!entry) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
"[react] entry-client not found in build manifest — запусти сборку (npm run build)",
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
bootstrapModules = [`/${entry.file}`];
|
|
62
|
-
} else {
|
|
63
|
-
bootstrapModules = ["/entry-client.tsx"];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// в prod SSR-бандл импортируется один раз, в dev — через vite (HMR)
|
|
67
|
-
let prodRenderer: Promise<typeof import("../frontend/entry-server.js")>;
|
|
68
|
-
const loadRenderer = () => {
|
|
69
|
-
if (isProd) {
|
|
70
|
-
prodRenderer ??= import(
|
|
71
|
-
pathToFileURL(paths.serverEntry).href
|
|
72
|
-
) as Promise<typeof import("../frontend/entry-server.js")>;
|
|
73
|
-
return prodRenderer;
|
|
74
|
-
}
|
|
75
|
-
return vite!.ssrLoadModule(
|
|
76
|
-
path.join(__dirname, "../frontend/entry-server.js"),
|
|
77
|
-
) as Promise<typeof import("../frontend/entry-server.js")>;
|
|
78
|
-
};
|
|
26
|
+
const { manifest, bootstrapModules } = resolveClientAssets();
|
|
27
|
+
const loadRenderer = createRendererLoader(vite);
|
|
79
28
|
|
|
80
29
|
return async function rendererMiddleware(req: Request, res: Response) {
|
|
81
30
|
try {
|
|
@@ -118,99 +67,17 @@ export default function getRenderMiddleware({ vite }: GetRenderMiddlewareArgs) {
|
|
|
118
67
|
const page = rendererModule.hasUglyPage(strippedPathname) as
|
|
119
68
|
UglyModule | undefined;
|
|
120
69
|
|
|
121
|
-
const
|
|
70
|
+
const registerModule = createPreloadTagWriter(manifest, res);
|
|
122
71
|
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
);
|
|
130
|
-
(_module.css ?? []).forEach((e) => {
|
|
131
|
-
if (!written[e]) {
|
|
132
|
-
written[e] = true;
|
|
133
|
-
res.write(`<link rel="stylesheet" href="/${e}"/>`);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
(_module.imports ?? []).map((_link) => extractTags(_link));
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
const registerModule = (_module: UglyModule) => {
|
|
140
|
-
if (!requiredModules.includes(_module)) {
|
|
141
|
-
requiredModules.push(_module);
|
|
142
|
-
|
|
143
|
-
extractTags(_module.modulePath.substring(1));
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// SSR-запросы к tRPC идут в собственный сервер по HTTP (dev-режим).
|
|
148
|
-
// Атомы плагина объявлены в его аугментации Quark, но здесь берём
|
|
149
|
-
// их структурно: значения нужны как есть, до готовности плагина.
|
|
150
|
-
const reactAtoms = (
|
|
151
|
-
globalThis as {
|
|
152
|
-
quark?: {
|
|
153
|
-
react?: {
|
|
154
|
-
port?: { value?: number };
|
|
155
|
-
lang?: { value?: string };
|
|
156
|
-
favicon?: { value?: string };
|
|
157
|
-
};
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
).quark?.react;
|
|
161
|
-
const port = reactAtoms?.port?.value ?? 3000;
|
|
162
|
-
// клиент собирается здесь, а роутер объявляет приложение — тот же
|
|
163
|
-
// шов, что и в сторе
|
|
164
|
-
const trpcClient = createTRPCClient({
|
|
165
|
-
links: [
|
|
166
|
-
httpLink({
|
|
167
|
-
url: `http://127.0.0.1:${port}/api/trpc`,
|
|
168
|
-
// пробрасываем cookie запроса, чтобы SSR рендерил
|
|
169
|
-
// страницу от имени залогиненного пользователя
|
|
170
|
-
headers: req.headers.cookie
|
|
171
|
-
? { cookie: req.headers.cookie }
|
|
172
|
-
: {},
|
|
173
|
-
fetch: jsonSafeFetch,
|
|
174
|
-
}),
|
|
175
|
-
],
|
|
176
|
-
}) as TRPCClient;
|
|
177
|
-
|
|
178
|
-
// Мета страницы резолвится ДО записи <head>, а её tRPC-вызовы
|
|
179
|
-
// дёргают callback store — писать их сразу нельзя: заголовки
|
|
180
|
-
// ушли бы раньше res.status(), а скрипты — раньше <!doctype>.
|
|
181
|
-
// Поэтому все записи кэша ждут, пока shell не будет записан.
|
|
182
|
-
let htmlReadyResolve!: () => void;
|
|
183
|
-
const htmlReady = new Promise<void>((resolve) => {
|
|
184
|
-
htmlReadyResolve = resolve;
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// по мере резолва данных при рендере стримим их в ответ:
|
|
188
|
-
// клиентский Store гидратируется из window.cache без повторных
|
|
189
|
-
// запросов; NOT_READY_STATE будит зависшие Suspense-чтения
|
|
190
|
-
const store = new Store({
|
|
72
|
+
const reactAtoms = getReactAtoms();
|
|
73
|
+
const trpcClient = createRequestTrpcClient(
|
|
74
|
+
reactAtoms?.port?.value ?? 3000,
|
|
75
|
+
req.headers.cookie,
|
|
76
|
+
);
|
|
77
|
+
const { store, releaseCacheWrites } = createStreamingStore(
|
|
191
78
|
trpcClient,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
void htmlReady.then(() => {
|
|
195
|
-
if (value === NOT_READY_STATE) {
|
|
196
|
-
res.write(
|
|
197
|
-
`<script>window.cache=window.cache||{};window.cache[${safeKey}]=${serializeForScript(NOT_READY_STATE)};</script>`,
|
|
198
|
-
);
|
|
199
|
-
} else {
|
|
200
|
-
// undefined сериализуем как null: literal undefined в
|
|
201
|
-
// window.cache читается клиентом как cache miss и
|
|
202
|
-
// приводит к повторному suspense при гидратации
|
|
203
|
-
res.write(
|
|
204
|
-
`<script>window.cache[${safeKey}]=${
|
|
205
|
-
typeof value === "undefined"
|
|
206
|
-
? "null"
|
|
207
|
-
: serializeForScript(value)
|
|
208
|
-
};window.store?.emit(${safeKey},window.cache[${safeKey}]);</script>`,
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
},
|
|
213
|
-
});
|
|
79
|
+
res,
|
|
80
|
+
);
|
|
214
81
|
|
|
215
82
|
// мета страницы: резолвится ПЕРЕД стримом (title/description
|
|
216
83
|
// должны попасть в <head>), функции меты видят store и params
|
|
@@ -252,7 +119,7 @@ export default function getRenderMiddleware({ vite }: GetRenderMiddlewareArgs) {
|
|
|
252
119
|
res.write(head);
|
|
253
120
|
res.write(`<body><div id="root">`);
|
|
254
121
|
// shell записан — отпускаем отложенные записи кэша
|
|
255
|
-
|
|
122
|
+
releaseCacheWrites();
|
|
256
123
|
|
|
257
124
|
const stream = renderer.pipe({
|
|
258
125
|
...data,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Хелперы SSR-мидлвары.
|
|
2
|
+
|
|
3
|
+
export const isProd = process.env.NODE_ENV === "production";
|
|
4
|
+
|
|
5
|
+
/** Разбор Cookie-заголовка без внешних зависимостей. */
|
|
6
|
+
export const parseCookies = (header?: string): Record<string, string> =>
|
|
7
|
+
Object.fromEntries(
|
|
8
|
+
(header ?? "")
|
|
9
|
+
.split(";")
|
|
10
|
+
.map((part) => part.trim().split("="))
|
|
11
|
+
.filter((pair) => pair.length === 2)
|
|
12
|
+
.map(([k, v]) => [k!, decodeURIComponent(v!)]),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Атомы react-плагина. Объявлены в его аугментации Quark, но читаются
|
|
17
|
+
* структурно: значения нужны как есть, до готовности плагина.
|
|
18
|
+
*/
|
|
19
|
+
export type ReactAtoms = {
|
|
20
|
+
port?: { value?: number };
|
|
21
|
+
lang?: { value?: string };
|
|
22
|
+
favicon?: { value?: string };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getReactAtoms = (): ReactAtoms | undefined =>
|
|
26
|
+
(globalThis as { quark?: { react?: ReactAtoms } }).quark?.react;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Manifest } from "vite";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { buildPaths } from "../utils/viteConfig.js";
|
|
4
|
+
import { isProd } from "./helpers.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Клиентские ассеты рендера: манифест сборки и entry-модули для
|
|
8
|
+
* bootstrapModules. В dev манифеста нет — vite отдаёт исходник напрямую.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveClientAssets(): {
|
|
11
|
+
manifest: Manifest;
|
|
12
|
+
bootstrapModules: string[];
|
|
13
|
+
} {
|
|
14
|
+
const paths = buildPaths();
|
|
15
|
+
|
|
16
|
+
const manifest = (
|
|
17
|
+
isProd ? JSON.parse(fs.readFileSync(paths.manifest, "utf8")) : {}
|
|
18
|
+
) as Manifest;
|
|
19
|
+
|
|
20
|
+
if (!isProd) return { manifest, bootstrapModules: ["/entry-client.tsx"] };
|
|
21
|
+
|
|
22
|
+
const entry = Object.entries(manifest).find(([key]) =>
|
|
23
|
+
key.includes("entry-client"),
|
|
24
|
+
)?.[1];
|
|
25
|
+
if (!entry) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"[react] entry-client not found in build manifest — запусти сборку (npm run build)",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return { manifest, bootstrapModules: [`/${entry.file}`] };
|
|
31
|
+
}
|
package/src/utils/vite.ts
CHANGED
|
@@ -20,8 +20,15 @@ export async function getVite({
|
|
|
20
20
|
strictPort: true,
|
|
21
21
|
middlewareMode: true,
|
|
22
22
|
// у каждого приложения свой HMR-сокет: иначе вторая
|
|
23
|
-
// запущенная апка ловит "Port 24678 is already in use"
|
|
24
|
-
|
|
23
|
+
// запущенная апка ловит "Port 24678 is already in use".
|
|
24
|
+
// Высокие appPort заворачиваются в валидный диапазон,
|
|
25
|
+
// иначе ERR_SOCKET_BAD_PORT (порт > 65535)
|
|
26
|
+
hmr: {
|
|
27
|
+
port:
|
|
28
|
+
appPort + 20000 <= 65535
|
|
29
|
+
? appPort + 20000
|
|
30
|
+
: 1024 + ((appPort + 20000) % 64512),
|
|
31
|
+
},
|
|
25
32
|
},
|
|
26
33
|
}),
|
|
27
34
|
);
|