Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
87 changes: 79 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,98 @@
# Devnotnull.com React application

Last updated - 26/08/2022
Last updated - 01/02/2026

[![On merge to main](https://github.com/devisnotnull/devnotnull.com-app/actions/workflows/on_merge_main.yml/badge.svg)](https://github.com/devisnotnull/devnotnull.com-app/actions/workflows/on_merge_main.yml)

<img width="1258" alt="Screenshot 2022-05-24 at 22 11 38" src="https://user-images.githubusercontent.com/702691/170133701-79db584a-e95a-4ac5-8a38-3163fa18a77c.png">

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.
Binary file modified bun.lockb
Binary file not shown.
12 changes: 12 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DevNotNull</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client/index.tsx"></script>
</body>
</html>
92 changes: 14 additions & 78 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
11 changes: 6 additions & 5 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -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
],
};
};
3 changes: 1 addition & 2 deletions src/client/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
49 changes: 38 additions & 11 deletions src/server/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'regenerator-runtime/runtime';
import 'source-map-support/register';

import * as Sentry from '@sentry/node';
Expand All @@ -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();

Expand Down Expand Up @@ -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<string[]> => {
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);
Expand All @@ -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);
Expand Down
Loading
Loading