From ae70493ce1c0078ed28f580628f86fb6881c22dc Mon Sep 17 00:00:00 2001 From: Mehdi Achour Date: Wed, 29 May 2024 18:27:52 +0100 Subject: [PATCH] refactor(socket.io): Switch to V2 + Vite (#490) --- socket.io/.eslintrc.cjs | 84 +++++++++++++++++++++++++++++++++++++++ socket.io/.eslintrc.js | 4 -- socket.io/.gitignore | 3 +- socket.io/app/root.tsx | 42 ++++++++++---------- socket.io/package.json | 53 ++++++++++++++---------- socket.io/remix.config.js | 11 ----- socket.io/remix.env.d.ts | 2 - socket.io/server.js | 77 +++++++++++++++++++++++++++++++++++ socket.io/server/index.js | 81 ------------------------------------- socket.io/tsconfig.json | 20 +++++++--- socket.io/vite.config.ts | 16 ++++++++ 11 files changed, 246 insertions(+), 147 deletions(-) create mode 100644 socket.io/.eslintrc.cjs delete mode 100644 socket.io/.eslintrc.js delete mode 100644 socket.io/remix.config.js delete mode 100644 socket.io/remix.env.d.ts create mode 100644 socket.io/server.js delete mode 100644 socket.io/server/index.js create mode 100644 socket.io/vite.config.ts diff --git a/socket.io/.eslintrc.cjs b/socket.io/.eslintrc.cjs new file mode 100644 index 00000000..bb26c82d --- /dev/null +++ b/socket.io/.eslintrc.cjs @@ -0,0 +1,84 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + }, + + // Node + { + files: [".eslintrc.cjs", "server.js"], + env: { + node: true, + }, + }, + ], +}; diff --git a/socket.io/.eslintrc.js b/socket.io/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/socket.io/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/socket.io/.gitignore b/socket.io/.gitignore index d9418258..80ec311f 100644 --- a/socket.io/.gitignore +++ b/socket.io/.gitignore @@ -1,6 +1,5 @@ node_modules /.cache -/server/build -/public/build +/build .env diff --git a/socket.io/app/root.tsx b/socket.io/app/root.tsx index 61943191..e4a57ceb 100644 --- a/socket.io/app/root.tsx +++ b/socket.io/app/root.tsx @@ -1,7 +1,5 @@ -import type { MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, @@ -13,11 +11,23 @@ import io from "socket.io-client"; import { SocketProvider } from "~/context"; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} export default function App() { const [socket, setSocket] = useState(); @@ -38,19 +48,7 @@ export default function App() { }, [socket]); return ( - - - - - - - - - - - - - - - ); + + + ); } diff --git a/socket.io/package.json b/socket.io/package.json index aedd8d25..a47d7fa4 100644 --- a/socket.io/package.json +++ b/socket.io/package.json @@ -1,36 +1,49 @@ { + "name": "eps", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "remix watch", - "start": "cross-env NODE_ENV=production node server/index.js", - "start:dev": "cross-env NODE_ENV=development node server/index.js", + "build": "remix vite:build", + "dev": "node ./server.js", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "cross-env NODE_ENV=production node ./server.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/express": "^1.19.3", - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", + "@remix-run/express": "^2.9.2", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", "compression": "^1.7.4", - "cross-env": "^7.0.3", - "express": "^4.17.3", + "express": "^4.18.2", + "isbot": "^4.1.0", "morgan": "^1.10.0", - "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0", - "socket.io": "^4.4.1", - "socket.io-client": "^4.4.1" + "socket.io": "^4.7.5", + "socket.io-client": "^4.7.5" }, "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "eslint": "^8.27.0", - "typescript": "^4.8.4" + "@remix-run/dev": "^2.9.2", + "@types/compression": "^1.7.5", + "@types/express": "^4.17.20", + "@types/morgan": "^1.9.9", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "cross-env": "^7.0.3", + "eslint": "^8.38.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } -} +} \ No newline at end of file diff --git a/socket.io/remix.config.js b/socket.io/remix.config.js deleted file mode 100644 index 76048d0c..00000000 --- a/socket.io/remix.config.js +++ /dev/null @@ -1,11 +0,0 @@ -/** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { - future: { - v2_routeConvention: true, - }, - ignoredRouteFiles: ["**/.*"], - serverBuildDirectory: "server/build", - // appDirectory: "app", - // assetsBuildDirectory: "public/build", - // publicPath: "/build/", -}; diff --git a/socket.io/remix.env.d.ts b/socket.io/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/socket.io/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/socket.io/server.js b/socket.io/server.js new file mode 100644 index 00000000..4936d516 --- /dev/null +++ b/socket.io/server.js @@ -0,0 +1,77 @@ +import { createRequestHandler } from "@remix-run/express"; +import compression from "compression"; +import express from "express"; +import morgan from "morgan"; +import { createServer } from "http"; +import { Server } from "socket.io"; + +const viteDevServer = + process.env.NODE_ENV === "production" + ? undefined + : await import("vite").then((vite) => + vite.createServer({ + server: { middlewareMode: true }, + }) + ); + +const remixHandler = createRequestHandler({ + build: viteDevServer + ? () => viteDevServer.ssrLoadModule("virtual:remix/server-build") + : await import("./build/server/index.js"), +}); + +const app = express(); + +// You need to create the HTTP server from the Express app +const httpServer = createServer(app); + +// And then attach the socket.io server to the HTTP server +const io = new Server(httpServer); + +// Then you can use `io` to listen the `connection` event and get a socket +// from a client +io.on("connection", (socket) => { + // from this point you are on the WS connection with a specific client + console.log(socket.id, "connected"); + + socket.emit("confirmation", "connected!"); + + socket.on("event", (data) => { + console.log(socket.id, data); + socket.emit("event", "pong"); + }); +}); + + +app.use(compression()); + +// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header +app.disable("x-powered-by"); + +// handle asset requests +if (viteDevServer) { + app.use(viteDevServer.middlewares); +} else { + // Vite fingerprints its assets so we can cache forever. + app.use( + "/assets", + express.static("build/client/assets", { immutable: true, maxAge: "1y" }) + ); +} + +// Everything else (like favicon.ico) is cached for an hour. You may want to be +// more aggressive with this caching. +app.use(express.static("build/client", { maxAge: "1h" })); + +app.use(morgan("tiny")); + +// handle SSR requests +app.all("*", remixHandler); + +const port = process.env.PORT || 3000; + +// instead of running listen on the Express app, do it on the HTTP server +httpServer.listen(port, () => { + console.log(`Express server listening at http://localhost:${port}`) +}); + diff --git a/socket.io/server/index.js b/socket.io/server/index.js deleted file mode 100644 index 17d45959..00000000 --- a/socket.io/server/index.js +++ /dev/null @@ -1,81 +0,0 @@ -const fs = require("fs"); -const { createServer } = require("http"); -const path = require("path"); - -const { createRequestHandler } = require("@remix-run/express"); -const compression = require("compression"); -const express = require("express"); -const morgan = require("morgan"); -const { Server } = require("socket.io"); - -const MODE = process.env.NODE_ENV; -const BUILD_DIR = path.join(process.cwd(), "server/build"); - -if (!fs.existsSync(BUILD_DIR)) { - console.warn( - "Build directory doesn't exist, please run `npm run dev` or `npm run build` before starting the server.", - ); -} - -const app = express(); - -// You need to create the HTTP server from the Express app -const httpServer = createServer(app); - -// And then attach the socket.io server to the HTTP server -const io = new Server(httpServer); - -// Then you can use `io` to listen the `connection` event and get a socket -// from a client -io.on("connection", (socket) => { - // from this point you are on the WS connection with a specific client - console.log(socket.id, "connected"); - - socket.emit("confirmation", "connected!"); - - socket.on("event", (data) => { - console.log(socket.id, data); - socket.emit("event", "pong"); - }); -}); - -app.use(compression()); - -// You may want to be more aggressive with this caching -app.use(express.static("public", { maxAge: "1h" })); - -// Remix fingerprints its assets so we can cache forever -app.use(express.static("public/build", { immutable: true, maxAge: "1y" })); - -app.use(morgan("tiny")); -app.all( - "*", - MODE === "production" - ? createRequestHandler({ build: require("./build") }) - : (req, res, next) => { - purgeRequireCache(); - const build = require("./build"); - return createRequestHandler({ build, mode: MODE })(req, res, next); - }, -); - -const port = process.env.PORT || 3000; - -// instead of running listen on the Express app, do it on the HTTP server -httpServer.listen(port, () => { - console.log(`Express server listening on port ${port}`); -}); - -//////////////////////////////////////////////////////////////////////////////// -function purgeRequireCache() { - // purge require cache on requests for "server side HMR" this won't let - // you have in-memory objects between requests in development, - // alternatively you can set up nodemon/pm2-dev to restart the server on - // file changes, we prefer the DX of this though, so we've included it - // for you by default - for (const key in require.cache) { - if (key.startsWith(BUILD_DIR)) { - delete require.cache[key]; - } - } -} diff --git a/socket.io/tsconfig.json b/socket.io/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/socket.io/tsconfig.json +++ b/socket.io/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2019"], + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@remix-run/node", "vite/client"], "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", - "moduleResolution": "node", + "module": "ESNext", + "moduleResolution": "Bundler", "resolveJsonModule": true, - "target": "ES2019", + "target": "ES2022", "strict": true, "allowJs": true, + "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { "~/*": ["./app/*"] }, - // Remix takes care of building everything in `remix build`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/socket.io/vite.config.ts b/socket.io/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/socket.io/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +});