From 3fd96ef4730fe62b54696b09e1052880afee5822 Mon Sep 17 00:00:00 2001 From: krutoo Date: Thu, 21 Mar 2024 11:10:01 +0500 Subject: [PATCH] #38 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deps: добавлен accepts (patch) - preset/server: перенесена часть провайдеров из preset/node (patch) - preset/server: добавлен провайдер функции рендера jsx в строку (patch) - preset/server: добавлен провайдер обьекта событий ответа (patch) - preset/server: добавлен провайдер формформаттера ответа на запрос страницы (patch) - preset/node-handler: добавлен провайдер событий ответа (patch) - preset/node-handler: провайдер main декомпозирован (patch) - preset/bun-handler: добавлен провайдер событий ответа (patch) - preset/bun-handler: провайдер main декомпозирован (patch) --- package-lock.json | 22 +++- package.json | 4 +- src/preset/bun-handler/index.ts | 14 ++- .../bun-handler/providers/accept-type.ts | 19 ++++ .../providers/fetch-log-handler.ts | 22 ---- .../bun-handler/providers/handler-main.tsx | 101 +++++------------- .../bun-handler/providers/page-helmet.ts | 15 --- src/preset/bun/index.ts | 4 +- src/preset/node-handler/index.ts | 14 ++- .../node-handler/providers/accepts-type.ts | 13 +++ .../node-handler/providers/handler-main.tsx | 85 ++++----------- .../node-handler/providers/response-events.ts | 14 +++ src/preset/node/index.ts | 4 +- .../server/providers/element-to-string.ts | 22 ++++ .../providers/fetch-log-handler.ts | 0 .../server/providers/format-page-response.ts | 70 ++++++++++++ .../providers/known-http-api-hosts.ts | 0 .../providers/page-helmet.ts | 8 +- .../providers/page-render.tsx | 0 .../server/providers/response-events.ts | 13 +++ .../providers/ssr-bridge-server-side.ts | 0 21 files changed, 253 insertions(+), 191 deletions(-) create mode 100644 src/preset/bun-handler/providers/accept-type.ts delete mode 100644 src/preset/bun-handler/providers/fetch-log-handler.ts delete mode 100644 src/preset/bun-handler/providers/page-helmet.ts create mode 100644 src/preset/node-handler/providers/accepts-type.ts create mode 100644 src/preset/node-handler/providers/response-events.ts create mode 100644 src/preset/server/providers/element-to-string.ts rename src/preset/{node-handler => server}/providers/fetch-log-handler.ts (100%) create mode 100644 src/preset/server/providers/format-page-response.ts rename src/preset/{node => server}/providers/known-http-api-hosts.ts (100%) rename src/preset/{node-handler => server}/providers/page-helmet.ts (66%) rename src/preset/{node-handler => server}/providers/page-render.tsx (100%) create mode 100644 src/preset/server/providers/response-events.ts rename src/preset/{node => server}/providers/ssr-bridge-server-side.ts (100%) diff --git a/package-lock.json b/package-lock.json index 4053f94..b212b44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@sentry/browser": "^7.81.0", "@sentry/bun": "^7.81.0", "@sentry/node": "^7.81.0", + "accepts": "^1.3.8", "dotenv": "^16.3.1", "express": "^4.18.2", "jsesc": "^3.0.2", @@ -40,6 +41,7 @@ "@babel/preset-typescript": "^7.22.5", "@sima-land/linters": "^4.0.0", "@testing-library/react": "^14.0.0", + "@types/accepts": "^1.3.7", "@types/express": "^4.17.17", "@types/jest": "^28.1.7", "@types/jsesc": "^3.0.1", @@ -58,7 +60,7 @@ "whatwg-fetch": "^3.6.17" }, "engines": { - "node": ">=16.15.1" + "node": ">=18.0.0" }, "peerDependencies": { "@reduxjs/toolkit": "^1.9.5", @@ -4499,6 +4501,15 @@ "node": ">= 10" } }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", @@ -19054,6 +19065,15 @@ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true }, + "@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", diff --git a/package.json b/package.json index a6c62b9..092a4de 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "www.sima-land.ru team", "license": "Apache-2.0", "engines": { - "node": ">=16.15.1" + "node": ">=18.0.0" }, "scripts": { "prepare": "husky install", @@ -27,6 +27,7 @@ "@babel/preset-typescript": "^7.22.5", "@sima-land/linters": "^4.0.0", "@testing-library/react": "^14.0.0", + "@types/accepts": "^1.3.7", "@types/express": "^4.17.17", "@types/jest": "^28.1.7", "@types/jsesc": "^3.0.1", @@ -59,6 +60,7 @@ "@sentry/browser": "^7.81.0", "@sentry/bun": "^7.81.0", "@sentry/node": "^7.81.0", + "accepts": "^1.3.8", "dotenv": "^16.3.1", "express": "^4.18.2", "jsesc": "^3.0.2", diff --git a/src/preset/bun-handler/index.ts b/src/preset/bun-handler/index.ts index 29cdf6a..719399b 100644 --- a/src/preset/bun-handler/index.ts +++ b/src/preset/bun-handler/index.ts @@ -5,14 +5,18 @@ import { PresetTuner } from '../isomorphic'; import { provideAbortController } from '../isomorphic/providers/abort-controller'; import { provideFetch } from '../isomorphic/providers/fetch'; import { provideReduxMiddlewareSaga } from '../isomorphic/providers/redux-middleware-saga'; -import { providePageRender } from '../node-handler/providers/page-render'; -import { provideFetchLogHandler } from './providers/fetch-log-handler'; +import { providePageRender } from '../server/providers/page-render'; +import { provideFetchLogHandler } from '../server/providers/fetch-log-handler'; import { provideFetchMiddleware } from './providers/fetch-middleware'; import { provideHandlerMain } from './providers/handler-main'; -import { providePageHelmet } from './providers/page-helmet'; +import { providePageHelmet } from '../server/providers/page-helmet'; import { provideSpecificParams } from './providers/specific-params'; import { provideCookieStore } from './providers/cookie-store'; import { SpecificExtras } from '../server/utils/specific-extras'; +import { provideElementToString } from '../server/providers/element-to-string'; +import { provideFormatPageResponse } from '../server/providers/format-page-response'; +import { provideAcceptType } from './providers/accept-type'; +import { provideResponseEvents } from '../server/providers/response-events'; /** * Возвращает preset с зависимостями для формирования обработчика входящего http-запроса. @@ -32,11 +36,15 @@ export function PresetBunHandler(customize?: PresetTuner) { // handler preset.set(KnownToken.Http.Handler.main, provideHandlerMain); + preset.set(KnownToken.Http.Handler.Request.acceptType, provideAcceptType); preset.set(KnownToken.Http.Handler.Request.specificParams, provideSpecificParams); + preset.set(KnownToken.Http.Handler.Response.events, provideResponseEvents); preset.set(KnownToken.Http.Handler.Response.specificExtras, () => new SpecificExtras()); preset.set(KnownToken.Http.Handler.Page.assets, () => ({ js: '', css: '' })); preset.set(KnownToken.Http.Handler.Page.helmet, providePageHelmet); preset.set(KnownToken.Http.Handler.Page.render, providePageRender); + preset.set(KnownToken.Http.Handler.Page.elementToString, provideElementToString); + preset.set(KnownToken.Http.Handler.Page.formatResponse, provideFormatPageResponse); // redux saga preset.set(KnownToken.Redux.Middleware.saga, provideReduxMiddlewareSaga); diff --git a/src/preset/bun-handler/providers/accept-type.ts b/src/preset/bun-handler/providers/accept-type.ts new file mode 100644 index 0000000..22fc1d3 --- /dev/null +++ b/src/preset/bun-handler/providers/accept-type.ts @@ -0,0 +1,19 @@ +import type { Resolve } from '../../../di'; +import { KnownToken } from '../../../tokens'; +import accepts from 'accepts'; + +/** + * Провайдер функции, которая определяет возможные типы ответа и их приоритет. + * @param resolve Resolve. + * @return Функция. + */ +export function provideAcceptType(resolve: Resolve) { + const context = resolve(KnownToken.Http.Handler.context); + + // @todo опасное место, будем решать как исправлять по итогам https://github.com/jshttp/accepts/issues/30 + const accept = accepts({ + headers: Object.fromEntries(context.request.headers.entries()), + } as any); + + return accept.type.bind(accept); +} diff --git a/src/preset/bun-handler/providers/fetch-log-handler.ts b/src/preset/bun-handler/providers/fetch-log-handler.ts deleted file mode 100644 index 2b004e3..0000000 --- a/src/preset/bun-handler/providers/fetch-log-handler.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */ -import { Resolve } from '../../../di'; -import { KnownToken } from '../../../tokens'; -import { LogHandler, LogHandlerFactory } from '../../../http'; -import { FetchLogging } from '../../isomorphic/utils/fetch-logging'; - -/** - * Провайдер обработчика логирования axios. - * @param resolve Функция для получения зависимости по токену. - * @return Обработчик логирования. - */ -export function provideFetchLogHandler(resolve: Resolve): LogHandler | LogHandlerFactory { - const logger = resolve(KnownToken.logger); - const abortController = resolve(KnownToken.Http.Fetch.abortController); - - const logHandler = new FetchLogging(logger); - - // ВАЖНО: отключаем логирование если запрос прерван - logHandler.disabled = () => abortController.signal.aborted; - - return logHandler; -} diff --git a/src/preset/bun-handler/providers/handler-main.tsx b/src/preset/bun-handler/providers/handler-main.tsx index 3859a84..0e275f3 100644 --- a/src/preset/bun-handler/providers/handler-main.tsx +++ b/src/preset/bun-handler/providers/handler-main.tsx @@ -1,12 +1,9 @@ /* eslint-disable require-jsdoc, jsdoc/require-jsdoc */ -import { renderToString } from 'react-dom/server'; import { Resolve } from '../../../di'; import { KnownToken } from '../../../tokens'; import { ResponseError, applyMiddleware } from '../../../http'; -import { PageAssets } from '../../isomorphic/types'; -import { PAGE_HANDLER_EVENT_TYPE } from '../../server/constants'; -import { getPageResponseFormat } from '../../server/utils/get-page-response-format'; import { HelmetContext } from '../../server/utils/regular-helmet'; +import { LogLevel } from '../../../log'; export function provideHandlerMain(resolve: Resolve) { const config = resolve(KnownToken.Config.base); @@ -16,7 +13,7 @@ export function provideHandlerMain(resolve: Resolve) { const extras = resolve(KnownToken.Http.Handler.Response.specificExtras); const Helmet = resolve(KnownToken.Http.Handler.Page.helmet); const abortController = resolve(KnownToken.Http.Fetch.abortController); - const context = resolve(KnownToken.Http.Handler.context); + const formatResponse = resolve(KnownToken.Http.Handler.Page.formatResponse); // @todo https://github.com/sima-land/isomorph/issues/69 // const cookieStore = resolve(KnownToken.Http.Fetch.cookieStore); @@ -27,65 +24,7 @@ export function provideHandlerMain(resolve: Resolve) { const getAssets = typeof assetsInit === 'function' ? assetsInit : () => assetsInit; - const elementToString = (element: JSX.Element) => { - context.events.dispatchEvent(new Event(PAGE_HANDLER_EVENT_TYPE.renderStart)); - const result = renderToString(element); - context.events.dispatchEvent(new Event(PAGE_HANDLER_EVENT_TYPE.renderFinish)); - - return result; - }; - - const getResponseHTML = (jsx: React.JSX.Element, assets: PageAssets, meta: unknown) => { - const headers = new Headers(); - - headers.set('content-type', 'text/html'); - headers.set('simaland-bundle-js', assets.js); - headers.set('simaland-bundle-css', assets.css); - - if (assets.criticalJs) { - headers.set('simaland-critical-js', assets.criticalJs); - } - - if (assets.criticalCss) { - headers.set('simaland-critical-css', assets.criticalCss); - } - - if (meta) { - headers.set('simaland-meta', JSON.stringify(meta)); - } - - // ВАЖНО: DOCTYPE обязательно нужен так как влияет на то как браузер будет парсить html/css - // ВАЖНО: DOCTYPE нужен только когда отдаем полноценную страницу - if (config.env === 'development') { - return new Response(`${elementToString(jsx)}`, { - headers, - }); - } else { - return new Response(elementToString(jsx), { - headers, - }); - } - }; - - const getResponseJSON = (jsx: React.JSX.Element, assets: PageAssets, meta: unknown) => { - const headers = new Headers(); - - headers.set('content-type', 'application/json'); - - return new Response( - JSON.stringify({ - markup: elementToString(jsx), - bundle_js: assets.js, - bundle_css: assets.css, - critical_js: assets.criticalJs, - critical_css: assets.criticalCss, - meta, - }), - { headers }, - ); - }; - - const handler = async (request: Request): Promise => { + const handler = async (): Promise => { try { const assets = await getAssets(); const meta = extras.getMeta(); @@ -96,33 +35,43 @@ export function provideHandlerMain(resolve: Resolve) { ); - switch (getPageResponseFormat(request)) { - case 'html': { - return getResponseHTML(jsx, assets, meta); - } - case 'json': { - return getResponseJSON(jsx, assets, meta); - } - } + const { body, headers } = await formatResponse(jsx, assets, meta); + + return new Response(body, { headers }); } catch (error) { + let logLevel: LogLevel | null = 'error'; let message: string; let statusCode = 500; // по умолчанию, если на этапе подготовки страницы что-то не так, отдаем 500 + let redirectLocation: string | null = null; if (error instanceof Error) { message = error.message; if (error instanceof ResponseError) { statusCode = error.statusCode; + redirectLocation = error.redirectLocation; + logLevel = error.logLevel; } } else { message = String(error); } - logger.error(error); + if (logLevel && logger[logLevel]) { + logger[logLevel](error); + } - return new Response(message, { - status: statusCode, - }); + if (statusCode > 299 && statusCode < 400 && redirectLocation) { + return new Response(null, { + status: statusCode, + headers: { + Location: redirectLocation, + }, + }); + } else { + return new Response(message, { + status: statusCode, + }); + } } }; diff --git a/src/preset/bun-handler/providers/page-helmet.ts b/src/preset/bun-handler/providers/page-helmet.ts deleted file mode 100644 index 6ff4195..0000000 --- a/src/preset/bun-handler/providers/page-helmet.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */ -import { Resolve } from '../../../di'; -import { KnownToken } from '../../../tokens'; -import { Fragment } from 'react'; -import { RegularHelmet } from '../../server/utils/regular-helmet'; -import { getPageResponseFormat } from '../../server/utils/get-page-response-format'; - -export function providePageHelmet(resolve: Resolve) { - const config = resolve(KnownToken.Config.base); - const { request } = resolve(KnownToken.Http.Handler.context); - - return config.env === 'development' && getPageResponseFormat(request) === 'html' - ? RegularHelmet - : Fragment; -} diff --git a/src/preset/bun/index.ts b/src/preset/bun/index.ts index f977065..414dec2 100644 --- a/src/preset/bun/index.ts +++ b/src/preset/bun/index.ts @@ -3,8 +3,8 @@ import { KnownToken } from '../../tokens'; import { PresetTuner } from '../isomorphic'; import { provideBaseConfig } from '../isomorphic/providers/base-config'; import { provideFetch } from '../isomorphic/providers/fetch'; -import { provideKnownHttpApiHosts } from '../node/providers/known-http-api-hosts'; -import { provideSsrBridgeServerSide } from '../node/providers/ssr-bridge-server-side'; +import { provideKnownHttpApiHosts } from '../server/providers/known-http-api-hosts'; +import { provideSsrBridgeServerSide } from '../server/providers/ssr-bridge-server-side'; import { provideConfigSource } from './providers/config-source'; import { provideLogger } from './providers/logger'; import { provideServe } from './providers/serve'; diff --git a/src/preset/node-handler/index.ts b/src/preset/node-handler/index.ts index 5ed186b..d466826 100644 --- a/src/preset/node-handler/index.ts +++ b/src/preset/node-handler/index.ts @@ -4,16 +4,20 @@ import { provideReduxMiddlewareSaga } from '../isomorphic/providers/redux-middle import { provideFetch } from '../isomorphic/providers/fetch'; import { provideAbortController } from '../isomorphic/providers/abort-controller'; import { PresetTuner } from '../isomorphic/types'; +import { provideFormatPageResponse } from '../server/providers/format-page-response'; +import { provideElementToString } from '../server/providers/element-to-string'; import { provideAxiosMiddleware } from './providers/axios-middleware'; import { provideAxiosLogHandler } from './providers/axios-log-handler'; import { provideHandlerMain } from './providers/handler-main'; import { provideSpecificParams } from './providers/specific-params'; -import { providePageHelmet } from './providers/page-helmet'; -import { providePageRender } from './providers/page-render'; +import { providePageHelmet } from '../server/providers/page-helmet'; +import { providePageRender } from '../server/providers/page-render'; import { provideFetchMiddleware } from './providers/fetch-middleware'; -import { provideFetchLogHandler } from './providers/fetch-log-handler'; +import { provideFetchLogHandler } from '../server/providers/fetch-log-handler'; import { provideCookieStore } from './providers/cookie-store'; import { SpecificExtras } from '../server/utils/specific-extras'; +import { provideAcceptType } from './providers/accepts-type'; +import { provideResponseEvents } from './providers/response-events'; /** * Возвращает preset с зависимостями по умолчанию для работы в рамках ответа на http-запрос. @@ -43,11 +47,15 @@ export function PresetHandler(customize?: PresetTuner): Preset { preset.set(KnownToken.ExpressHandler.main, provideHandlerMain); // http handler + preset.set(KnownToken.Http.Handler.Request.acceptType, provideAcceptType); preset.set(KnownToken.Http.Handler.Request.specificParams, provideSpecificParams); preset.set(KnownToken.Http.Handler.Response.specificExtras, () => new SpecificExtras()); + preset.set(KnownToken.Http.Handler.Response.events, provideResponseEvents); preset.set(KnownToken.Http.Handler.Page.assets, () => ({ js: '', css: '' })); preset.set(KnownToken.Http.Handler.Page.helmet, providePageHelmet); preset.set(KnownToken.Http.Handler.Page.render, providePageRender); + preset.set(KnownToken.Http.Handler.Page.elementToString, provideElementToString); + preset.set(KnownToken.Http.Handler.Page.formatResponse, provideFormatPageResponse); if (customize) { customize({ override: preset.set.bind(preset) }); diff --git a/src/preset/node-handler/providers/accepts-type.ts b/src/preset/node-handler/providers/accepts-type.ts new file mode 100644 index 0000000..57dac3a --- /dev/null +++ b/src/preset/node-handler/providers/accepts-type.ts @@ -0,0 +1,13 @@ +import type { Resolve } from '../../../di'; +import { KnownToken } from '../../../tokens'; + +/** + * Провайдер функции, которая определяет возможные типы ответа и их приоритет. + * @param resolve Resolve. + * @return Функция. + */ +export function provideAcceptType(resolve: Resolve) { + const context = resolve(KnownToken.ExpressHandler.context); + + return context.req.accepts.bind(context.req); +} diff --git a/src/preset/node-handler/providers/handler-main.tsx b/src/preset/node-handler/providers/handler-main.tsx index 37f254f..a7bb80f 100644 --- a/src/preset/node-handler/providers/handler-main.tsx +++ b/src/preset/node-handler/providers/handler-main.tsx @@ -1,11 +1,8 @@ import type { Resolve } from '../../../di'; import { KnownToken } from '../../../tokens'; -import { renderToString } from 'react-dom/server'; -import { PAGE_HANDLER_EVENT_TYPE } from '../../server'; import { HelmetContext } from '../../server/utils/regular-helmet'; -import { getPageResponseFormat } from '../../node/utils/get-page-response-format'; -import { ConventionalJson } from '../../isomorphic'; import { ResponseError } from '../../../http'; +import { LogLevel } from '../../../log'; /** * Провайдер главной функции обработчика входящего http-запроса. @@ -15,28 +12,16 @@ import { ResponseError } from '../../../http'; export function provideHandlerMain(resolve: Resolve): VoidFunction { const config = resolve(KnownToken.Config.base); const logger = resolve(KnownToken.logger); - const assetsInit = resolve(KnownToken.Http.Handler.Page.assets); + const context = resolve(KnownToken.ExpressHandler.context); const render = resolve(KnownToken.Http.Handler.Page.render); + const assetsInit = resolve(KnownToken.Http.Handler.Page.assets); const extras = resolve(KnownToken.Http.Handler.Response.specificExtras); const Helmet = resolve(KnownToken.Http.Handler.Page.helmet); - const { req, res } = resolve(KnownToken.ExpressHandler.context); const abortController = resolve(KnownToken.Http.Fetch.abortController); + const format = resolve(KnownToken.Http.Handler.Page.formatResponse); const getAssets = typeof assetsInit === 'function' ? assetsInit : () => assetsInit; - /** - * Рендер JSX-элемента в строку. - * @param element Элемент. - * @return Строка. - */ - const elementToString = (element: JSX.Element) => { - res.emit(PAGE_HANDLER_EVENT_TYPE.renderStart); - const result = renderToString(element); - res.emit(PAGE_HANDLER_EVENT_TYPE.renderFinish); - - return result; - }; - // @todo https://github.com/sima-land/isomorph/issues/69 // const cookieStore = resolve(KnownToken.Http.Fetch.cookieStore); // cookieStore.subscribe(setCookieList => { @@ -52,67 +37,43 @@ export function provideHandlerMain(resolve: Resolve): VoidFunction { const assets = await getAssets(); const meta = extras.getMeta(); - const jsx = ( + const { body, headers } = await format( {await render()} - + , + assets, + meta, ); - switch (getPageResponseFormat(req)) { - case 'html': { - res.setHeader('simaland-bundle-js', assets.js); - res.setHeader('simaland-bundle-css', assets.css); - - if (assets.criticalJs) { - res.setHeader('simaland-critical-js', assets.criticalJs); - } - - if (assets.criticalCss) { - res.setHeader('simaland-critical-css', assets.criticalCss); - } - - if (meta) { - res.setHeader('simaland-meta', JSON.stringify(meta)); - } - - // ВАЖНО: DOCTYPE обязательно нужен так как влияет на то как браузер будет парсить html/css - // ВАЖНО: DOCTYPE нужен только когда отдаем полноценную страницу - if (config.env === 'development') { - res.send(`${elementToString(jsx)}`); - } else { - res.send(elementToString(jsx)); - } - break; - } - - case 'json': { - res.json({ - markup: elementToString(jsx), - bundle_js: assets.js, - bundle_css: assets.css, - critical_js: assets.criticalJs, - critical_css: assets.criticalCss, - meta, - } satisfies ConventionalJson); - break; - } - } + headers.forEach((hValue, hName) => context.res.setHeader(hName, hValue)); + context.res.send(body); } catch (error) { + let logLevel: LogLevel | null = 'error'; let message: string; let statusCode = 500; // по умолчанию, если на этапе подготовки страницы что-то не так, отдаем 500 + let redirectLocation: string | null = null; if (error instanceof Error) { message = error.message; if (error instanceof ResponseError) { statusCode = error.statusCode; + redirectLocation = error.redirectLocation; + logLevel = error.logLevel; } } else { message = String(error); } - res.status(statusCode).send(message); - logger.error(error); + if (statusCode > 299 && statusCode < 400 && redirectLocation) { + context.res.redirect(statusCode, redirectLocation); + } else { + context.res.status(statusCode).send(message); + } + + if (logLevel && logger[logLevel]) { + logger[logLevel](error); + } } // ВАЖНО: прерываем исходящие в рамках обработчика http-запросы diff --git a/src/preset/node-handler/providers/response-events.ts b/src/preset/node-handler/providers/response-events.ts new file mode 100644 index 0000000..fdcfd16 --- /dev/null +++ b/src/preset/node-handler/providers/response-events.ts @@ -0,0 +1,14 @@ +import type { Resolve } from '../../../di'; +import { KnownToken } from '../../../tokens'; +import { EmitterAsTarget } from '../../node/utils/emitter-as-target'; + +/** + * Провайдер объекта событий ответа. + * @param resolve Resolve. + * @return Объект событий. + */ +export function provideResponseEvents(resolve: Resolve): EventTarget { + const context = resolve(KnownToken.ExpressHandler.context); + + return new EmitterAsTarget(context.res); +} diff --git a/src/preset/node/index.ts b/src/preset/node/index.ts index e29c27a..98e115a 100644 --- a/src/preset/node/index.ts +++ b/src/preset/node/index.ts @@ -12,11 +12,11 @@ import { provideExpressLogMiddleware } from './providers/express-log-middleware' import { provideExpressMetricsMiddleware } from './providers/express-metrics-middleware'; import { provideExpressRequestMiddleware } from './providers/express-request-middleware'; import { provideExpressTracingMiddleware } from './providers/express-tracing-middleware'; -import { provideKnownHttpApiHosts } from './providers/known-http-api-hosts'; +import { provideKnownHttpApiHosts } from '../server/providers/known-http-api-hosts'; import { provideLogger } from './providers/logger'; import { provideMetricsHttpApp } from './providers/metrics-http-app'; import { provideSpanExporter } from './providers/span-exporter'; -import { provideSsrBridgeServerSide } from './providers/ssr-bridge-server-side'; +import { provideSsrBridgeServerSide } from '../server/providers/ssr-bridge-server-side'; import { provideTracer } from './providers/tracer'; import { provideTracerProvider } from './providers/tracer-provider'; import { provideTracerProviderResource } from './providers/tracer-provider-resource'; diff --git a/src/preset/server/providers/element-to-string.ts b/src/preset/server/providers/element-to-string.ts new file mode 100644 index 0000000..525c5bf --- /dev/null +++ b/src/preset/server/providers/element-to-string.ts @@ -0,0 +1,22 @@ +import type { Resolve } from '../../../di'; +import type { JSX } from 'react'; +import { KnownToken } from '../../../tokens'; +import { renderToString } from 'react-dom/server'; +import { PAGE_HANDLER_EVENT_TYPE } from '../constants'; + +/** + * Провайдера функции рендера элемента в строку. + * @param resolve Resolve. + * @return Функция рендера. + */ +export function provideElementToString(resolve: Resolve): (jsx: JSX.Element) => string { + const events = resolve(KnownToken.Http.Handler.Response.events); + + return jsx => { + events.dispatchEvent(new Event(PAGE_HANDLER_EVENT_TYPE.renderStart)); + const result = renderToString(jsx); + events.dispatchEvent(new Event(PAGE_HANDLER_EVENT_TYPE.renderFinish)); + + return result; + }; +} diff --git a/src/preset/node-handler/providers/fetch-log-handler.ts b/src/preset/server/providers/fetch-log-handler.ts similarity index 100% rename from src/preset/node-handler/providers/fetch-log-handler.ts rename to src/preset/server/providers/fetch-log-handler.ts diff --git a/src/preset/server/providers/format-page-response.ts b/src/preset/server/providers/format-page-response.ts new file mode 100644 index 0000000..06f83c9 --- /dev/null +++ b/src/preset/server/providers/format-page-response.ts @@ -0,0 +1,70 @@ +import type { Resolve } from '../../../di'; +import type { ConventionalJson } from '../../isomorphic'; +import type { PageResponseFormatter } from '../types'; +import { KnownToken } from '../../../tokens'; +import { PAGE_FORMAT_PRIORITY } from '../constants'; + +/** + * Провайдер функции форматирования ответа. + * Функция форматирования вернёт данные ответа на запрос страницы в нужном формате в зависимости от заголовка Accept. + * @param resolve Резолвер. + * @return Функция форматирования. + */ +export function provideFormatPageResponse(resolve: Resolve): PageResponseFormatter { + const config = resolve(KnownToken.Config.base); + const acceptType = resolve(KnownToken.Http.Handler.Request.acceptType); + const elementToString = resolve(KnownToken.Http.Handler.Page.elementToString); + + return async (jsx, assets, meta) => { + const headers = new Headers(); + let body: string; + + switch (acceptType(PAGE_FORMAT_PRIORITY)) { + case 'json': { + headers.set('content-type', 'application/json'); + + body = JSON.stringify({ + markup: await elementToString(jsx), + bundle_js: assets.js, + bundle_css: assets.css, + critical_js: assets.criticalJs, + critical_css: assets.criticalCss, + meta, + } satisfies ConventionalJson); + + break; + } + + case 'html': + default: { + headers.set('content-type', 'text/html'); + headers.set('simaland-bundle-js', assets.js); + headers.set('simaland-bundle-css', assets.css); + + if (assets.criticalJs) { + headers.set('simaland-critical-js', assets.criticalJs); + } + + if (assets.criticalCss) { + headers.set('simaland-critical-css', assets.criticalCss); + } + + if (meta) { + headers.set('simaland-meta', JSON.stringify(meta)); + } + + // ВАЖНО: DOCTYPE обязательно нужен так как влияет на то как браузер будет парсить html/css + // ВАЖНО: DOCTYPE нужен только когда отдаем полноценную страницу + if (config.env === 'development') { + body = `${await elementToString(jsx)}`; + } else { + body = await elementToString(jsx); + } + + break; + } + } + + return { body, headers }; + }; +} diff --git a/src/preset/node/providers/known-http-api-hosts.ts b/src/preset/server/providers/known-http-api-hosts.ts similarity index 100% rename from src/preset/node/providers/known-http-api-hosts.ts rename to src/preset/server/providers/known-http-api-hosts.ts diff --git a/src/preset/node-handler/providers/page-helmet.ts b/src/preset/server/providers/page-helmet.ts similarity index 66% rename from src/preset/node-handler/providers/page-helmet.ts rename to src/preset/server/providers/page-helmet.ts index 958629c..e8d4796 100644 --- a/src/preset/node-handler/providers/page-helmet.ts +++ b/src/preset/server/providers/page-helmet.ts @@ -1,8 +1,8 @@ import { Fragment } from 'react'; import { Resolve } from '../../../di'; import { KnownToken } from '../../../tokens'; -import { getPageResponseFormat } from '../../node/utils/get-page-response-format'; -import { RegularHelmet } from '../../server/utils/regular-helmet'; +import { RegularHelmet } from '../utils/regular-helmet'; +import { PAGE_FORMAT_PRIORITY } from '../constants'; /** * Провайдер helmet-компонента. Этот компонент является контейнером для результата render-функции. @@ -11,9 +11,9 @@ import { RegularHelmet } from '../../server/utils/regular-helmet'; */ export function providePageHelmet(resolve: Resolve) { const config = resolve(KnownToken.Config.base); - const { req } = resolve(KnownToken.ExpressHandler.context); + const acceptType = resolve(KnownToken.Http.Handler.Request.acceptType); - return config.env === 'development' && getPageResponseFormat(req) === 'html' + return config.env === 'development' && acceptType(PAGE_FORMAT_PRIORITY) === 'html' ? RegularHelmet : Fragment; } diff --git a/src/preset/node-handler/providers/page-render.tsx b/src/preset/server/providers/page-render.tsx similarity index 100% rename from src/preset/node-handler/providers/page-render.tsx rename to src/preset/server/providers/page-render.tsx diff --git a/src/preset/server/providers/response-events.ts b/src/preset/server/providers/response-events.ts new file mode 100644 index 0000000..85b52fc --- /dev/null +++ b/src/preset/server/providers/response-events.ts @@ -0,0 +1,13 @@ +import type { Resolve } from '../../../di'; +import { KnownToken } from '../../../tokens'; + +/** + * Провайдер объекта подписки на события и вызова событий ответа. + * @param resolve Resolve. + * @return EventTarget. + */ +export function provideResponseEvents(resolve: Resolve): EventTarget { + const context = resolve(KnownToken.Http.Handler.context); + + return context.events; +} diff --git a/src/preset/node/providers/ssr-bridge-server-side.ts b/src/preset/server/providers/ssr-bridge-server-side.ts similarity index 100% rename from src/preset/node/providers/ssr-bridge-server-side.ts rename to src/preset/server/providers/ssr-bridge-server-side.ts