From ef59ca4ebe581ebfd2a6e8ea8e50fa5853fd26f2 Mon Sep 17 00:00:00 2001
From: Vladimir Shestakov <boolive@yandex.ru>
Date: Wed, 22 Nov 2023 21:16:42 +0300
Subject: [PATCH] deleted server

---
 server/config.ts                |  26 ------
 server/index.ts                 |  20 -----
 server/routers/common/index.ts  |  22 ------
 server/routers/index.ts         |  11 ---
 server/routers/initial/index.ts |  18 -----
 server/routers/proxy/index.ts   |  38 ---------
 server/routers/render/index.ts  | 135 --------------------------------
 server/types.ts                 |  29 -------
 server/utils/initial-store.ts   |  44 -----------
 server/utils/loadEnv.ts         |  14 ----
 server/utils/react-template.ts  |  78 ------------------
 server/utils/stream-helmet.ts   |  29 -------
 12 files changed, 464 deletions(-)
 delete mode 100644 server/config.ts
 delete mode 100644 server/index.ts
 delete mode 100644 server/routers/common/index.ts
 delete mode 100644 server/routers/index.ts
 delete mode 100644 server/routers/initial/index.ts
 delete mode 100644 server/routers/proxy/index.ts
 delete mode 100644 server/routers/render/index.ts
 delete mode 100644 server/types.ts
 delete mode 100644 server/utils/initial-store.ts
 delete mode 100644 server/utils/loadEnv.ts
 delete mode 100644 server/utils/react-template.ts
 delete mode 100644 server/utils/stream-helmet.ts

