diff --git a/.eslintrc b/.eslintrc
index e414272..b440d7b 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -21,9 +21,10 @@
"rules": {
"no-console": "off",
"semi": ["error", "always"],
- "@typescript-eslint/explicit-function-return-type": "off", // Optional, customize TypeScript specific rules
- "@typescript-eslint/no-unused-vars": ["error"], // Example: detect unused variables in TypeScript
- "@typescript-eslint/no-explicit-any": "warn" // Discourages usage of 'any' in TypeScript
+ "react/react-in-jsx-scope": "off", // Not needed with React 17+ new JSX transform
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/no-unused-vars": ["error"],
+ "@typescript-eslint/no-explicit-any": "warn"
},
"settings": {
"react": {
diff --git a/README.md b/README.md
index 79a01a1..e24b2b4 100755
--- a/README.md
+++ b/README.md
@@ -1,27 +1,98 @@
# Devnotnull.com React application
-Last updated - 26/08/2022
+Last updated - 01/02/2026
[](https://github.com/devisnotnull/devnotnull.com-app/actions/workflows/on_merge_main.yml)
-An isomorphic web app which can be run using three distict deployment paradigms
+An isomorphic web app which can be run using three distinct deployment paradigms:
- Client only bundle
- Deployed via Docker
-- Deployed via the Serverless framework (AWS Lambda)
+- Deployed via the Serverless framework (AWS Lambda)
+
+## Tech Stack
+
+- **Build Tool**: Vite 5
+- **Framework**: React 18
+- **Routing**: React Router 6 (with SSR support)
+- **Styling**: Tailwind CSS
+- **Language**: TypeScript
+- **Runtime**: Node.js / Bun
+
+## Requirements
+
+- **Node.js**: v20.0.0 or higher
+- **Bun**: v1.0.0 or higher (used as package manager)
## Running the application
-### Native (Docker)
+### Development
+
+```bash
+bun install
+bun dev
+```
+
+This starts:
+- Vite dev server on port 9000 (client HMR)
+- Server build in watch mode
+- Express server on port 3000
+
+### Production Build
+
+```bash
+bun build:prod
+```
+
+### Production (Docker)
+
+```bash
+bun build:prod
+docker build -t devnotnull-ui .
+docker run -p 3000:3000 devnotnull-ui
+```
+
+## Project Structure
+
+```
+src/
+├── client/ # Client entry point
+├── server/ # Express server with SSR
+├── components/ # React components
+├── core/ # Business logic & API calls
+├── models/ # TypeScript/Zod schemas
+├── page/ # Route page components
+├── style/ # Tailwind CSS styles
+└── routes.tsx # Route definitions
+```
+
+## Build Output
-yarn prod:pure
+```
+build/
+├── server.js # SSR server bundle
+├── server.js.map # Source map
+└── static/
+ ├── client-manifest.json # Vite manifest
+ ├── css/ # CSS bundles
+ └── js/ # JS bundles
+```
-### Serverless
+## Scripts
-yarn prod
+| Script | Description |
+|--------|-------------|
+| `bun dev` | Start development server |
+| `bun build` | Build client and server |
+| `bun build:prod` | Production build |
+| `bun build:client` | Build client only |
+| `bun build:server` | Build server only |
+| `bun lint` | Run ESLint |
+| `bun lint:fix` | Fix ESLint issues |
+| `bun test` | Run tests |
## Deploy
-The deployment pipeline depends on your choice of deployment paradigms
+The deployment pipeline depends on your choice of deployment paradigms.
diff --git a/bun.lockb b/bun.lockb
index 6e46342..9b91bfe 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..e11b683
--- /dev/null
+++ b/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ DevNotNull
+
+
+
+
+
+
diff --git a/package.json b/package.json
index d14ec8b..2996a04 100644
--- a/package.json
+++ b/package.json
@@ -2,36 +2,24 @@
"name": "@devnotnull/devnotnull-ui",
"version": "1.0.0",
"private": true,
+ "type": "module",
"scripts": {
"cleanup": "rimraf build/",
"test": "jest",
- "build:client:dev": "TARGET=client webpack",
- "build:client:prod": "TARGET=client NODE_ENV=production webpack",
- "build:server": "TARGET=server webpack",
- "build:server:prod": "NODE_ENV=production TARGET=server webpack",
- "build": "bun build:client:dev&& bun build:server",
- "build:dev": "bun cleanup && NODE_ENV=development bun build:client:dev&& bun build:server",
- "build:prod": "bun cleanup && NODE_ENV=production bun build:client:dev&& NODE_ENV=production bun build:server",
- "dev": "bun cleanup && bun build:style-typings && NODE_ENV=development IS_LOCAL=TRUE NODE_RUNTIME_ENV=local NODE_ENV=development concurrently -r --kill-others \"bun devserver\" \"TARGET=server webpack --watch\" \"wait-on ./build/server.js && node build/server.js\"",
- "prod": "bun cleanup && bun build:style-typings && bun build:client:prod && NODE_ENV=production IS_LOCAL=TRUE NODE_RUNTIME_ENV=local NODE_ENV=production concurrently -r --kill-others \"bun prodserver\" \"TARGET=server webpack --watch\" \"wait-on ./build/server.js && node build/server.js\"",
- "build:style-typings": "node webpack/style-typings.js",
+ "dev": "bun cleanup && concurrently -r --kill-others \"vite\" \"vite build --config vite.config.server.ts --watch\" \"wait-on ./build/server.js && node --experimental-specifier-resolution=node build/server.js\"",
+ "build": "bun cleanup && bun build:client && bun build:server",
+ "build:client": "vite build",
+ "build:server": "vite build --config vite.config.server.ts",
+ "build:prod": "bun cleanup && NODE_ENV=production bun build:client && NODE_ENV=production bun build:server",
+ "preview": "vite preview",
"lint": "eslint ./src --ext .ts,.tsx",
"lint:fix": "eslint --ext .ts,.tsx --fix ./src",
"types": "tcm src",
- "devserver": "cross-env NODE_ENV=development TARGET=client webpack serve --mode development --inline --hot",
- "prodserver": "cross-env NODE_ENV=production TARGET=client webpack serve --mode production",
"prepare": "husky install",
"infra": "cdk deploy --require-approval never",
"infra:synth": "cdk synth"
},
"devDependencies": {
- "@babel/cli": "^7.22.5",
- "@babel/core": "^7.22.5",
- "@babel/plugin-transform-react-inline-elements": "^7.22.5",
- "@babel/preset-env": "^7.22.5",
- "@babel/preset-react": "^7.22.5",
- "@babel/preset-typescript": "^7.22.5",
- "@babel/register": "^7.18.9",
"@testing-library/react": "^13.4.0",
"@types/aws-lambda": "^8.10.46",
"@types/classnames": "^2.2.8",
@@ -44,89 +32,37 @@
"@types/react-syntax-highlighter": "^13.5.2",
"@types/serialize-javascript": "^1.5.0",
"@types/source-map-support": "^0.5.1",
- "@types/webpack": "^4.41.7",
- "@types/webpack-dev-middleware": "^2.0.2",
- "@types/webpack-env": "^1.13.9",
- "@types/webpack-hot-middleware": "^2.16.5",
- "@types/webpack-merge": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
- "assets-webpack-plugin": "^7.0.0",
+ "@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.14",
"aws-sdk": "^2.824.0",
- "babel-jest": "^24.0.0",
- "babel-loader": "^8.0.6",
- "babel-plugin-graphql-tag": "^2.5.0",
- "babel-plugin-istanbul": "^4.1.6",
- "babel-plugin-module-resolver": "^3.2.0",
- "babel-plugin-root-import": "^6.6.0",
- "before-build-webpack": "^0.2.11",
- "buffer": "^6.0.0",
- "circular-dependency-plugin": "^5.2.2",
- "concurrently": "^4.1.0",
- "copy-webpack-plugin": "^7.0.0",
- "cross-env": "^4.0.0",
+ "concurrently": "^8.2.2",
+ "cross-env": "^7.0.3",
"cross-fetch": "^3.1.4",
- "css-loader": "6.8.1",
- "css-minimizer-webpack-plugin": "^5.0.1",
- "cz-customizable": "^6.2.0",
- "duplicate-package-checker-webpack-plugin": "^3.0.0",
- "esbuild-loader": "^3.0.1",
"eslint": "8.9.0",
"eslint-plugin-prettier": "5.2.1",
- "extend": "^3.0.1",
- "file-loader": "^3.0.1",
- "fork-ts-checker-webpack-plugin": "^1.3.5",
- "fs-extra": "7.0.0",
- "http-server": "^14.1.1",
- "https-browserify": "^1.0.0",
+ "fs-extra": "11.2.0",
"husky": "^8.0.0",
- "isomorphic-style-loader": "^5.1.0",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^13.2.2",
- "mini-css-extract-plugin": "^1.3.3",
"nodemon": "^2.0.7",
- "os-browserify": "^0.3.0",
- "path-browserify": "^1.0.1",
"postcss": "^8.4.25",
- "postcss-assets": "^5.0.0",
- "postcss-custom-media": "^7.0.8",
- "postcss-flexbugs-fixes": "^4.1.0",
- "postcss-loader": "^7.3.3",
- "postcss-media-variables": "^2.0.1",
"postcss-preset-env": "^9.0.0",
- "postcss-simple-vars": "^6.0.3",
- "postcss-url": "^10.1.1",
"prettier": "^3.0.3",
- "process": "0.11.10",
- "redux-devtools-extension": "^2.13.0",
- "resolve": "^1.22.2",
- "resolve-url-loader": "^3.1.2",
- "rimraf": "^2.6.2",
+ "rimraf": "^5.0.5",
"serverless": "^3.33.0",
"serverless-deployment-bucket": "1.6.0",
"serverless-offline": "12.0.4",
"serverless-prune-plugin": "^1.4.3",
"serverless-s3-local": "^0.7.2",
"serverless-s3-sync": "^1.15.0",
- "stream-browserify": "^3.0.0",
- "stream-http": "^3.2.0",
- "style-loader": "^2.0.0",
- "terser-webpack-plugin": "^5.0.3",
"ts-jest": "^29.0.1",
- "tsconfig-paths-webpack-plugin": "^4.0.1",
"typed-css-modules": "^0.7.2",
"typescript": "5.1.6",
- "url-loader": "^4.1.1",
- "wait-on": "^6.0.0",
- "webpack": "^5.76.0",
- "webpack-cli": "^4.3.1",
- "webpack-dev-middleware": "^4.0.2",
- "webpack-dev-server": "^3.11.1",
- "webpack-manifest-plugin": "^3.0.0",
- "webpack-merge": "^4.2.1",
- "webpackbar": "^4.0.0"
+ "vite": "^5.4.11",
+ "wait-on": "^6.0.0"
},
"dependencies": {
"@babel/eslint-parser": "^7.25.8",
diff --git a/postcss.config.js b/postcss.config.js
index 856c311..0781fb4 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1,8 +1,9 @@
-const tailwindcss = require('tailwindcss');
+import tailwindcss from 'tailwindcss';
+import postcssPresetEnv from 'postcss-preset-env';
-module.exports = {
+export default {
plugins: [
- 'postcss-preset-env',
- tailwindcss
+ tailwindcss,
+ postcssPresetEnv
],
-};
\ No newline at end of file
+};
diff --git a/src/client/index.tsx b/src/client/index.tsx
index ce1223d..7b7c57b 100755
--- a/src/client/index.tsx
+++ b/src/client/index.tsx
@@ -1,6 +1,5 @@
-import 'source-map-support/register';
+import '../style/tailwind.css';
-import React from 'react';
import { createRoot } from 'react-dom/client';
import {
createBrowserRouter,
diff --git a/src/server/http.ts b/src/server/http.ts
index 0f22969..f0a68e6 100644
--- a/src/server/http.ts
+++ b/src/server/http.ts
@@ -1,4 +1,3 @@
-import 'regenerator-runtime/runtime';
import 'source-map-support/register';
import * as Sentry from '@sentry/node';
@@ -9,11 +8,17 @@ import {
} from 'react-router-dom/server';
import express, { Express, NextFunction, Request, Response } from 'express';
import { readFile } from 'fs/promises';
+import { existsSync } from 'fs';
+import { resolve, dirname } from 'path';
+import { fileURLToPath } from 'url';
import compression from 'compression';
import { config } from './config';
import { render } from './render';
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
const PROD: boolean = process.env.NODE_ENV === 'production';
const app: Express = express();
@@ -49,6 +54,36 @@ app.use('/static', express.static(__dirname + '/static'));
app.use(express.static('static'));
app.use(express.static(__dirname + '/static'));
+// Helper to load CSS from Vite manifest
+const loadCssFromManifest = async (): Promise => {
+ const manifestPath = resolve(__dirname, 'static/client-manifest.json');
+ if (!existsSync(manifestPath)) {
+ return [];
+ }
+
+ try {
+ const manifestContent = await readFile(manifestPath, 'utf-8');
+ const manifest = JSON.parse(manifestContent);
+ const cssFiles: string[] = [];
+
+ for (const entry of Object.values(manifest) as { css?: string[] }[]) {
+ if (entry.css) {
+ for (const cssFile of entry.css) {
+ const cssPath = resolve(__dirname, 'static', cssFile);
+ if (existsSync(cssPath)) {
+ const cssContent = await readFile(cssPath, 'utf-8');
+ cssFiles.push(cssContent);
+ }
+ }
+ }
+ }
+
+ return cssFiles;
+ } catch {
+ return [];
+ }
+};
+
// All other routes will be directed to React
app.get('*', async (request: Request, res: Response) => {
const { query, dataRoutes } = createStaticHandler(routes);
@@ -61,16 +96,8 @@ app.get('*', async (request: Request, res: Response) => {
const router = createStaticRouter(dataRoutes, context);
- // Load production css if present
- // Gross but does the job for now
- const assets = __ASSETS__ ?? {};
- const keys = Object.keys(assets);
- const css = keys.filter((a) => a.includes('.css') && !a.includes('.map'));
- const cssPayload = css.map((key) => assets[key]);
- const cssHydrate = cssPayload.map(async (pred) =>
- (await readFile(`${__dirname}${pred}`)).toString()
- );
- const cssHydrated = await Promise.all(cssHydrate);
+ // Load production css from Vite manifest
+ const cssHydrated = await loadCssFromManifest();
// Just inline the CSS for better page perf
return render(config, router, context, res, cssHydrated);
diff --git a/src/style/tailwind.css b/src/style/tailwind.css
index 29f1ca2..207f80b 100644
--- a/src/style/tailwind.css
+++ b/src/style/tailwind.css
@@ -1,12 +1,9 @@
-/* webpackIgnore: true */
+@import "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900";
+
@tailwind base;
-/* webpackIgnore: true */
@tailwind components;
-/* webpackIgnore: true */
@tailwind utilities;
-@import "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900";
-
@font-face {
font-family: 'HarmanSans';
src: url('https://devnotnull-ui-production.s3.eu-west-2.amazonaws.com/media/Harman-Sans.woff2') format('opentype');
@@ -16,8 +13,8 @@
}
@layer base {
- html {
- font-family: "Roboto", sans-serif;
- font-weight: 100;
- }
-}
\ No newline at end of file
+ html {
+ font-family: "Roboto", sans-serif;
+ font-weight: 100;
+ }
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index c5522b9..198b596 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,15 +1,17 @@
+import tailwindAnimated from 'tailwindcss-animated';
+
/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ["./src/**/*.{js,jsx,ts,tsx}"],
+export default {
+ content: ["./src/**/*.{js,jsx,ts,tsx}", "./index.html"],
theme: {
container: {
- screens: {
- 'sm': '100%',
- 'md': '100%',
- 'lg': '900px',
- 'xl': '1000px',
- '2xl': '1200px',
- }
+ screens: {
+ 'sm': '100%',
+ 'md': '100%',
+ 'lg': '900px',
+ 'xl': '1000px',
+ '2xl': '1200px',
+ }
},
fontSize: {
xs: ['0.8125rem', { lineHeight: '1.5rem' }],
@@ -29,8 +31,8 @@ module.exports = {
fontFamily: {
harman: ['HarmanSans', 'sans-serif'],
},
-},
-plugins: [
- require('tailwindcss-animated')
-],
-};
\ No newline at end of file
+ },
+ plugins: [
+ tailwindAnimated
+ ],
+};
diff --git a/tsconfig.json b/tsconfig.json
index df1a1c9..55460f8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
"outDir": "dist",
- "module": "commonjs",
- "target": "es5",
- "lib": ["dom", "es2018"],
+ "module": "ESNext",
+ "target": "ES2020",
+ "lib": ["dom", "ES2020", "DOM.Iterable"],
"baseUrl": ".",
"paths": {
"@models/*": ["src/models/*"],
@@ -17,9 +17,9 @@
"skipLibCheck": true,
"sourceMap": true,
"allowJs": true,
- "jsx": "react",
- "moduleResolution": "node",
- "forceConsistentCasingInFileNames": false,
+ "jsx": "react-jsx",
+ "moduleResolution": "bundler",
+ "forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": false,
@@ -27,16 +27,18 @@
"noUnusedLocals": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
- "experimentalDecorators": true,
+ "isolatedModules": true,
"typeRoots": [
"./typings",
"node_modules/@types"
- ]
+ ]
},
+ "include": ["src"],
"exclude": [
"node_modules",
- "webpack",
+ "webpack",
"typings",
- "dist"
+ "dist",
+ "build"
]
-}
\ No newline at end of file
+}
diff --git a/vite.config.server.ts b/vite.config.server.ts
new file mode 100644
index 0000000..66425fd
--- /dev/null
+++ b/vite.config.server.ts
@@ -0,0 +1,37 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { resolve } from 'path';
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@models': resolve(__dirname, 'src/models'),
+ '@client': resolve(__dirname, 'src/client'),
+ '@server': resolve(__dirname, 'src/server'),
+ '@core': resolve(__dirname, 'src/core'),
+ '@components': resolve(__dirname, 'src/components'),
+ '@config': resolve(__dirname, 'src/config'),
+ '@styles': resolve(__dirname, 'src/style'),
+ },
+ },
+ build: {
+ ssr: true,
+ outDir: 'build',
+ emptyOutDir: false, // Don't delete the static folder from client build
+ rollupOptions: {
+ input: resolve(__dirname, 'src/server/server.ts'),
+ output: {
+ entryFileNames: 'server.js',
+ format: 'esm',
+ },
+ },
+ sourcemap: true,
+ },
+ ssr: {
+ noExternal: ['react-helmet-async'],
+ },
+ css: {
+ postcss: './postcss.config.js',
+ },
+});
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..5795587
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,46 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { resolve } from 'path';
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@models': resolve(__dirname, 'src/models'),
+ '@client': resolve(__dirname, 'src/client'),
+ '@server': resolve(__dirname, 'src/server'),
+ '@core': resolve(__dirname, 'src/core'),
+ '@components': resolve(__dirname, 'src/components'),
+ '@config': resolve(__dirname, 'src/config'),
+ '@styles': resolve(__dirname, 'src/style'),
+ },
+ },
+ build: {
+ outDir: 'build/static',
+ manifest: 'client-manifest.json',
+ rollupOptions: {
+ input: resolve(__dirname, 'src/client/index.tsx'),
+ output: {
+ entryFileNames: 'js/[name].[hash].js',
+ chunkFileNames: 'js/[name].[hash].js',
+ assetFileNames: (assetInfo) => {
+ if (assetInfo.name?.endsWith('.css')) {
+ return 'css/[name].[hash][extname]';
+ }
+ return 'assets/[name].[hash][extname]';
+ },
+ },
+ },
+ sourcemap: true,
+ },
+ css: {
+ postcss: './postcss.config.js',
+ },
+ server: {
+ port: 9000,
+ cors: true,
+ },
+ optimizeDeps: {
+ include: ['react', 'react-dom', 'react-router-dom'],
+ },
+});