@quark-fw/plugin-react 0.1.6 → 0.1.8

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.
@@ -1,16 +1,5 @@
1
- import {
2
- createContext,
3
- ReactNode,
4
- useContext,
5
- useEffect,
6
- useMemo,
7
- useState,
8
- } from "react";
9
- import { useLocation } from "react-router-dom";
10
- import { useStore } from "@quark-fw/store";
11
- import { matchPath } from "react-router-dom";
12
- import { getMatchedRoute } from "../Router/getMatchedRoute.js";
13
- import type { PageMeta, ResolvedSeo } from "./createPageMeta.js";
1
+ import { createContext, useContext } from "react";
2
+ import type { ResolvedSeo } from "./createPageMeta.js";
14
3
 
15
4
  export type SeoMap = Record<string, ResolvedSeo>;
16
5
 
@@ -26,69 +15,4 @@ const SeoContext = createContext<SeoContextValue>({
26
15
 
27
16
  export const useSeo = () => useContext(SeoContext);
28
17
 
29
- export function SeoProvider({
30
- children,
31
- seoObject: initial,
32
- }: {
33
- children: ReactNode;
34
- seoObject?: SeoMap;
35
- }) {
36
- const [seoObject, setSeoObject] = useState<SeoMap>(initial ?? {});
37
- const { pathname } = useLocation();
38
- const store = useStore();
39
-
40
- const current = seoObject[pathname];
41
-
42
- // клиентская навигация: меты для нового пути ещё нет — резолвим её
43
- // из модуля страницы (тот же resolveMeta, что и на сервере)
44
- useEffect(() => {
45
- if (current) return;
46
- let cancelled = false;
47
- void (async () => {
48
- const uglyModule = getMatchedRoute(pathname);
49
- if (!uglyModule) return;
50
- const loaded = await uglyModule.load();
51
- const meta = (loaded?.default as { meta?: PageMeta } | undefined)
52
- ?.meta;
53
- if (!meta || cancelled) return;
54
- const routeProps = matchPath(
55
- { path: uglyModule.urlPath, end: false, caseSensitive: false },
56
- pathname,
57
- );
58
- const { resolveMeta } = await import("./createPageMeta.js");
59
- const resolved = await resolveMeta(meta, {
60
- store,
61
- params: (routeProps?.params ?? {}) as Record<string, string>,
62
- });
63
- if (!cancelled) {
64
- setSeoObject((prev) => ({ ...prev, [pathname]: resolved }));
65
- }
66
- })();
67
- return () => {
68
- cancelled = true;
69
- };
70
- // store живёт всё время жизни приложения и меняться не может, а его
71
- // добавление в зависимости ничего бы не изменило
72
- // eslint-disable-next-line react-hooks/exhaustive-deps
73
- }, [pathname, current]);
74
-
75
- // заголовок вкладки при клиентской навигации
76
- useEffect(() => {
77
- if (typeof document === "undefined") return;
78
- if (current?.title) document.title = current.title;
79
- }, [current?.title]);
80
-
81
- const value = useMemo<SeoContextValue>(
82
- () => ({
83
- ...(current ?? {}),
84
- seoObject,
85
- setPathnameSeo: (path, seo) =>
86
- setSeoObject((prev) => ({ ...prev, [path]: seo })),
87
- }),
88
- [current, seoObject],
89
- );
90
-
91
- return <SeoContext.Provider value={value}>{children}</SeoContext.Provider>;
92
- }
93
-
94
18
  export default SeoContext;
@@ -0,0 +1,72 @@
1
+ import { ReactNode, useEffect, useMemo, useState } from "react";
2
+ import { useLocation } from "react-router-dom";
3
+ import { useStore } from "@quark-fw/store";
4
+ import { matchPath } from "react-router-dom";
5
+ import { getMatchedRoute } from "../Router/getMatchedRoute.js";
6
+ import type { PageMeta } from "./createPageMeta.js";
7
+ import SeoContext, { type SeoContextValue, type SeoMap } from "./SeoContext.js";
8
+
9
+ export function SeoProvider({
10
+ children,
11
+ seoObject: initial,
12
+ }: {
13
+ children: ReactNode;
14
+ seoObject?: SeoMap;
15
+ }) {
16
+ const [seoObject, setSeoObject] = useState<SeoMap>(initial ?? {});
17
+ const { pathname } = useLocation();
18
+ const store = useStore();
19
+
20
+ const current = seoObject[pathname];
21
+
22
+ // клиентская навигация: меты для нового пути ещё нет — резолвим её
23
+ // из модуля страницы (тот же resolveMeta, что и на сервере)
24
+ useEffect(() => {
25
+ if (current) return;
26
+ let cancelled = false;
27
+ void (async () => {
28
+ const uglyModule = getMatchedRoute(pathname);
29
+ if (!uglyModule) return;
30
+ const loaded = await uglyModule.load();
31
+ const meta = (loaded?.default as { meta?: PageMeta } | undefined)
32
+ ?.meta;
33
+ if (!meta || cancelled) return;
34
+ const routeProps = matchPath(
35
+ { path: uglyModule.urlPath, end: false, caseSensitive: false },
36
+ pathname,
37
+ );
38
+ const { resolveMeta } = await import("./createPageMeta.js");
39
+ const resolved = await resolveMeta(meta, {
40
+ store,
41
+ params: (routeProps?.params ?? {}) as Record<string, string>,
42
+ });
43
+ if (!cancelled) {
44
+ setSeoObject((prev) => ({ ...prev, [pathname]: resolved }));
45
+ }
46
+ })();
47
+ return () => {
48
+ cancelled = true;
49
+ };
50
+ // store живёт всё время жизни приложения и меняться не может, а его
51
+ // добавление в зависимости ничего бы не изменило
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ }, [pathname, current]);
54
+
55
+ // заголовок вкладки при клиентской навигации
56
+ useEffect(() => {
57
+ if (typeof document === "undefined") return;
58
+ if (current?.title) document.title = current.title;
59
+ }, [current?.title]);
60
+
61
+ const value = useMemo<SeoContextValue>(
62
+ () => ({
63
+ ...(current ?? {}),
64
+ seoObject,
65
+ setPathnameSeo: (path, seo) =>
66
+ setSeoObject((prev) => ({ ...prev, [path]: seo })),
67
+ }),
68
+ [current, seoObject],
69
+ );
70
+
71
+ return <SeoContext.Provider value={value}>{children}</SeoContext.Provider>;
72
+ }
@@ -3,7 +3,11 @@ import { Request, Response } from "express";
3
3
  import path from "node:path";
4
4
  import * as fs from "node:fs";
5
5
  import { createTRPCClient, httpLink } from "@trpc/client";
6
- import Store, { jsonSafeFetch, type TRPCClient } from "@quark-fw/store";
6
+ import Store, {
7
+ jsonSafeFetch,
8
+ NOT_READY_STATE,
9
+ type TRPCClient,
10
+ } from "@quark-fw/store";
7
11
  import { UglyModule } from "../Router/gatherPages.js";
8
12
  import removeSearchParamsFromUrl from "../utils/removeSearchParamsFromUrl.js";
9
13
  import serializeForScript from "../utils/serializeForScript.js";
@@ -188,9 +192,9 @@ export default function getRenderMiddleware({ vite }: GetRenderMiddlewareArgs) {
188
192
  callback: (key, value) => {
189
193
  const safeKey = serializeForScript(key);
190
194
  void htmlReady.then(() => {
191
- if (value === "NOT_READY_STATE") {
195
+ if (value === NOT_READY_STATE) {
192
196
  res.write(
193
- `<script>window.cache=window.cache||{};window.cache[${safeKey}]='NOT_READY_STATE';</script>`,
197
+ `<script>window.cache=window.cache||{};window.cache[${safeKey}]=${serializeForScript(NOT_READY_STATE)};</script>`,
194
198
  );
195
199
  } else {
196
200
  // undefined сериализуем как null: literal undefined в
@@ -0,0 +1,81 @@
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
+ /**
14
+ * Пакеты @quark/* в приложении — это симлинки на ИСХОДНИКИ соседних
15
+ * workspace-пакетов. Если позволить vite их пре-бандлить, он кэширует
16
+ * результат в node_modules/.vite и продолжает отдавать старую версию даже
17
+ * после правок в пакете (правка видна только после ручной очистки кэша).
18
+ * Поэтому исключаем их из оптимизации — тогда HMR работает как для
19
+ * обычных файлов приложения.
20
+ */
21
+ const workspacePackages = (cwd: string): string[] => {
22
+ try {
23
+ const pkg = JSON.parse(
24
+ fs.readFileSync(path.join(cwd, "package.json"), "utf8"),
25
+ ) as Record<string, Record<string, string> | undefined>;
26
+ return Object.keys({
27
+ ...(pkg.dependencies ?? {}),
28
+ ...(pkg.devDependencies ?? {}),
29
+ }).filter(
30
+ (name) =>
31
+ name.startsWith("@quark/") || name.startsWith("@quark-fw/"),
32
+ );
33
+ } catch {
34
+ return [];
35
+ }
36
+ };
37
+
38
+ /** Плагин поставлен из npm (лежит внутри node_modules), а не симлинкован из монорепы. */
39
+ const installedFromRegistry = fs
40
+ .realpathSync(__dirname)
41
+ .replace(/\\/g, "/")
42
+ .includes("/node_modules/");
43
+
44
+ /** Единая конфигурация vite для dev-сервера и для сборки. */
45
+ export function createViteConfig(overrides?: InlineConfig): InlineConfig {
46
+ return {
47
+ plugins: [
48
+ patchCssModules(),
49
+ svgr({ include: "**/*.svg" }),
50
+ react(),
51
+ tsconfigPaths(),
52
+ loadModulesFromExternalFolderPlugin(),
53
+ ],
54
+ // Установленный из реестра плагин лежит в node_modules, куда
55
+ // @vitejs/plugin-react не заглядывает (у него exclude /node_modules/).
56
+ // Без явного automatic-рантайма его собственный JSX собирается в
57
+ // React.createElement и падает с "React is not defined".
58
+ esbuild: { jsx: "automatic", jsxImportSource: "react" },
59
+ optimizeDeps: {
60
+ exclude: workspacePackages(process.cwd()),
61
+ // Плагин исключён из оптимизации, поэтому vite не сканирует его
62
+ // импорты: установленный из реестра, он тянет react-router и mantine
63
+ // сырыми, и CJS-пакеты внутри них (cookie) падают на named-импортах.
64
+ // В монорепо, наоборот, форсировать пре-бандл нельзя — рядом с
65
+ // симлинкованными исходниками появилась бы вторая копия React.
66
+ include: installedFromRegistry
67
+ ? [
68
+ "react",
69
+ "react-dom",
70
+ "react-dom/client",
71
+ "react-router-dom",
72
+ "@mantine/core",
73
+ "@mantine/dates",
74
+ "@mantine/modals",
75
+ "@mantine/notifications",
76
+ ]
77
+ : [],
78
+ },
79
+ ...overrides,
80
+ };
81
+ }
package/src/utils/vite.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createServer, ViteDevServer } from "vite";
2
- import { createViteConfig } from "./viteConfig.js";
2
+ import { createViteConfig } from "./createViteConfig.js";
3
3
 
4
4
  let vite: ViteDevServer | null = null;
5
5
 
@@ -1,12 +1,7 @@
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";
1
+ // Пути фронтенда и сборки (хелперы). Сама конфигурация vite —
2
+ // в createViteConfig.ts.
7
3
  import path from "node:path";
8
4
  import { fileURLToPath } from "node:url";
9
- import { loadModulesFromExternalFolderPlugin } from "./plugins.js";
10
5
 
11
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
7
 
@@ -28,72 +23,3 @@ export const buildPaths = (cwd = process.cwd()) => ({
28
23
  manifest: path.join(cwd, "build/client/.vite/manifest.json"),
29
24
  serverEntry: path.join(cwd, "build/server/entry-server.js"),
30
25
  });
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
- /** Плагин поставлен из npm (лежит внутри node_modules), а не симлинкован из монорепы. */
57
- const installedFromRegistry = fs
58
- .realpathSync(__dirname)
59
- .replace(/\\/g, "/")
60
- .includes("/node_modules/");
61
-
62
- /** Единая конфигурация vite для dev-сервера и для сборки. */
63
- export function createViteConfig(overrides?: InlineConfig): InlineConfig {
64
- return {
65
- plugins: [
66
- patchCssModules(),
67
- svgr({ include: "**/*.svg" }),
68
- react(),
69
- tsconfigPaths(),
70
- loadModulesFromExternalFolderPlugin(),
71
- ],
72
- // Установленный из реестра плагин лежит в node_modules, куда
73
- // @vitejs/plugin-react не заглядывает (у него exclude /node_modules/).
74
- // Без явного automatic-рантайма его собственный JSX собирается в
75
- // React.createElement и падает с "React is not defined".
76
- esbuild: { jsx: "automatic", jsxImportSource: "react" },
77
- optimizeDeps: {
78
- exclude: workspacePackages(process.cwd()),
79
- // Плагин исключён из оптимизации, поэтому vite не сканирует его
80
- // импорты: установленный из реестра, он тянет react-router и mantine
81
- // сырыми, и CJS-пакеты внутри них (cookie) падают на named-импортах.
82
- // В монорепо, наоборот, форсировать пре-бандл нельзя — рядом с
83
- // симлинкованными исходниками появилась бы вторая копия React.
84
- include: installedFromRegistry
85
- ? [
86
- "react",
87
- "react-dom",
88
- "react-dom/client",
89
- "react-router-dom",
90
- "@mantine/core",
91
- "@mantine/dates",
92
- "@mantine/modals",
93
- "@mantine/notifications",
94
- ]
95
- : [],
96
- },
97
- ...overrides,
98
- };
99
- }
@@ -1,24 +0,0 @@
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
- }