diff --git a/server/config.ts b/server/config.ts
deleted file mode 100644
index dbd9d6b..0000000
--- a/server/config.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import {IServerConfig} from "./types";
-
-export default (env: ImportMetaEnv): IServerConfig => {
-  const config: IServerConfig = {
-    // HTTP сервер для рендера. Параметры также используются для dev сервера Vite
-    server: {
-      host: env.HOST || 'localhost',
-      port: env.PORT || 8050,
-    },
-    proxy: {
-      enabled: true,//env.PROD, //В dev режиме работает прокси Vite(в режиме middleware), но у него ошибка на POST запросы, поэтому включен свой прокси
-      routes: {}
-    },
-    render: {
-      enabled: true,
-    }
-  };
-  if (env.API_PATH) {
-    config.proxy.routes[env.API_PATH] = {
-      target: env.API_URL,
-      secure: false,
-      changeOrigin: true,
-    };
-  }
-  return config;
-};
diff --git a/server/index.ts b/server/index.ts
deleted file mode 100644
index fd3f42d..0000000
--- a/server/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * HTTP server for render
- */
-import express from "express";
-import InitialStore from "./utils/initial-store";
-import routers from './routers/index';
-import serverConfig from "./config";
-import loadEnv from "./utils/loadEnv";
-
-(async () => {
-  const env  = loadEnv();
-  const config = serverConfig(env);
-  const initialStore = new InitialStore();
-  const app = express();
-  for (const route of routers) {
-    await route({app, initialStore, config, env});
-  }
-  app.listen(config.server.port);
-  console.info(`Server run on http://${config.server.host}:${config.server.port}`);
-})().catch(console.warn);
diff --git a/server/routers/common/index.ts b/server/routers/common/index.ts
deleted file mode 100644
index 22155a4..0000000
--- a/server/routers/common/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import path from "path";
-import express from "express";
-import cookieParser from "cookie-parser";
-import {fileURLToPath} from "url";
-import {IRouteContext} from "../../types";
-
-/**
- * Общие обработки запроса в express и роут на production файлы.
- * @param app
- * @param config
- * @param env
- */
-export default async ({app, env}: IRouteContext) => {
-  const __dirname = path.dirname(fileURLToPath(import.meta.url));
-  // Отдача файлов кроме index.html
-  if (env.PROD) {
-    app.use(express.static(path.resolve(__dirname, '../../../dist/client'), {index: false}));
-  }
-  app.use(express.json()); // for parsing application/json
-  app.use(express.urlencoded({extended: true})); // for parsing application/x-www-form-urlencoded
-  app.use(cookieParser());
-};
diff --git a/server/routers/index.ts b/server/routers/index.ts
deleted file mode 100644
index a59c859..0000000
--- a/server/routers/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import initial from "./initial/index.js";
-import common from "./common/index.js";
-import proxy from "./proxy/index.js";
-import render from "./render/index.js";
-
-export default [
-  proxy,
-  common,
-  initial,
-  render,
-];
diff --git a/server/routers/initial/index.ts b/server/routers/initial/index.ts
deleted file mode 100644
index b6396d4..0000000
--- a/server/routers/initial/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {IRouteContext} from "../../types";
-
-/**
- * Роут для отдачи состояния (данных) после рендера React приложения
- * Состояние не отправляется вместе с HTML, его необходимо запросить по ключу
- * Состояние можно получить один раз, после чего оно удаляется из хранилища.
- * @param app
- * @param initialStore
- */
-export default async ({app, initialStore}: IRouteContext) => {
-  // Выборка состояния с которым выполнялся рендер
-  // Выборка доступна один раз
-  app.get('/initial/:key', async (req, res) => {
-    const key = req.params.key;
-    const secret = req.cookies.stateSecret;
-    res.json(initialStore.get({key, secret}) || {});
-  });
-};
diff --git a/server/routers/proxy/index.ts b/server/routers/proxy/index.ts
deleted file mode 100644
index 3985ff7..0000000
--- a/server/routers/proxy/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import httpProxy from "http-proxy";
-import {IRouteContext} from "../../types";
-
-/**
- * Проксирование запросов в соответствии с настройками приложения
- * Обычно проксируются запросы к АПИ для обхода CORS
- * @param app
- * @param config
- */
-export default async ({app, config}: IRouteContext) => {
-
-  if (config.proxy.enabled) {
-    // // Прокси на внешний сервер по конфигу (обычно для апи)
-    const proxy = httpProxy.createProxyServer({/*timeout: 5000, */proxyTimeout: 5000});
-    proxy.on('error', function (err, req, res) {
-      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-      // @ts-ignore
-      res.writeHead(500, {'Content-Type': 'text/plain'});
-      res.end(err.toString());
-    });
-
-    app.use((req, res, next) => {
-      for (const path of Object.keys(config.proxy.routes)) {
-        if ((path[0] === '^' && new RegExp(path).test(req.url)) || req.url.startsWith(path)) {
-          try {
-            return proxy.web(req, res, config.proxy.routes[path]);
-          } catch (e) {
-            console.error(e);
-            res.send(500);
-          }
-        }
-      }
-      next();
-    });
-
-    console.log(`Proxy in SSR enabled`);
-  }
-};
diff --git a/server/routers/render/index.ts b/server/routers/render/index.ts
deleted file mode 100644
index a7315b4..0000000
--- a/server/routers/render/index.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import React from "react";
-import {renderToPipeableStream} from "react-dom/server";
-import reactTemplate from "../../utils/react-template";
-import streamHelmet from "../../utils/stream-helmet";
-import {createServer as createViteServer} from "vite";
-import fs from "fs";
-import path from "path";
-import {IRouteContext} from "../../types";
-import {Request, Response} from "express";
-import {fileURLToPath} from "url";
-import {HelmetServerState} from "react-helmet-async";
-
-/**
- * SSR - рендер React приложения в HTML
- * @param app Express приложение
- * @param initialStore Хранилище для запоминания состояния сервисов
- * @param config Настройки сервера
- * @param env Переменные окружения
- */
-export default async ({app, initialStore, config, env}: IRouteContext) => {
-  const __dirname = path.dirname(fileURLToPath(import.meta.url));
-  // Fix for render;
-  React.useLayoutEffect = React.useEffect;
-
-  // Сборщик Vite для рендера в режиме разработки
-  const vite = env.DEV ? await createViteServer({
-    server: {middlewareMode: true},
-    appType: 'custom',
-  }) : undefined;
-  if (vite) app.use(vite.middlewares);
-
-  // React приложение для ренедра. В режиме разработки импортируются исходники через Vite
-  const root = vite
-    ? (await vite.ssrLoadModule('../src/root.tsx')).default
-    : (await import('../../../dist/server/root.js')).default;
-
-  // HTML шаблон
-  const rootTemplate = fs.readFileSync(
-    path.resolve(__dirname, env.DEV
-      ? '../../../src/index.html'
-      : '../../../dist/client/index.html'),
-    'utf-8',
-  );
-
-  // Рендер
-  app.get('/*', async (req: Request, res: Response) => {
-    // Запрос на файл, которого нет (чтобы не рендерить приложение из-за этого)
-    // Если файл есть, то он бы отправился обработчиком файлов
-    if (req.originalUrl.match(/\.[a-z0-9]+$/u)) {
-      res.writeHead(404, {'Content-Type': 'text/html; charset=utf-8'});
-      res.end('Not Found');
-      return;
-    }
-
-    // В режиме разработки в шаблон вставляются скрипты Vite для горячего обновления
-    const template = vite
-      ? await vite.transformIndexHtml(req.originalUrl, rootTemplate)
-      : rootTemplate;
-
-    // Если рендер отключен, то отдаём index.html (шаблон)
-    if (!config.render.enabled) {
-      res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
-      res.end(template);
-      return;
-    }
-
-    // @todo Если есть кэш, то отдать его. Предусмотреть, что состояние тоже было запомнено (возможно, отдельно и временно помещается в initialStore)
-
-    // Секрет для идентификации дампа от всех сервисов
-    const secret = initialStore.makeSecretKey();
-
-    // Корневой React компонент, сервис менеджер приложения и контекст с мета-данными html
-    const {Root, servicesManager, head} = await root({
-      ...env,
-      req: {
-        url: req.originalUrl,
-        headers: req.headers,
-        cookies: req.cookies
-      }
-    });
-
-    // HTML шаблон конвертирует в ReactNode со вставкой в него компонента приложения.
-    // Из шаблона вычленяются скрипты, чтобы отдать их в потоке, но после html разметки.
-    const {jsx, scripts, modules} = reactTemplate(Root, template);
-
-    // Признак ошибки
-    let didError = false;
-
-    const sendHeaders = (res: Response) => {
-      res.cookie('stateSecret', secret.secret, {httpOnly: true/*, maxAge: 100/*, secure: true*/});
-      res.writeHead(didError ? 500 : 200, {
-        'Content-Type': 'text/html; charset=utf-8',
-        'Cache-Control': 'public,max-age=300,s-maxage=900'
-      });
-    };
-
-    const {pipe, abort} = renderToPipeableStream(jsx, {
-      bootstrapScriptContent: `window.initialKey="${secret.key}"`,
-      bootstrapModules: modules,
-      bootstrapScripts: scripts,
-      // Частичный рендер требует отдачи состояния вместе с каждым патчем (не реализовано)
-      // onShellReady() {
-      //   if (config.render.partial) {
-      //     sendHeaders(res);
-      //     pipe(res);
-      //   }
-      // },
-      // onShellError(error) {
-      //   res.writeHead(500, {'Content-Type': 'text/html; charset=utf-8'});
-      //   res.send('<h1>Something went wrong</h1>');
-      //   if (vite) vite.ssrFixStacktrace(error as Error);
-      //   console.error(error);
-      // },
-      onAllReady() {
-        // if (!config.render.partial) {
-        sendHeaders(res);
-        if ('helmet' in head) {
-          streamHelmet(pipe, head.helmet as HelmetServerState).pipe(res);
-        }
-        // @todo Если нужно кэшировать, то понадобится читать поток рендера
-        // @todo Дамп также кэшируется, а не запомианется надолго в initialStore
-        /// }
-        // Дамп всех сервисов запоминается в initialStore
-        initialStore.remember(secret, servicesManager.collectDump());
-      },
-      onError(error) {
-        didError = true;
-        if (vite) vite.ssrFixStacktrace(error as Error);
-        console.error(error);
-      },
-    });
-    // Timeout
-    setTimeout(() => abort(), 10000);
-  });
-};
diff --git a/server/types.ts b/server/types.ts
deleted file mode 100644
index a3ad697..0000000
--- a/server/types.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import {Application} from "express";
-import InitialStore from "./utils/initial-store";
-import {ServerOptions} from "http-proxy";
-
-export interface IServerConfig {
-  server: {
-    host: string,
-    port: number,
-  },
-  proxy: {
-    // Включить прокси на сервере рендара
-    enabled: boolean
-    // Пути перенаправления запросов.
-    routes: {
-      [path: string]: ServerOptions
-    }
-  },
-  render: {
-    // SSR или отдать SPA? Можно использовать для включения рендера только для поисковых ботов
-    enabled: boolean,
-  }
-}
-
-export interface IRouteContext {
-  app: Application,
-  initialStore: InitialStore,
-  config: IServerConfig,
-  env: ImportMetaEnv
-}
diff --git a/server/utils/initial-store.ts b/server/utils/initial-store.ts
deleted file mode 100644
index cc3a100..0000000
--- a/server/utils/initial-store.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import uniqid from "uniqid";
-
-export type TSecretKey = {
-  key: string;
-  secret: string;
-}
-
-/**
- * Хранилище состояний, с доступом по двойному ключу (паре строк)
- * Доступ к данным одноразовый.
- * Используется для запоминания состояния, с которым рендерилось приложение.
- */
-export default class InitialStore {
-  private items: Map<string, {[secret: string]: unknown}>;
-
-  constructor() {
-    this.items = new Map();
-  }
-
-  makeSecretKey(){
-    const key = uniqid();
-    const secret = uniqid(key);
-    return {key, secret};
-  }
-
-  remember({key, secret}: TSecretKey, state: unknown, time = 15000){
-    this.items.set(key, { [secret]: state });
-    setTimeout(() => {
-      //delete this.items.delete(key);
-    }, time);
-  }
-
-  get({key, secret}: TSecretKey){
-    let result = undefined;
-    if (this.items.has(key)){
-      const record = this.items.get(key);
-      if (record){
-        result = record[secret];
-        this.items.delete(key);
-      }
-    }
-    return result;
-  }
-}
diff --git a/server/utils/loadEnv.ts b/server/utils/loadEnv.ts
deleted file mode 100644
index 9c7056b..0000000
--- a/server/utils/loadEnv.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import typedVariables from 'dotenv-parse-variables';
-import {loadEnv as loadEnvVite} from "vite";
-
-export default function loadEnv(){
-  return {
-    SSR: true,
-    MODE: process.env.NODE_ENV,
-    PROD: !process.env.NODE_ENV || process.env.NODE_ENV === 'production',
-    DEV: Boolean(process.env.NODE_ENV && process.env.NODE_ENV !== 'production'),
-    ...typedVariables(
-      loadEnvVite(process.env.NODE_ENV || 'production', process.cwd(), '')
-    )
-  } as ImportMetaEnv;
-}
diff --git a/server/utils/react-template.ts b/server/utils/react-template.ts
deleted file mode 100644
index c7b4a02..0000000
--- a/server/utils/react-template.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import parse from "html-dom-parser";
-import React, {FunctionComponent, ReactNode} from "react";
-
-export type TAttrMapNames = { [attr: string]: string };
-export type TAttributes = { [attr: string]: string | number | boolean | null };
-export type TDomItems = ReturnType<typeof parse>
-
-/**
- * Словарь переименования атрибутов
- */
-const attrMap: TAttrMapNames = {
-  'xml:lang': 'xmlLang',
-  'http-equiv': 'httpEquiv',
-  'crossorigin': 'crossOrigin',
-  'class': 'className'
-};
-
-/**
- * переименование атрибутов в формат React JSX
- * @param attr
- * @param names
- */
-function attrRenames(attr: TAttributes, names: TAttrMapNames = attrMap) {
-  const keys = Object.keys(attr);
-  const result: TAttributes = {};
-  for (const key of keys) {
-    if (key in names) {
-      result[names[key]] = attr[key];
-    } else {
-      result[key] = attr[key];
-    }
-  }
-  return result;
-}
-
-/**
- * Конвертирует строковый HTML шаблон в react элемент со вставкой в него компонента React
- * @param Component React компонент (функция)
- * @param template HTML шаблон
- * @param place Идентификатор html тега, внутри которого вставить компонент
- * @param props Свойства компоненту
- */
-export default function reactTemplate(Component: FunctionComponent, template: string, place = 'app', props = {}) {
-  const domTree = parse(template.trim().replace(/>\s+</ug, '><'));
-  const scripts: string[] = [];
-  const modules: string[] = [];
-
-  const buildReactElements = (nodes: TDomItems): ReactNode[] => {
-    const result = [];
-    for (const item of nodes) {
-      const children = 'children' in item ? buildReactElements(item.children as TDomItems) : [];
-      if (item.type === 'tag') {
-        if (item.attribs.id === place) children.push(React.createElement(Component, props));
-        result.push(React.createElement(item.name, attrRenames(item.attribs), ...children));
-      } else if (item.type === 'text') {
-        result.push(item.data);
-      } else if (item.type === 'script') {
-        if (children.length) {
-          result.push(React.createElement(item.name, {
-            ...attrRenames(item.attribs),
-            dangerouslySetInnerHTML: {__html: children.join('')}
-          }));
-        } else {
-          if (item.attribs.type === 'module') {
-            modules.push(item.attribs.src);
-          } else {
-            scripts.push(item.attribs.src);
-          }
-        }
-      }
-    }
-    return result;
-  };
-
-  const result = buildReactElements(domTree);
-
-  return {jsx: result.at(0), scripts, modules};
-}
diff --git a/server/utils/stream-helmet.ts b/server/utils/stream-helmet.ts
deleted file mode 100644
index 3805401..0000000
--- a/server/utils/stream-helmet.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import replacestream from 'replacestream';
-import {HelmetServerState} from "react-helmet-async";
-
-type TPipe = <Writable extends NodeJS.WritableStream>(destination: Writable) => Writable;
-
-/**
- * Трансформация потока - вставка мета-тегов перед закрытием </head>
- * @param pipe
- * @param helmet
- * @return {*}
- */
-export default function streamHelmet(pipe: TPipe, helmet: HelmetServerState) {
-  return pipe(
-    replacestream(/<html[^>]*>/ui, `<html ${helmet.htmlAttributes.toString()}>`, {limit: 1})
-  ).pipe(
-    replacestream(/<title>[^<]*<\/title>/ui, helmet.title.toString(), {limit: 1})
-  ).pipe(
-    replacestream('</head>',
-      helmet.meta.toString() +
-      helmet.link.toString() +
-      helmet.script.toString() +
-      helmet.noscript.toString() +
-      helmet.style.toString() +
-      '</head>'
-      , {limit: 1})
-  ).pipe(
-    replacestream(/<body[^>]*>/ui, `<body ${helmet.bodyAttributes.toString()}>`, {limit: 1})
-  );
-}