From 5c421fc8ea586fcf6e26a795f89a9aa567c83d29 Mon Sep 17 00:00:00 2001 From: krutoo Date: Mon, 20 Mar 2023 12:56:48 +0500 Subject: [PATCH] #38 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - examples/server: проведен рефакторинг - examples/server: апи заменен на JSONPlaceholder --- examples/server/.eslintrc.js | 9 --- examples/server/package-lock.json | 66 +++++++--------- examples/server/package.json | 3 +- examples/server/src/app/index.ts | 32 ++++++++ .../server/src/components/desktop.module.css | 33 -------- examples/server/src/components/desktop.tsx | 43 ---------- examples/server/src/di/app/desktop.tsx | 78 ------------------- examples/server/src/di/app/mobile.tsx | 36 --------- examples/server/src/di/app/root.ts | 59 -------------- examples/server/src/di/apps/main.ts | 62 +++++++++++++++ examples/server/src/di/apps/posts.tsx | 54 +++++++++++++ examples/server/src/di/apps/users.tsx | 54 +++++++++++++ examples/server/src/di/tokens.ts | 22 +++--- examples/server/src/index.ts | 17 ++-- examples/server/src/pages/posts/index.tsx | 53 +++++++++++++ examples/server/src/pages/users/index.tsx | 53 +++++++++++++ examples/server/src/reducers/app.ts | 55 ------------- examples/server/src/sagas/index.ts | 36 --------- examples/server/src/types.ts | 22 ------ examples/server/tsconfig.json | 5 +- 20 files changed, 358 insertions(+), 434 deletions(-) create mode 100644 examples/server/src/app/index.ts delete mode 100644 examples/server/src/components/desktop.module.css delete mode 100644 examples/server/src/components/desktop.tsx delete mode 100644 examples/server/src/di/app/desktop.tsx delete mode 100644 examples/server/src/di/app/mobile.tsx delete mode 100644 examples/server/src/di/app/root.ts create mode 100644 examples/server/src/di/apps/main.ts create mode 100644 examples/server/src/di/apps/posts.tsx create mode 100644 examples/server/src/di/apps/users.tsx create mode 100644 examples/server/src/pages/posts/index.tsx create mode 100644 examples/server/src/pages/users/index.tsx delete mode 100644 examples/server/src/reducers/app.ts delete mode 100644 examples/server/src/sagas/index.ts delete mode 100644 examples/server/src/types.ts diff --git a/examples/server/.eslintrc.js b/examples/server/.eslintrc.js index 626ae44..8af948e 100644 --- a/examples/server/.eslintrc.js +++ b/examples/server/.eslintrc.js @@ -7,14 +7,5 @@ module.exports = { 'jsdoc/require-jsdoc': 'off', }, }, - - // allow use only typed-redux-saga/macro - { - files: ['./**/*.{ts,tsx}'], - rules: { - '@jambit/typed-redux-saga/use-typed-effects': ['error', 'macro'], - '@jambit/typed-redux-saga/delegate-effects': 'error', - }, - }, ], }; diff --git a/examples/server/package-lock.json b/examples/server/package-lock.json index 02af5a3..3fe8a5d 100644 --- a/examples/server/package-lock.json +++ b/examples/server/package-lock.json @@ -20,7 +20,6 @@ "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", - "@jambit/eslint-plugin-typed-redux-saga": "^0.4.0", "@types/react": "^17.0.40", "@types/react-dom": "^17.0.13", "babel-loader": "^9.1.2", @@ -2022,12 +2021,6 @@ "node": ">=10.0.0" } }, - "node_modules/@jambit/eslint-plugin-typed-redux-saga": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@jambit/eslint-plugin-typed-redux-saga/-/eslint-plugin-typed-redux-saga-0.4.0.tgz", - "integrity": "sha512-toReLCgaBZ6N0gbq6QTKUCkDqGS4yi0RV16HPLJ9rz8H1TE78KwLHAGhW3XLqyVn+DbcuL7+89RkQse8T5oj8g==", - "dev": true - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -3214,10 +3207,13 @@ "dev": true }, "node_modules/fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", - "dev": true + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } }, "node_modules/fill-range": { "version": "7.0.1", @@ -3477,9 +3473,9 @@ } }, "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -4109,9 +4105,9 @@ } }, "node_modules/postcss": { - "version": "8.4.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", - "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "dev": true, "funding": [ { @@ -4192,9 +4188,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -6372,12 +6368,6 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, - "@jambit/eslint-plugin-typed-redux-saga": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@jambit/eslint-plugin-typed-redux-saga/-/eslint-plugin-typed-redux-saga-0.4.0.tgz", - "integrity": "sha512-toReLCgaBZ6N0gbq6QTKUCkDqGS4yi0RV16HPLJ9rz8H1TE78KwLHAGhW3XLqyVn+DbcuL7+89RkQse8T5oj8g==", - "dev": true - }, "@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -7334,9 +7324,9 @@ "dev": true }, "fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true }, "fill-range": { @@ -7525,9 +7515,9 @@ } }, "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "dev": true, "requires": { "has": "^1.0.3" @@ -7980,9 +7970,9 @@ } }, "postcss": { - "version": "8.4.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", - "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "dev": true, "requires": { "nanoid": "^3.3.4", @@ -8027,9 +8017,9 @@ } }, "postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", "dev": true, "requires": { "cssesc": "^3.0.0", diff --git a/examples/server/package.json b/examples/server/package.json index b926227..f54a035 100644 --- a/examples/server/package.json +++ b/examples/server/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "NODE_ENV='development' webpack watch", "start": "NODE_ENV='development' node ./dist/index.js", - "lint": "eslint --cache ./src --ext .js,.jsx,.ts,.tsx", + "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", "type-check": "tsc -p . --noEmit" }, "author": "www.sima-land.ru team", @@ -16,7 +16,6 @@ "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", - "@jambit/eslint-plugin-typed-redux-saga": "^0.4.0", "@types/react": "^17.0.40", "@types/react-dom": "^17.0.13", "babel-loader": "^9.1.2", diff --git a/examples/server/src/app/index.ts b/examples/server/src/app/index.ts new file mode 100644 index 0000000..11f07d7 --- /dev/null +++ b/examples/server/src/app/index.ts @@ -0,0 +1,32 @@ +import type { BaseConfig } from '@sima-land/isomorph/config'; +import type { SauceResponse } from '@sima-land/isomorph/http-client/sauce'; + +export interface AppConfig extends BaseConfig { + httpPort: { + main: number; + metrics: number; + }; +} + +export interface SagaDeps { + api: HttpApi; +} + +export interface HttpApi { + getUsers(): Promise>; + getPosts(): Promise>; +} + +export interface User { + id: number; + name: string; + username: string; + email: string; +} + +export interface Post { + userId: number; + id: number; + title: string; + body: string; +} diff --git a/examples/server/src/components/desktop.module.css b/examples/server/src/components/desktop.module.css deleted file mode 100644 index 8a45e12..0000000 --- a/examples/server/src/components/desktop.module.css +++ /dev/null @@ -1,33 +0,0 @@ -* { - margin: 0; - padding: 0; - line-height: inherit; - font: inherit; - box-sizing: border-box; -} - -li { - list-style-position: inside; -} - -.root { - margin: 0 auto; - max-width: 1024px; - display: flex; - gap: 32px; - padding: 32px; - flex-wrap: wrap; -} - -.card { - padding: 24px; - border-radius: 16px; - box-shadow: inset 0 0 0 2px #eee; - width: calc(50% - 32px); -} - -.title { - margin-bottom: 16px; - font-size: 24px; - font-weight: bold; -} diff --git a/examples/server/src/components/desktop.tsx b/examples/server/src/components/desktop.tsx deleted file mode 100644 index 9bbdc24..0000000 --- a/examples/server/src/components/desktop.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { ReactNode } from 'react'; -import { useSelector } from 'react-redux'; -import { selectors } from '../reducers/app'; -import styles from './desktop.module.css'; - -function Card({ title, children }: { title?: string; children: ReactNode }) { - return ( -
- {title &&
{title}
} - {children} -
- ); -} - -export function DesktopApp() { - const user = useSelector(selectors.user); - const currencies = useSelector(selectors.currencies); - - return ( -
- - {user.data && user.status === 'success' && ( -
    -
  • id: {user.data.id}
  • -
  • name: {user.data.name}
  • -
- )} - {user.status === 'failure' && user.error} -
- - - {currencies.data && currencies.status === 'success' && ( -
    - {currencies.data.map((currency: any) => ( -
  • {currency.name}
  • - ))} -
- )} - {currencies.status === 'failure' && currencies.error} -
-
- ); -} diff --git a/examples/server/src/di/app/desktop.tsx b/examples/server/src/di/app/desktop.tsx deleted file mode 100644 index ee9a883..0000000 --- a/examples/server/src/di/app/desktop.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import type { PageAssets } from '@sima-land/isomorph/http-server/types'; -import { createApplication, Resolve } from '@sima-land/isomorph/di'; -import { PresetResponse } from '@sima-land/isomorph/preset/node/response'; -import { KnownToken } from '@sima-land/isomorph/tokens'; -import { TOKEN } from '../tokens'; -import { Api } from '../../types'; -import { sauce } from '@sima-land/isomorph/http-client/sauce'; -import { Provider } from 'react-redux'; -import { GlobalDataScript } from '@sima-land/isomorph/utils/ssr'; -import { DesktopApp as Desktop } from '../../components/desktop'; -import { reducer } from '../../reducers/app'; -import { configureStore } from '@reduxjs/toolkit'; -import { rootSaga } from '../../sagas'; - -export function DesktopApp() { - const app = createApplication(); - - app.preset(PresetResponse()); - - app.bind(TOKEN.Response.api).toProvider(provideApi); - app.bind(KnownToken.Response.assets).toProvider(provideAssets); - app.bind(KnownToken.Response.prepare).toProvider(providePrepare); - - return app; -} - -function provideApi(resolve: Resolve): Api { - const knownHosts = resolve(KnownToken.Http.Api.knownHosts); - const createClient = resolve(KnownToken.Http.Client.factory); - const client = sauce(createClient({ baseURL: knownHosts.get('simaV3') })); - - return { - getCurrencies() { - return client.get('currency/'); - }, - getUser() { - return client.get('user/'); - }, - }; -} - -function provideAssets(): PageAssets { - return { - js: '', - css: 'index.css', - }; -} - -function providePrepare(resolve: Resolve): () => Promise { - const api = resolve(TOKEN.Response.api); - const sagaMiddleware = resolve(KnownToken.sagaMiddleware); - const bridge = resolve(KnownToken.SsrBridge.serverSide); - const builder = resolve(KnownToken.Response.builder); - - return async function prepare() { - const store = configureStore({ - reducer, - middleware: [sagaMiddleware], - }); - - await sagaMiddleware.timeout(3000).run(rootSaga, { api }); - - // пример установки meta-данных в ответ - builder.meta(JSON.stringify({ userId: store.getState().user?.data?.id })); - - return ( - -
- -
- -
- ); - }; -} diff --git a/examples/server/src/di/app/mobile.tsx b/examples/server/src/di/app/mobile.tsx deleted file mode 100644 index 96de142..0000000 --- a/examples/server/src/di/app/mobile.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { PageAssets } from '@sima-land/isomorph/http-server/types'; -import { Resolve, createApplication } from '@sima-land/isomorph/di'; -import { PresetResponse } from '@sima-land/isomorph/preset/node/response'; -import { KnownToken } from '@sima-land/isomorph/tokens'; - -export function MobileApp() { - const app = createApplication(); - - app.preset(PresetResponse()); - - app.bind(KnownToken.Response.assets).toProvider(provideAssets); - app.bind(KnownToken.Response.prepare).toProvider(providePrepare); - - return app; -} - -function providePrepare() { - return function prepareMobilePage() { - return ( -
-

Example app

-

Mobile version

-

Lorem ipsum dolor sit amet consectetur adipisicing elit. Ratione, minima.

-
- ); - }; -} - -function provideAssets(resolve: Resolve): PageAssets { - const source = resolve(KnownToken.Config.source); - - return { - js: source.get('MOBILE_CLIENT_ASSET_JS') || '', - css: source.get('MOBILE_CLIENT_ASSET_CSS') || '', - }; -} diff --git a/examples/server/src/di/app/root.ts b/examples/server/src/di/app/root.ts deleted file mode 100644 index 09c6e52..0000000 --- a/examples/server/src/di/app/root.ts +++ /dev/null @@ -1,59 +0,0 @@ -import express, { Application as ExpressApp } from 'express'; -import { createApplication, Resolve } from '@sima-land/isomorph/di'; -import { Config } from '../../types'; -import { PresetNode } from '@sima-land/isomorph/preset/node'; -import { KnownToken } from '@sima-land/isomorph/tokens'; -import { TOKEN } from '../tokens'; -import { DesktopApp } from './desktop'; -import { MobileApp } from './mobile'; -import { HandlerProvider } from '@sima-land/isomorph/preset/node/response'; -import { healthCheck } from '@sima-land/isomorph/http-server/handler/health-check'; - -export function RootApp() { - const app = createApplication(); - - app.preset(PresetNode()); - - app.bind(TOKEN.Root.config).toProvider(provideConfig); - app.bind(TOKEN.Root.mainServer).toProvider(provideHttpApp); - app.bind(TOKEN.Root.mobileHandler).toProvider(HandlerProvider(MobileApp)); - app.bind(TOKEN.Root.desktopHandler).toProvider(HandlerProvider(DesktopApp)); - - return app; -} - -function provideConfig(resolve: Resolve): Config { - const source = resolve(KnownToken.Config.source); - - return { - mainPort: Number(source.require('MAIN_HTTP_PORT')) || -1, - metricsPort: Number(source.require('METRICS_HTTP_PORT')) || -1, - }; -} - -function provideHttpApp(resolve: Resolve): ExpressApp { - const desktop = resolve(TOKEN.Root.desktopHandler); - const mobile = resolve(TOKEN.Root.mobileHandler); - const createServer = resolve(KnownToken.Http.Server.factory); - const middleware = resolve(KnownToken.Http.Server.Defaults.middleware); - - const app = createServer(); - - app.use(express.static('dist/static')); - app.use( - ['/', '/desktop', '/mobile'], - [ - ...middleware.start, - ...middleware.logging, - ...middleware.metrics, - ...middleware.tracing, - ...middleware.finish, - ], - ); - - app.get(['/', '/desktop'], desktop); - app.get(['/mobile'], mobile); - app.get('/healthcheck', healthCheck()); - - return app; -} diff --git a/examples/server/src/di/apps/main.ts b/examples/server/src/di/apps/main.ts new file mode 100644 index 0000000..82d0e4e --- /dev/null +++ b/examples/server/src/di/apps/main.ts @@ -0,0 +1,62 @@ +import { TOKEN } from '../tokens'; +import { AppConfig } from '../../app'; +import express, { Application as ExpressApp } from 'express'; +import { createApplication, Resolve } from '@sima-land/isomorph/di'; +import { PresetNode } from '@sima-land/isomorph/preset/node'; +import { HandlerProvider } from '@sima-land/isomorph/preset/node/handler'; +import { healthCheck } from '@sima-land/isomorph/http-server/handler/health-check'; +import { UsersHandler } from './users'; +import { PostsHandler } from './posts'; + +export function MainApp() { + const app = createApplication(); + + app.preset(PresetNode()); + + app.bind(TOKEN.appConfig).toProvider(provideAppConfig); + app.bind(TOKEN.httpServer).toProvider(provideHttpServer); + app.bind(TOKEN.usersHandler).toProvider(HandlerProvider(UsersHandler)); + app.bind(TOKEN.postsHandler).toProvider(HandlerProvider(PostsHandler)); + + return app; +} + +function provideAppConfig(resolve: Resolve): AppConfig { + const source = resolve(TOKEN.Known.Config.source); + const base = resolve(TOKEN.Known.Config.base); + + return { + ...base, + httpPort: { + main: Number(source.require('MAIN_HTTP_PORT')) || -1, + metrics: Number(source.require('METRICS_HTTP_PORT')) || -1, + }, + }; +} + +function provideHttpServer(resolve: Resolve): ExpressApp { + const createServer = resolve(TOKEN.Known.Http.Server.factory); + const usersHandler = resolve(TOKEN.usersHandler); + const postsHandler = resolve(TOKEN.postsHandler); + + // builtin middleware + const requestHandle = resolve(TOKEN.Known.Http.Server.Middleware.request); + const logging = resolve(TOKEN.Known.Http.Server.Middleware.log); + const metrics = resolve(TOKEN.Known.Http.Server.Middleware.metrics); + const tracing = resolve(TOKEN.Known.Http.Server.Middleware.tracing); + const errorHandle = resolve(TOKEN.Known.Http.Server.Middleware.error); + + const app = createServer(); + + // register middleware + app.use(express.static('dist/static')); + app.use(['/', '/users', '/posts'], [requestHandle, logging, metrics, tracing, errorHandle]); + + // register routes + app.get('/', usersHandler); + app.get('/users', usersHandler); + app.get('/posts', postsHandler); + app.get('/healthcheck', healthCheck()); + + return app; +} diff --git a/examples/server/src/di/apps/posts.tsx b/examples/server/src/di/apps/posts.tsx new file mode 100644 index 0000000..ad0d23a --- /dev/null +++ b/examples/server/src/di/apps/posts.tsx @@ -0,0 +1,54 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { createApplication, Resolve } from '@sima-land/isomorph/di'; +import { sauce } from '@sima-land/isomorph/http-client/sauce'; +import { PresetHandler } from '@sima-land/isomorph/preset/node/handler'; +import { Provider } from 'react-redux'; +import { HttpApi } from '../../app'; +import { PostsPage, reducer, saga } from '../../pages/posts'; +import { TOKEN } from '../tokens'; + +export function PostsHandler() { + const app = createApplication(); + + app.preset(PresetHandler()); + + app.bind(TOKEN.httpApi).toProvider(provideHttpApi); + app.bind(TOKEN.Known.Http.Handler.Response.Page.prepare).toProvider(providePagePrepare); + + return app; +} + +function provideHttpApi(resolve: Resolve): HttpApi { + const createClient = resolve(TOKEN.Known.Http.Client.factory); + + const client = sauce(createClient({ baseURL: 'https://jsonplaceholder.typicode.com/' })); + + return { + getPosts() { + return client.get('posts/'); + }, + getUsers() { + return client.get('users/'); + }, + }; +} + +function providePagePrepare(resolve: Resolve) { + const httpApi = resolve(TOKEN.httpApi); + const sagaMiddleware = resolve(TOKEN.Known.sagaMiddleware); + + return async () => { + const store = configureStore({ + reducer, + middleware: [sagaMiddleware], + }); + + await sagaMiddleware.run(saga, { api: httpApi }); + + return ( + + + + ); + }; +} diff --git a/examples/server/src/di/apps/users.tsx b/examples/server/src/di/apps/users.tsx new file mode 100644 index 0000000..de1c48f --- /dev/null +++ b/examples/server/src/di/apps/users.tsx @@ -0,0 +1,54 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { createApplication, Resolve } from '@sima-land/isomorph/di'; +import { sauce } from '@sima-land/isomorph/http-client/sauce'; +import { PresetHandler } from '@sima-land/isomorph/preset/node/handler'; +import { Provider } from 'react-redux'; +import { HttpApi } from '../../app'; +import { UsersPage, reducer, saga } from '../../pages/users'; +import { TOKEN } from '../tokens'; + +export function UsersHandler() { + const app = createApplication(); + + app.preset(PresetHandler()); + + app.bind(TOKEN.httpApi).toProvider(provideHttpApi); + app.bind(TOKEN.Known.Http.Handler.Response.Page.prepare).toProvider(providePagePrepare); + + return app; +} + +function provideHttpApi(resolve: Resolve): HttpApi { + const createClient = resolve(TOKEN.Known.Http.Client.factory); + + const client = sauce(createClient({ baseURL: 'https://jsonplaceholder.typicode.com/' })); + + return { + getPosts() { + return client.get('posts/'); + }, + getUsers() { + return client.get('users/'); + }, + }; +} + +function providePagePrepare(resolve: Resolve) { + const httpApi = resolve(TOKEN.httpApi); + const sagaMiddleware = resolve(TOKEN.Known.sagaMiddleware); + + return async () => { + const store = configureStore({ + reducer, + middleware: [sagaMiddleware], + }); + + await sagaMiddleware.run(saga, { api: httpApi }); + + return ( + + + + ); + }; +} diff --git a/examples/server/src/di/tokens.ts b/examples/server/src/di/tokens.ts index 75bcf0c..5984978 100644 --- a/examples/server/src/di/tokens.ts +++ b/examples/server/src/di/tokens.ts @@ -1,18 +1,16 @@ import { createToken } from '@sima-land/isomorph/di'; +import { KnownToken } from '@sima-land/isomorph/tokens'; +import type { AppConfig, HttpApi } from '../app'; import type { Application, Handler } from 'express'; -import type { Config, Api } from '../types'; export const TOKEN = { - // scope: root - Root: { - config: createToken('root/config'), - mainServer: createToken('root/main-server'), - mobileHandler: createToken('root/mobile-handler'), - desktopHandler: createToken('root/desktop-handler'), - }, + // reexport for convenient use + Known: KnownToken, - // scope: response - Response: { - api: createToken('response/api'), - }, + // tokens for our application components + appConfig: createToken('app-config'), + httpServer: createToken('http-server'), + usersHandler: createToken('users-handler'), + postsHandler: createToken('posts-handler'), + httpApi: createToken('response/api'), } as const; diff --git a/examples/server/src/index.ts b/examples/server/src/index.ts index da8a447..a2a47d0 100644 --- a/examples/server/src/index.ts +++ b/examples/server/src/index.ts @@ -1,18 +1,15 @@ -import { RootApp } from './di/app/root'; +import { MainApp } from './di/apps/main'; import { TOKEN } from './di/tokens'; -import { KnownToken } from '@sima-land/isomorph/tokens'; -RootApp().invoke( - [TOKEN.Root.config, KnownToken.logger, TOKEN.Root.mainServer, KnownToken.Metrics.httpApp], +MainApp().invoke( + [TOKEN.appConfig, TOKEN.Known.logger, TOKEN.httpServer, TOKEN.Known.Metrics.httpApp], (config, logger, mainServer, metricsServer) => { - // main app - mainServer.listen(config.mainPort, () => { - logger.info(`Server started on port ${config.mainPort}`); + mainServer.listen(config.httpPort.main, () => { + logger.info(`Server started on port ${config.httpPort.main}`); }); - // metrics app - metricsServer.listen(config.metricsPort, () => { - logger.info(`Metrics app started on port ${config.metricsPort}`); + metricsServer.listen(config.httpPort.metrics, () => { + logger.info(`Metrics app started on port ${config.httpPort.metrics}`); }); }, ); diff --git a/examples/server/src/pages/posts/index.tsx b/examples/server/src/pages/posts/index.tsx new file mode 100644 index 0000000..5606ba0 --- /dev/null +++ b/examples/server/src/pages/posts/index.tsx @@ -0,0 +1,53 @@ +import { createReducer } from '@reduxjs/toolkit'; +import { RemoteData, RemoteDataState } from '@sima-land/isomorph/utils/redux/remote-data'; +import { useSelector } from 'react-redux'; +import { END } from 'redux-saga'; +import { call, put } from 'typed-redux-saga/macro'; +import { Post, SagaDeps } from '../../app'; + +type PostsState = RemoteDataState; + +const initialState: PostsState = { + data: [], + error: null, + status: 'initial', +}; + +export const actions = { + ...RemoteData.createActions('posts'), +} as const; + +export const reducer = createReducer(initialState, builder => { + RemoteData.applyHandlers(actions, builder); +}); + +export function* saga({ api }: SagaDeps) { + const response = yield* call(api.getPosts); + + if (response.ok) { + yield* put(actions.success(response.data)); + } else { + yield* put(actions.failure(response.data || response.error)); + } + + yield* put(END); +} + +export function PostsPage() { + const items = useSelector((state: PostsState) => state.data); + + return ( +
+

Posts

+ + {items.map(item => ( +
+

{item.title}

+ {item.body} +
+ ))} + + Go to users +
+ ); +} diff --git a/examples/server/src/pages/users/index.tsx b/examples/server/src/pages/users/index.tsx new file mode 100644 index 0000000..2916256 --- /dev/null +++ b/examples/server/src/pages/users/index.tsx @@ -0,0 +1,53 @@ +import { createReducer } from '@reduxjs/toolkit'; +import { RemoteData, RemoteDataState } from '@sima-land/isomorph/utils/redux/remote-data'; +import { useSelector } from 'react-redux'; +import { END } from 'redux-saga'; +import { call, put } from 'typed-redux-saga/macro'; +import { SagaDeps, User } from '../../app'; + +type PostsState = RemoteDataState; + +const initialState: PostsState = { + data: [], + error: null, + status: 'initial', +}; + +export const actions = { + ...RemoteData.createActions('posts'), +} as const; + +export const reducer = createReducer(initialState, builder => { + RemoteData.applyHandlers(actions, builder); +}); + +export function* saga({ api }: SagaDeps) { + const response = yield* call(api.getUsers); + + if (response.ok) { + yield* put(actions.success(response.data)); + } else { + yield* put(actions.failure(response.data || response.error)); + } + + yield* put(END); +} + +export function UsersPage() { + const items = useSelector((state: PostsState) => state.data); + + return ( +
+

Users

+ + {items.map(item => ( +
+

{item.username}

+ {item.name} +
+ ))} + + Go to posts +
+ ); +} diff --git a/examples/server/src/reducers/app.ts b/examples/server/src/reducers/app.ts deleted file mode 100644 index e9e1d22..0000000 --- a/examples/server/src/reducers/app.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Status } from '@sima-land/isomorph/utils/redux/remote-data'; -import { Currency, User } from '../types'; - -interface AppState { - user: { - data: null | User; - status: Status; - error?: string; - }; - currencies: { - data: null | Currency[]; - status: Status; - error?: string; - }; -} - -const initialState: AppState = { - user: { - status: 'initial', - data: null, - }, - currencies: { - status: 'initial', - data: null, - }, -}; - -export const { actions, reducer } = createSlice({ - name: 'app', - initialState, - reducers: { - userFetchDone(state, { payload }: PayloadAction) { - state.user.status = 'success'; - state.user.data = payload; - }, - userFetchFail(state, { payload }: PayloadAction) { - state.user.status = 'failure'; - state.user.error = payload; - }, - currenciesFetchDone(state, { payload }: PayloadAction) { - state.currencies.status = 'success'; - state.currencies.data = payload; - }, - currenciesFetchFail(state, { payload }: PayloadAction) { - state.currencies.status = 'failure'; - state.currencies.error = payload; - }, - }, -}); - -export const selectors = { - user: (state: AppState) => state.user, - currencies: (state: AppState) => state.currencies, -} as const; diff --git a/examples/server/src/sagas/index.ts b/examples/server/src/sagas/index.ts deleted file mode 100644 index becd6cd..0000000 --- a/examples/server/src/sagas/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { END } from 'redux-saga'; -import { call, put, all } from 'redux-saga/effects'; -import { Api } from '../types'; -import { actions } from '../reducers/app'; - -export interface SagaOptions { - api: Api; -} - -export function* rootSaga(options: SagaOptions): Generator { - // делаем два параллельных запроса - yield all([call(fetchUser, options), call(fetchCurrencies, options)]); - - // завершаем сагу - yield put(END); -} - -function* fetchUser({ api }: SagaOptions) { - const response: Awaited> = yield call(api.getUser); - - if (response.ok) { - yield put(actions.userFetchDone(response.data.items[0])); - } else { - yield put(actions.userFetchFail(String(response.error))); - } -} - -function* fetchCurrencies({ api }: SagaOptions) { - const response: Awaited> = yield call(api.getCurrencies); - - if (response.ok) { - yield put(actions.currenciesFetchDone(response.data.items)); - } else { - yield put(actions.currenciesFetchFail(String(response.error))); - } -} diff --git a/examples/server/src/types.ts b/examples/server/src/types.ts deleted file mode 100644 index 028f7ce..0000000 --- a/examples/server/src/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { SauceResponse } from '@sima-land/isomorph/http-client/sauce'; - -export interface Config { - mainPort: number; - metricsPort: number; -} - -export interface Api { - getUser(): Promise>; - getCurrencies(): Promise>; -} - -export interface User { - id: string; - name: string; -} - -export interface Currency { - id: number; - name: string; - grapheme: string; -} diff --git a/examples/server/tsconfig.json b/examples/server/tsconfig.json index 1c08140..62c82b3 100644 --- a/examples/server/tsconfig.json +++ b/examples/server/tsconfig.json @@ -6,7 +6,10 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "paths": { + "@sima-land/isomorph/*": ["../../dist/*"] + } }, "include": ["./src/"] }