diff --git a/examples/with-nest/README.md b/examples/with-nest/README.md new file mode 100644 index 0000000..9e5b652 --- /dev/null +++ b/examples/with-nest/README.md @@ -0,0 +1,79 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework with Reactus and React. + +## Project setup + +```bash +$ yarn +``` + +## Compile and run the project + +```bash +# development +$ yarn start + +# watch mode (nest start --watch) +$ yarn start:dev + +# production mode +$ yarn start:prod +``` + +## Run tests + +```bash +# unit tests +$ yarn test +``` + +## Resources + +Check out a few resources that may come in handy when working with NestJS: + +- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. +- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). +- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). +- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. +- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). +- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). +- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). +- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myƛliwiec](https://twitter.com/kammysliwiec) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/examples/with-nest/components/Edit.tsx b/examples/with-nest/components/Edit.tsx new file mode 100644 index 0000000..5a0c205 --- /dev/null +++ b/examples/with-nest/components/Edit.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export default function Edit({ name }: { name: string }) { + return ( +

+ Edit pages/{name}.tsx and save to test HMR +

+ ) + } \ No newline at end of file diff --git a/examples/with-nest/jest.config.mjs b/examples/with-nest/jest.config.mjs new file mode 100644 index 0000000..bca8be1 --- /dev/null +++ b/examples/with-nest/jest.config.mjs @@ -0,0 +1,11 @@ +export default { + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + extensionsToTreatAsEsm: ['.ts'], + transform: { + '^.+\\.ts$': ['ts-jest', { useESM: true }] + }, + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1' + } +}; diff --git a/examples/with-nest/nest-cli.json b/examples/with-nest/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/examples/with-nest/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/examples/with-nest/package.json b/examples/with-nest/package.json new file mode 100644 index 0000000..9934a3a --- /dev/null +++ b/examples/with-nest/package.json @@ -0,0 +1,71 @@ +{ + "name": "reactus-with-nest", + "version": "1.0.0", + "private": true, + "license": "UNLICENSED", + "type": "module", + "scripts": { + "build": "tsx scripts/build.ts", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "NODE_ENV=production nest start --prod", + "test": "jest --config jest.config.mjs", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "@nestjs/platform-express": "^11.0.1", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "reactus": "^0.2.10", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@swc/cli": "^0.6.0", + "@swc/core": "^1.10.7", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.15.3", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.3", + "@types/supertest": "^6.0.2", + "@vitejs/plugin-react": "^4.4.1", + "globals": "^16.0.0", + "jest": "^29.7.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "tsx": "^4.19.4", + "typescript": "^5.8.3", + "vite": "^6.3.4" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/examples/with-nest/pages/about.tsx b/examples/with-nest/pages/about.tsx new file mode 100644 index 0000000..f7aecb8 --- /dev/null +++ b/examples/with-nest/pages/about.tsx @@ -0,0 +1,29 @@ +import './page.css'; +import React from 'react'; +import Edit from '../components/Edit.js'; + +export function Head({ styles = [] }: { styles?: string[] }) { + return ( + <> + About Reactus + + + + {styles.map((href, index) => ( + + ))} + + ) +} + +export default function AboutPage() { + return ( + <> +

About Reactus

+
+ + Home Page +
+ + ) +} \ No newline at end of file diff --git a/examples/with-nest/pages/home.tsx b/examples/with-nest/pages/home.tsx new file mode 100644 index 0000000..9708233 --- /dev/null +++ b/examples/with-nest/pages/home.tsx @@ -0,0 +1,34 @@ +import './page.css'; +import React, { useState } from 'react'; +import Edit from '../components/Edit.js'; + +export function Head({ styles = [] }: { styles?: string[] }) { + return ( + <> + Reactus + + + + {styles.map((href, index) => ( + + ))} + + ) +} + +export default function HomePage() { + const [count, setCount] = useState(0) + + return ( + <> +

React + Reactus with NestJS!

+
+ + + About Reactus +
+ + ) +} \ No newline at end of file diff --git a/examples/with-nest/pages/page.css b/examples/with-nest/pages/page.css new file mode 100644 index 0000000..30e8b0c --- /dev/null +++ b/examples/with-nest/pages/page.css @@ -0,0 +1,7 @@ +:root { display: initial; } +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} \ No newline at end of file diff --git a/examples/with-nest/public/global.css b/examples/with-nest/public/global.css new file mode 100644 index 0000000..bcf1636 --- /dev/null +++ b/examples/with-nest/public/global.css @@ -0,0 +1,70 @@ +:root { + display: none; + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} \ No newline at end of file diff --git a/examples/with-nest/scripts/build.ts b/examples/with-nest/scripts/build.ts new file mode 100644 index 0000000..aa109d4 --- /dev/null +++ b/examples/with-nest/scripts/build.ts @@ -0,0 +1,41 @@ +//node +import path from 'node:path'; +//reactus +import { build } from 'reactus'; + +async function builder() { + const cwd = process.cwd(); + const engine = build({ + cwd, + //path where to save assets (css, images, etc) + assetPath: path.join(cwd, 'public/assets'), + //path where to save and load (live) the client scripts (js) + clientPath: path.join(cwd, 'public/client'), + //path where to save and load (live) the server script (js) + pagePath: path.join(cwd, '.build/pages') + }); + + await engine.set('@/pages/home'); + await engine.set('@/pages/about'); + + const responses = [ + ...await engine.buildAllClients(), + ...await engine.buildAllAssets(), + ...await engine.buildAllPages() + ].map(response => { + const results = response.results; + if (typeof results?.contents === 'string') { + results.contents = results.contents.substring(0, 100) + ' ...'; + } + return results; + }); + + //console.log(responses); + //fix for unused variable :) + if (responses.length) return; +} + +builder().catch(e => { + console.error(e); + process.exit(1); +}); \ No newline at end of file diff --git a/examples/with-nest/src/app.controller.spec.ts b/examples/with-nest/src/app.controller.spec.ts new file mode 100644 index 0000000..676cc6c --- /dev/null +++ b/examples/with-nest/src/app.controller.spec.ts @@ -0,0 +1,59 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppController } from './app.controller.js'; +import { AppService } from './app.service.js'; +import { ReactusService } from './reactus/reactus.service.js'; +import { Response } from 'express' + +describe('AppController', () => { + let appController: AppController; + let reactusServiceMock: Partial; + + beforeEach(async () => { + reactusServiceMock = { + render: jest.fn().mockResolvedValue('Mocked Page'), + }; + + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [ + AppService, + { + provide: ReactusService, + useValue: reactusServiceMock, + }, + ], + }).compile(); + + appController = app.get(AppController); + }); + + it('should be defined', () => { + expect(appController).toBeDefined(); + }); + + it('should render the home page', async () => { + const res = { + setHeader: jest.fn(), + end: jest.fn(), + } as unknown as Response; + + await appController.home(res); + + expect(reactusServiceMock.render).toHaveBeenCalledWith('@/pages/home', { title: 'Home' }); + expect(res.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html'); + expect(res.end).toHaveBeenCalledWith('Mocked Page'); + }); + + it('should render the about page', async () => { + const res = { + setHeader: jest.fn(), + end: jest.fn(), + } as unknown as Response; + + await appController.about(res); + + expect(reactusServiceMock.render).toHaveBeenCalledWith('@/pages/about', { title: 'About' }); + expect(res.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html'); + expect(res.end).toHaveBeenCalledWith('Mocked Page'); + }); +}); diff --git a/examples/with-nest/src/app.controller.ts b/examples/with-nest/src/app.controller.ts new file mode 100644 index 0000000..bf2535d --- /dev/null +++ b/examples/with-nest/src/app.controller.ts @@ -0,0 +1,25 @@ +import { Controller, Get, Res } from '@nestjs/common'; +import { ReactusService } from './reactus/reactus.service.js'; +import { Response } from 'express'; + +@Controller() +export class AppController { + constructor(private readonly reactusService: ReactusService) {} + + @Get('/') + async home(@Res() res: Response) { + res.setHeader('Content-Type', 'text/html'); + const html = await this.reactusService.render('@/pages/home', { + title: 'Home', + }); + res.end(html); + } + @Get('/about') + async about(@Res() res: Response) { + res.setHeader('Content-Type', 'text/html'); + const html = await this.reactusService.render('@/pages/about', { + title: 'About', + }); + res.end(html); + } +} diff --git a/examples/with-nest/src/app.module.ts b/examples/with-nest/src/app.module.ts new file mode 100644 index 0000000..e218222 --- /dev/null +++ b/examples/with-nest/src/app.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AppController } from './app.controller.js'; +import { AppService } from './app.service.js'; +import { ReactusService } from './reactus/reactus.service.js'; + +@Module({ + imports: [], + controllers: [AppController], + providers: [AppService, ReactusService], +}) +export class AppModule {} diff --git a/examples/with-nest/src/app.service.ts b/examples/with-nest/src/app.service.ts new file mode 100644 index 0000000..927d7cc --- /dev/null +++ b/examples/with-nest/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/examples/with-nest/src/main.ts b/examples/with-nest/src/main.ts new file mode 100644 index 0000000..9980097 --- /dev/null +++ b/examples/with-nest/src/main.ts @@ -0,0 +1,47 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module.js'; +import { NestExpressApplication } from '@nestjs/platform-express'; +import { ReactusService } from './reactus/reactus.service.js'; +import { join } from 'path'; +import sirv from 'sirv'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const express = app.getHttpAdapter().getInstance(); + const cwd = process.cwd(); + + const reactusService = app.get(ReactusService); + + // Serve static assets in production + if (process.env.NODE_ENV === 'production') { + express.use( + '/client', + sirv(join(cwd, 'public/client'), { maxAge: 31536000, immutable: true }) + ); + express.use( + '/assets', + sirv(join(cwd, 'public/assets'), { maxAge: 31536000, immutable: true }) + ); + express.use( + '/public', + sirv(join(cwd, 'public'), { maxAge: 31536000, immutable: true }) + ); + } + + // In dev mode, use the Reactus service for rendering and asset handling + express.use(async (req, res, next) => { + if (process.env.NODE_ENV !== 'production') { + // Handle assets and rendering in dev mode + await reactusService.handleAssets(req, res); + } + if (!res.headersSent) next(); + }); + + await app.listen(process.env.PORT ?? 3000); + console.log(`Server is listening at http://localhost:${process.env.PORT ?? 3000}`); +} + +bootstrap().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/examples/with-nest/src/reactus/reactus.service.spec.ts b/examples/with-nest/src/reactus/reactus.service.spec.ts new file mode 100644 index 0000000..74f40b1 --- /dev/null +++ b/examples/with-nest/src/reactus/reactus.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ReactusService } from './reactus.service.js'; + +describe('ReactusService', () => { + let service: ReactusService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ReactusService], + }).compile(); + + service = module.get(ReactusService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/examples/with-nest/src/reactus/reactus.service.ts b/examples/with-nest/src/reactus/reactus.service.ts new file mode 100644 index 0000000..0966eff --- /dev/null +++ b/examples/with-nest/src/reactus/reactus.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { IncomingMessage } from 'http'; +import { dev, serve } from 'reactus'; +import { SR } from 'reactus/types'; +import path from 'path'; + +@Injectable() +export class ReactusService { + private readonly engine: + | ReturnType + | ReturnType; + + private readonly isDev = process.env.NODE_ENV !== 'production'; + + constructor() { + const cwd = process.cwd(); + + if (this.isDev) { + this.engine = dev({ cwd }); + } else { + this.engine = serve({ + cwd, + clientRoute: '/client', + pagePath: path.join(cwd, '.build/pages'), + cssRoute: '/assets', + }); + } + } + + async render(path: string, props: Record = {}): Promise { + return this.engine.render(path, props); + } + + async handleAssets(req: IncomingMessage, res: SR) { + if (this.isDev && 'http' in this.engine) { + await this.engine.http(req, res); + } + } +} diff --git a/examples/with-nest/tsconfig.build.json b/examples/with-nest/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/examples/with-nest/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/examples/with-nest/tsconfig.json b/examples/with-nest/tsconfig.json new file mode 100644 index 0000000..a55bde1 --- /dev/null +++ b/examples/with-nest/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "jsx": "react", + "module": "nodenext", + "moduleResolution": "nodenext", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false + } + } + \ No newline at end of file diff --git a/package.json b/package.json index 6479e14..618c0b7 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "fastify": "yarn --cwd examples/with-fastify", "hapi": "yarn --cwd examples/with-hapi", "http": "yarn --cwd examples/with-http", + "nest": "yarn --cwd examples/with-nest", "restify": "yarn --cwd examples/with-restify", "tailwind": "yarn --cwd examples/with-tailwind", "unocss": "yarn --cwd examples/with-unocss",