Skip to content

Commit

Permalink
SSR prod и dev режимы (#59)
Browse files Browse the repository at this point in the history
* добавил ssr в дев и прод режимы

* сборка клиента для корректного линтинга проекта

* добавил команду build:ssr

* обработка стора на SSR

* добавил доступ к стору в лоадерах роутов

* сервер порт 3001 -> 5000 для совместимости с oauth

* добавил комплицию типов пакета client

* client-only перевод в jsx

* мелкие правки

* добавил rimraf для кроссплатформенности

* замена `querySelector`

* подготовка к мерджу

* экспорт тип leaderboard стейта, исправление в условии SSR
  • Loading branch information
DNLHC authored Apr 11, 2023
1 parent 7f19694 commit 473dd6e
Show file tree
Hide file tree
Showing 45 changed files with 920 additions and 420 deletions.
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
CLIENT_PORT=3000
SERVER_PORT=3001
SERVER_PORT=5000
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jobs:
run: yarn
- name: Initialize
run: yarn lerna bootstrap
- name: Build client
run: yarn build --scope=client
- name: Build ssr
run: yarn build:ssr
- name: Lint
run: yarn lint
- name: Test
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
"scripts": {
"bootstrap": "yarn && node init.js && lerna clean && yarn && lerna bootstrap",
"build": "lerna run build",
"build:ssr": "lerna run build:ssr",
"dev:client": "lerna run dev --scope=client",
"dev:server": "lerna run dev --scope=server",
"dev": "lerna run dev",
"test": "lerna run test",
"lint": "lerna run lint",
"format": "lerna run format",
"preview": "lerna run preview"
"preview": "lerna run preview",
"start": "lerna run start"
},
"license": "MIT",
"workspaces": [
Expand Down
1 change: 1 addition & 0 deletions packages/client/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_API_BASE_URL=http://localhost:5000/api/v2
2 changes: 2 additions & 0 deletions packages/client/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
node_modules
9 changes: 6 additions & 3 deletions packages/client/index.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="{{__THEME__}}">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fancy Colors</title>
<script>
window.__INITIAL_STATE__ = <!--store-data-->;
</script>
</head>
<body>
<div id="root"></div>
<div id="root"><!--ssr-outlet--></div>
<div id="modals"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="module" src="/src/entry-client.tsx"></script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @type {import('jest').Config} */
import dotenv from 'dotenv';
dotenv.config();

/** @type {import('jest').Config} */
export default {
preset: 'ts-jest',
testEnvironment: 'jsdom',
Expand Down
27 changes: 21 additions & 6 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@
"name": "client",
"version": "0.0.0",
"type": "module",
"main": "./dist/server/assets/js/entry-server.js",
"exports": {
".": {
"import": "./dist/server/assets/js/entry-server.js",
"types": "./dist/server/assets/js/entry-server.d.ts"
},
"./index.html": "./dist/client/index.html",
"./dev/index.html": "./index.html",
"./dev/entry-server": "./src/entry-server.tsx",
"./package.json": "./package.json"
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "vite build --outDir dist/client && yarn build:types",
"build:types": "tsc --p tsconfig.prod.json",
"build:ssr": "vite build --outDir dist/server --ssr ./src/entry-server.tsx && yarn build:types",
"preview": "vite preview --host",
"lint": "run-p lint:*",
"lint:eslint": "eslint \"src/**/*.{ts,tsx}\" --cache --cache-location ./node_modules/.cache/eslint",
Expand All @@ -20,23 +33,25 @@
"node": "18.x"
},
"dependencies": {
"@reduxjs/toolkit": "1.9.3",
"@reduxjs/toolkit": "2.0.0-alpha.1",
"classnames": "2.3.2",
"colorjs.io": "0.4.3",
"dotenv": "16.0.2",
"js-cookie": "3.0.1",
"qs": "6.11.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.43.2",
"react-redux": "8.0.5",
"react-router-dom": "6.8.1",
"swiper": "9.1.0",
"colorjs.io": "0.4.3"
"react-router-dom": "6.10.0",
"swiper": "9.1.0"
},
"devDependencies": {
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/jest": "29.4.0",
"@types/js-cookie": "3.0.3",
"@types/react": "18.0.18",
"@types/react-dom": "18.0.11",
"@vitejs/plugin-react": "3.1.0",
Expand All @@ -62,7 +77,7 @@
"ts-jest": "29.0.5",
"ts-jest-mock-import-meta": "1.0.0",
"typescript": "4.9.5",
"vite": "4.1.4",
"vite": "4.2.1",
"vite-plugin-svgr": "2.4.0",
"whatwg-fetch": "3.6.2"
},
Expand Down
6 changes: 4 additions & 2 deletions packages/client/public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const URLS = [
'/game/village',
'/index.html',
'/assets/css/index.css',
'/assets/js/main.js',
'/assets/js/entry-server.js',
'/assets/png/bg-image.png',
'/assets/woff2/MontserratAlternates-Bold.woff2',
'/assets/woff2/MontserratAlternates-Regular.woff2',
Expand Down Expand Up @@ -59,13 +59,15 @@ self.addEventListener('fetch', event => {
function tryNetwork (request, timeout) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(reject, timeout);
// NOTE: https://github.com/w3c/ServiceWorker/issues/693
const allowedMethods = ['GET', 'HEAD'];

fetch(request).then(response => {
clearTimeout(timeoutId);
const responseClone = response.clone();

caches.open(CACHE_NAME).then(cache => {
if (request.url.match('^(http|https)://')) {
if (request.url.match('^(http|https)://') && allowedMethods.includes(request.method)) {
cache.put(request, responseClone);
}
})
Expand Down
12 changes: 10 additions & 2 deletions packages/client/src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ export class AuthApi extends BaseApi {
});
}

me() {
return this.http.get<UserDTO | APIError>('/user');
me(request?: Request) {
const headers: Record<string, string> = {};

if (import.meta.env.SSR && request) {
headers.cookie = request.headers.get('cookie') as string;
}

return this.http.get<UserDTO | APIError>('/user', {
headers,
});
}

logout() {
Expand Down
82 changes: 26 additions & 56 deletions packages/client/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,29 @@
import { AuthLayout, getCurrentUser } from 'components/auth-layout';
import {
Route,
defer,
createBrowserRouter,
createRoutesFromElements,
RouterProvider,
} from 'react-router-dom';
import { ProtectedRoutes } from 'utils/protected-routes';
import { Profile } from 'pages/profile';
import { MainPage } from 'pages/main';
import { GamePage } from 'pages/game';
import { RouterPaths } from './app.types';
import { LoginPage, RegisterPage } from 'pages/auth';
import { MainLayout } from 'components/main-layout';
import { Error404, Error500 } from 'pages/error';
import { Leaderboard } from 'pages/leaderboard';
import { ForumThread } from 'pages/forum-thread';
import { NewThreadModal } from 'components/modal-new-thread';
import { Forum } from 'pages/forum';
import { ThemeProvider, Theme } from 'components/hooks/use-theme';
import React from 'react';
import { Provider as StoreProvider } from 'react-redux';
import type { AppStore, InitialState } from './store';
import './styles/index.pcss';

const router = createBrowserRouter(
createRoutesFromElements(
<Route
element={<AuthLayout />}
loader={() => defer({ userPromise: getCurrentUser() })}
>
<Route path={RouterPaths.REGISTER} element={<RegisterPage />} />
<Route path={RouterPaths.LOGIN} element={<LoginPage />} />
<Route
element={<MainLayout />}
loader={({ request }) => {
const url = new URL(request.url);
return url.searchParams.get('modal');
}}
>
<Route path={RouterPaths.MAIN} element={<MainPage />} />
<Route path={RouterPaths.LEADERBOARD} element={<Leaderboard />} />
<Route path={RouterPaths.FORUM} element={<Forum />}>
<Route path={RouterPaths.NEW_THREAD} element={<NewThreadModal />} />
</Route>
<Route path={`${RouterPaths.FORUM}/:id`} element={<ForumThread />} />
<Route element={<ProtectedRoutes />}>
<Route path={RouterPaths.PROFILE} element={<Profile />} />
<Route path={`${RouterPaths.GAME}/:id`} element={<GamePage />} />
</Route>
</Route>
<Route path={RouterPaths.ERROR_500} element={<Error500 />} />
<Route path={RouterPaths.ERROR_404} element={<Error404 />} />
<Route path="*" element={<Error404 />} />
</Route>
)
);

function App() {
return <RouterProvider router={router} />;
function App({
children,
theme = Theme.LIGHT,
store,
initialState,
}: {
children: React.ReactNode;
theme?: string;
store: AppStore;
initialState?: InitialState;
}) {
return (
<React.StrictMode>
<ThemeProvider initialValue={theme}>
<StoreProvider store={store} serverState={initialState}>
{children}
</StoreProvider>
</ThemeProvider>
</React.StrictMode>
);
}

export default App;
export default App;
9 changes: 9 additions & 0 deletions packages/client/src/client.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
declare const __SERVER_PORT__: number;

declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/naming-convention
__INITIAL_STATE__?: string;
}
}

export {};
21 changes: 6 additions & 15 deletions packages/client/src/components/auth-layout/auth-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { authApi } from 'api/auth';
import { AuthProvider } from 'components/hooks/use-auth';
import { Suspense } from 'react';
import { Await, useLoaderData, useOutlet } from 'react-router-dom';
import { useLoaderData, useOutlet } from 'react-router-dom';
import { transformUser } from 'utils/api-transformers';
import { hasApiError } from 'utils/has-api-error';

export async function getCurrentUser() {
export async function getCurrentUser(request: Request) {
try {
const response = await authApi.me();
const response = await authApi.me(request);

if (!response || hasApiError(response)) {
return null;
Expand All @@ -19,21 +18,13 @@ export async function getCurrentUser() {
}
}

const LoadingIndicator = () => <div>Загрузка...</div>;

type LoaderDataResponse = {
userPromise: ReturnType<typeof getCurrentUser>;
user: Awaited<ReturnType<typeof getCurrentUser>>;
};

export const AuthLayout = () => {
const outlet = useOutlet();
const { userPromise } = useLoaderData() as LoaderDataResponse;
const { user } = useLoaderData() as LoaderDataResponse;

return (
<Suspense fallback={<LoadingIndicator />}>
<Await resolve={userPromise}>
{(user) => <AuthProvider userData={user}>{outlet}</AuthProvider>}
</Await>
</Suspense>
);
return <AuthProvider userData={user}>{outlet}</AuthProvider>;
};
13 changes: 13 additions & 0 deletions packages/client/src/components/client-only/client-only.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useState, useEffect } from 'react';

export const ClientOnly = ({ children }: { children: React.ReactNode }) => {
const [hasMounted, setHasMounted] = useState(false);

useEffect(() => {
setHasMounted(true);
}, []);

if (!hasMounted) return null;

return <>{children}</>;
};
1 change: 1 addition & 0 deletions packages/client/src/components/client-only/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ClientOnly } from './client-only';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { resetCompletedGame } from 'src/services/game-slice';
import { setUserToLeaderboard } from 'src/actions';
import styles from './game-view.module.pcss';
import cn from 'classnames';
import { usePatternImage } from './utils/use-pattern-image';

type Props = {
data: GameData;
Expand All @@ -26,6 +27,7 @@ export const GameViewCompleted: FC<Props> = ({ data }) => {

const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);

const patternImage = usePatternImage();
const canvasRef = useRef<HTMLCanvasElement>(null);
const resizableRef = useRef<HTMLDivElement>(null);
const fieldRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -84,8 +86,8 @@ export const GameViewCompleted: FC<Props> = ({ data }) => {

useEffect(() => {
if (!ctx) return;
drawHistory(ctx, data, movesHistory);
}, [ctx, data, movesHistory]);
drawHistory(ctx, data, movesHistory, patternImage);
}, [ctx, data, movesHistory, patternImage]);

const GameEndMessage = () => {
return (
Expand Down
Loading

0 comments on commit 473dd6e

Please sign in to comment.