@quark-fw/plugin-react 0.1.2
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 +50 -0
- package/src/Router/ErrorPage.tsx +18 -0
- package/src/Router/Router.tsx +178 -0
- package/src/Router/gatherPages.ts +165 -0
- package/src/Router/getMatchedRoute.ts +24 -0
- package/src/Router/getRenderPath.ts +32 -0
- package/src/build.ts +64 -0
- package/src/frontend/App.tsx +99 -0
- package/src/frontend/ErrorHandler.tsx +58 -0
- package/src/frontend/HTML/index.tsx +155 -0
- package/src/frontend/displayError.ts +16 -0
- package/src/frontend/entry-client.tsx +74 -0
- package/src/frontend/entry-server.tsx +131 -0
- package/src/global.d.ts +19 -0
- package/src/i18n/I18nContext.tsx +217 -0
- package/src/i18n/config.ts +112 -0
- package/src/i18n/gatherLocales.ts +12 -0
- package/src/index.html +13 -0
- package/src/index.ts +102 -0
- package/src/seo/SeoContext.tsx +94 -0
- package/src/seo/createPageMeta.ts +68 -0
- package/src/ssr/getRenderMiddleware.ts +273 -0
- package/src/utils/plugins.ts +73 -0
- package/src/utils/removeSearchParamsFromUrl.ts +18 -0
- package/src/utils/serializeForScript.ts +26 -0
- package/src/utils/usePromise.ts +24 -0
- package/src/utils/vite.ts +31 -0
- package/src/utils/viteConfig.ts +71 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { Plugin } from "vite";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const uglyPath = path.join(__dirname, "../frontend");
|
|
8
|
+
|
|
9
|
+
export function loadModulesFromExternalFolderPlugin(params?: {
|
|
10
|
+
externalPaths?: string[];
|
|
11
|
+
}): Plugin {
|
|
12
|
+
const externalFolderPaths = params?.externalPaths || [uglyPath];
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
name: "load-modules-from-external-folder",
|
|
16
|
+
enforce: "post",
|
|
17
|
+
|
|
18
|
+
resolveFileUrl({ fileName, relativePath }) {
|
|
19
|
+
for (const externalFolderPath of externalFolderPaths) {
|
|
20
|
+
const p = path.join(externalFolderPath, relativePath, fileName);
|
|
21
|
+
if (fs.existsSync(p) && fs.statSync(p).isFile()) {
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
resolveId(fileName) {
|
|
28
|
+
for (const externalFolderPath of externalFolderPaths) {
|
|
29
|
+
const requested = path.join(externalFolderPath, fileName);
|
|
30
|
+
|
|
31
|
+
// prefer .js over .jsx
|
|
32
|
+
const jsFile = requested.replace(".jsx", ".js");
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync(jsFile) && fs.statSync(jsFile).isFile()) {
|
|
35
|
+
return jsFile;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
load(id) {
|
|
41
|
+
// Vite may pass a directory path: avoid reading it
|
|
42
|
+
if (!fs.existsSync(id)) return;
|
|
43
|
+
|
|
44
|
+
const stat = fs.statSync(id);
|
|
45
|
+
if (stat.isDirectory()) {
|
|
46
|
+
return null; // ignore directories
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
return fs.readFileSync(id, "utf8");
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.warn(`Error loading ${id}:`, e);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
configureServer(server) {
|
|
57
|
+
server.middlewares.use((req, res, next) => {
|
|
58
|
+
if (!req.url) return next();
|
|
59
|
+
|
|
60
|
+
for (const externalFolderPath of externalFolderPaths) {
|
|
61
|
+
const file = path.join(externalFolderPath, req.url);
|
|
62
|
+
|
|
63
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile()) {
|
|
64
|
+
req.url = file;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
next();
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default function removeSearchParamsFromUrl(
|
|
2
|
+
url: string,
|
|
3
|
+
base: string = "http://localhost",
|
|
4
|
+
): string {
|
|
5
|
+
try {
|
|
6
|
+
const parsedUrl = new URL(url, base);
|
|
7
|
+
parsedUrl.search = "";
|
|
8
|
+
|
|
9
|
+
// If the original URL was relative, return only the pathname and hash
|
|
10
|
+
if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) {
|
|
11
|
+
return parsedUrl.pathname + parsedUrl.hash;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return parsedUrl.toString();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new Error("Invalid URL", { cause: error });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Безопасная сериализация значения для вставки внутрь inline <script>.
|
|
2
|
+
// JSON.stringify сам по себе небезопасен: не экранирует </script>, открытие
|
|
3
|
+
// HTML-комментария и JS-разделители строк U+2028/U+2029 — строка со
|
|
4
|
+
// </script> внутри вырвалась бы из контекста скрипта (XSS). Экранирование в
|
|
5
|
+
// \uXXXX прозрачно для JSON.parse и движка JS.
|
|
6
|
+
const LINE_SEPARATOR = String.fromCharCode(0x2028);
|
|
7
|
+
const PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029);
|
|
8
|
+
|
|
9
|
+
const ESCAPE_LOOKUP: Record<string, string> = {
|
|
10
|
+
"<": "\\u003c",
|
|
11
|
+
">": "\\u003e",
|
|
12
|
+
"&": "\\u0026",
|
|
13
|
+
[LINE_SEPARATOR]: "\\u2028",
|
|
14
|
+
[PARAGRAPH_SEPARATOR]: "\\u2029",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ESCAPE_REGEX = new RegExp(
|
|
18
|
+
`[<>&${LINE_SEPARATOR}${PARAGRAPH_SEPARATOR}]`,
|
|
19
|
+
"g",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export default function serializeForScript(value: unknown): string {
|
|
23
|
+
const json = JSON.stringify(value);
|
|
24
|
+
if (json === undefined) return "undefined";
|
|
25
|
+
return json.replace(ESCAPE_REGEX, (char) => ESCAPE_LOOKUP[char]!);
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function usePromise<T>(promise: Promise<T>): () => T {
|
|
2
|
+
let status = "pending";
|
|
3
|
+
let response: T;
|
|
4
|
+
const suspender = promise.then(
|
|
5
|
+
(res) => {
|
|
6
|
+
status = "success";
|
|
7
|
+
response = res;
|
|
8
|
+
},
|
|
9
|
+
(err) => {
|
|
10
|
+
status = "error";
|
|
11
|
+
response = err as never;
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
return () => {
|
|
15
|
+
switch (status) {
|
|
16
|
+
case "pending":
|
|
17
|
+
throw suspender;
|
|
18
|
+
case "error":
|
|
19
|
+
throw response;
|
|
20
|
+
default:
|
|
21
|
+
return response;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createServer, ViteDevServer } from "vite";
|
|
2
|
+
import { createViteConfig } from "./viteConfig.js";
|
|
3
|
+
|
|
4
|
+
let vite: ViteDevServer | null = null;
|
|
5
|
+
|
|
6
|
+
export async function getVite({
|
|
7
|
+
root,
|
|
8
|
+
/** порт HTTP-сервера приложения; от него считается порт HMR-сокета */
|
|
9
|
+
appPort,
|
|
10
|
+
}: {
|
|
11
|
+
root: string;
|
|
12
|
+
appPort: number;
|
|
13
|
+
}): Promise<ViteDevServer> {
|
|
14
|
+
if (!vite) {
|
|
15
|
+
vite = await createServer(
|
|
16
|
+
createViteConfig({
|
|
17
|
+
appType: "custom",
|
|
18
|
+
root,
|
|
19
|
+
server: {
|
|
20
|
+
strictPort: true,
|
|
21
|
+
middlewareMode: true,
|
|
22
|
+
// у каждого приложения свой HMR-сокет: иначе вторая
|
|
23
|
+
// запущенная апка ловит "Port 24678 is already in use"
|
|
24
|
+
hmr: { port: appPort + 20000 },
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return vite;
|
|
31
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { InlineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import tsconfigPaths from "vite-tsconfig-paths";
|
|
4
|
+
import svgr from "vite-plugin-svgr";
|
|
5
|
+
import { patchCssModules } from "vite-css-modules";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { loadModulesFromExternalFolderPlugin } from "./plugins.js";
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
/** Каталог фронтенд-исходников самого плагина (entry-client / entry-server). */
|
|
14
|
+
export const frontendDir = path.join(__dirname, "../frontend");
|
|
15
|
+
|
|
16
|
+
export const clientEntry = path.join(frontendDir, "entry-client.tsx");
|
|
17
|
+
export const serverEntry = path.join(frontendDir, "entry-server.tsx");
|
|
18
|
+
|
|
19
|
+
/** Корень фронтенда приложения (src/frontend). */
|
|
20
|
+
export const appFrontendRoot = (cwd = process.cwd()) =>
|
|
21
|
+
path.join(cwd, "src/frontend");
|
|
22
|
+
|
|
23
|
+
/** Каталоги сборки приложения. */
|
|
24
|
+
export const buildPaths = (cwd = process.cwd()) => ({
|
|
25
|
+
root: path.join(cwd, "build"),
|
|
26
|
+
client: path.join(cwd, "build/client"),
|
|
27
|
+
server: path.join(cwd, "build/server"),
|
|
28
|
+
manifest: path.join(cwd, "build/client/.vite/manifest.json"),
|
|
29
|
+
serverEntry: path.join(cwd, "build/server/entry-server.js"),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Пакеты @quark/* в приложении — это симлинки на ИСХОДНИКИ соседних
|
|
34
|
+
* workspace-пакетов. Если позволить vite их пре-бандлить, он кэширует
|
|
35
|
+
* результат в node_modules/.vite и продолжает отдавать старую версию даже
|
|
36
|
+
* после правок в пакете (правка видна только после ручной очистки кэша).
|
|
37
|
+
* Поэтому исключаем их из оптимизации — тогда HMR работает как для
|
|
38
|
+
* обычных файлов приложения.
|
|
39
|
+
*/
|
|
40
|
+
const workspacePackages = (cwd: string): string[] => {
|
|
41
|
+
try {
|
|
42
|
+
const pkg = JSON.parse(
|
|
43
|
+
fs.readFileSync(path.join(cwd, "package.json"), "utf8"),
|
|
44
|
+
) as Record<string, Record<string, string> | undefined>;
|
|
45
|
+
return Object.keys({
|
|
46
|
+
...(pkg.dependencies ?? {}),
|
|
47
|
+
...(pkg.devDependencies ?? {}),
|
|
48
|
+
}).filter(
|
|
49
|
+
(name) => name.startsWith("@quark/") || name.startsWith("@quark-fw/"),
|
|
50
|
+
);
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Единая конфигурация vite для dev-сервера и для сборки. */
|
|
57
|
+
export function createViteConfig(overrides?: InlineConfig): InlineConfig {
|
|
58
|
+
return {
|
|
59
|
+
plugins: [
|
|
60
|
+
patchCssModules(),
|
|
61
|
+
svgr({ include: "**/*.svg" }),
|
|
62
|
+
react(),
|
|
63
|
+
tsconfigPaths(),
|
|
64
|
+
loadModulesFromExternalFolderPlugin(),
|
|
65
|
+
],
|
|
66
|
+
optimizeDeps: {
|
|
67
|
+
exclude: workspacePackages(process.cwd()),
|
|
68
|
+
},
|
|
69
|
+
...overrides,
|
|
70
|
+
};
|
|
71
|
+
}
|