diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..2648c44 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,31 @@ +{ + "mcpServers": { + "nx-mcp": { + "url": "http://localhost:9774/sse" + }, + "playwright": { + "command": "npx", + "args": [ + "-y", + "@playwright/mcp@latest", + "--vision" + ] + }, + "postgres": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-postgres", + "postgresql://postgres:postgres@localhost:5432/postgres" + ] + }, + "stripe": { + "command": "npx", + "args": ["-y", "@stripe/mcp@latest", "--tools=all"] + }, + "context7": { + "command": "npx", + "args": ["-y", "@upstash/context7-mcp@latest"] + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 54613c9..bb9a890 100644 --- a/.gitignore +++ b/.gitignore @@ -41,9 +41,14 @@ Thumbs.db .nx/cache .nx/workspace-data +vite.config.*.timestamp* +vitest.config.*.timestamp* +**/vite.config.{js,ts,mjs,mts,cjs,cts}.timestamp* + +test-output + storybook-static # env files -.env -**/vite.config.{js,ts,mjs,mts,cjs,cts}.timestamp* \ No newline at end of file +.env \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..1c00299 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +legacy-peer-deps=true +engine-strict=false \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 97e81d4..b06bf3f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,7 @@ "recommendations": [ "nrwl.angular-console", "esbenp.prettier-vscode", - "firsttris.vscode-jest-runner" + "firsttris.vscode-jest-runner", + "ms-playwright.playwright" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index df404fa..ce602ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "nxConsole.nxWorkspacePath": "${workspaceFolder}/nx.json" -} \ No newline at end of file + "nxConsole.nxWorkspacePath": "${workspaceFolder}/nx.json", + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "nxConsole.generateAiAgentRules": true +} diff --git a/Dockerfile b/Dockerfile index 027aab9..d4112ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ WORKDIR /app # Copy package.json and package-lock.json COPY package.json . COPY package-lock.json . +COPY .npmrc . # Clean npm cache and rebuild node-gyp RUN npm cache clean --force diff --git a/README.md b/README.md index 664d4ec..156c24f 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,9 @@ stripe trigger payment_intent.succeeded stripe listen --forward-to localhost:8081/order/webhook // or using the secure tunnel created by Ngrok ``` +## MCP Servers +- [Nx MCP Server](https://nx.dev/blog/nx-made-cursor-smarter) + ## Supporting 🍻 I believe in Unicorns 🦄 Support [me](http://www.paypal.me/jdnichollsc/2), if you do too. diff --git a/apps/auth-e2e/eslint.config.js b/apps/auth-e2e/eslint.config.js deleted file mode 100644 index df7cfc2..0000000 --- a/apps/auth-e2e/eslint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -const baseConfig = require('../../eslint.config.js'); - -module.exports = [...baseConfig]; diff --git a/apps/auth-e2e/jest.config.ts b/apps/auth-e2e/jest.config.ts deleted file mode 100644 index b27f299..0000000 --- a/apps/auth-e2e/jest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -export default { - displayName: 'auth-e2e', - preset: '../../jest.preset.js', - globalSetup: '/src/support/global-setup.ts', - globalTeardown: '/src/support/global-teardown.ts', - setupFiles: ['/src/support/test-setup.ts'], - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': [ - 'ts-jest', - { - tsconfig: '/tsconfig.spec.json', - }, - ], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/auth-e2e', -}; diff --git a/apps/auth-e2e/project.json b/apps/auth-e2e/project.json deleted file mode 100644 index 5acd3ef..0000000 --- a/apps/auth-e2e/project.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "auth-e2e", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "application", - "implicitDependencies": ["auth"], - "targets": { - "e2e": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{e2eProjectRoot}"], - "options": { - "jestConfig": "apps/auth-e2e/jest.config.ts", - "passWithNoTests": true - }, - "dependsOn": ["auth:build"] - } - } -} diff --git a/apps/auth-e2e/src/auth/auth.spec.ts b/apps/auth-e2e/src/auth/auth.spec.ts deleted file mode 100644 index e8ac2a6..0000000 --- a/apps/auth-e2e/src/auth/auth.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'axios'; - -describe('GET /api', () => { - it('should return a message', async () => { - const res = await axios.get(`/api`); - - expect(res.status).toBe(200); - expect(res.data).toEqual({ message: 'Hello API' }); - }); -}); diff --git a/apps/auth-e2e/src/support/global-setup.ts b/apps/auth-e2e/src/support/global-setup.ts deleted file mode 100644 index c1f5144..0000000 --- a/apps/auth-e2e/src/support/global-setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var __TEARDOWN_MESSAGE__: string; - -module.exports = async function () { - // Start services that that the app needs to run (e.g. database, docker-compose, etc.). - console.log('\nSetting up...\n'); - - // Hint: Use `globalThis` to pass variables to global teardown. - globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n'; -}; diff --git a/apps/auth-e2e/src/support/global-teardown.ts b/apps/auth-e2e/src/support/global-teardown.ts deleted file mode 100644 index 32ea345..0000000 --- a/apps/auth-e2e/src/support/global-teardown.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable */ - -module.exports = async function () { - // Put clean up logic here (e.g. stopping services, docker-compose, etc.). - // Hint: `globalThis` is shared between setup and teardown. - console.log(globalThis.__TEARDOWN_MESSAGE__); -}; diff --git a/apps/auth-e2e/src/support/test-setup.ts b/apps/auth-e2e/src/support/test-setup.ts deleted file mode 100644 index 07f2870..0000000 --- a/apps/auth-e2e/src/support/test-setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ - -import axios from 'axios'; - -module.exports = async function () { - // Configure axios for tests to use. - const host = process.env.HOST ?? 'localhost'; - const port = process.env.PORT ?? '3000'; - axios.defaults.baseURL = `http://${host}:${port}`; -}; diff --git a/apps/auth-e2e/tsconfig.json b/apps/auth-e2e/tsconfig.json deleted file mode 100644 index ed633e1..0000000 --- a/apps/auth-e2e/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.spec.json" - } - ], - "compilerOptions": { - "esModuleInterop": true - } -} diff --git a/apps/auth-e2e/tsconfig.spec.json b/apps/auth-e2e/tsconfig.spec.json deleted file mode 100644 index d7f9cf2..0000000 --- a/apps/auth-e2e/tsconfig.spec.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": ["jest.config.ts", "src/**/*.ts"] -} diff --git a/apps/auth/.spec.swcrc b/apps/auth/.spec.swcrc new file mode 100644 index 0000000..3b52a53 --- /dev/null +++ b/apps/auth/.spec.swcrc @@ -0,0 +1,22 @@ +{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] +} diff --git a/apps/auth/eslint.config.js b/apps/auth/eslint.config.js deleted file mode 100644 index df7cfc2..0000000 --- a/apps/auth/eslint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -const baseConfig = require('../../eslint.config.js'); - -module.exports = [...baseConfig]; diff --git a/apps/auth/eslint.config.mjs b/apps/auth/eslint.config.mjs new file mode 100644 index 0000000..b7f6277 --- /dev/null +++ b/apps/auth/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [...baseConfig]; diff --git a/apps/auth/jest.config.ts b/apps/auth/jest.config.ts index 24b3d19..b8fbcb7 100644 --- a/apps/auth/jest.config.ts +++ b/apps/auth/jest.config.ts @@ -1,19 +1,21 @@ -import type { Config } from '@jest/types'; +/* eslint-disable */ +import { readFileSync } from 'fs'; -const config: Config.InitialOptions = { - displayName: 'auth', +// Reading the SWC compilation config for the spec files +const swcJestConfig = JSON.parse( + readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8') +); + +// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves +swcJestConfig.swcrc = false; + +export default { + displayName: '@projectx/auth', preset: '../../jest.preset.js', - globals: { - 'ts-jest': { - tsconfig: '/tsconfig.spec.json', - }, - }, - coverageDirectory: '../../coverage/apps/auth', + testEnvironment: 'node', transform: { - '^.+\\.[tj]s$': 'ts-jest', + '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], }, moduleFileExtensions: ['ts', 'js', 'html'], - testEnvironment: 'node', + coverageDirectory: 'test-output/jest/coverage', }; - -export default config; diff --git a/apps/auth/project.json b/apps/auth/project.json index d5b2794..6956bf9 100644 --- a/apps/auth/project.json +++ b/apps/auth/project.json @@ -19,7 +19,6 @@ "apps/auth/src/assets", "apps/auth/src/workflows" ], - "isolatedConfig": true, "webpackConfig": "apps/auth/webpack.config.js" }, "configurations": { @@ -27,7 +26,10 @@ "mode": "development" }, "production": { - "mode": "production" + "mode": "production", + "optimization": true, + "extractLicenses": true, + "inspect": false } } }, @@ -40,12 +42,15 @@ }, "configurations": { "development": { - "buildTarget": "auth:build:development" + "buildTarget": "auth:build:development", + "watch": true, + "inspect": true }, "production": { - "buildTarget": "auth:build:production" + "buildTarget": "auth:build:production", + "watch": false } } } } -} +} \ No newline at end of file diff --git a/apps/auth/src/app/app.controller.spec.ts b/apps/auth/src/app/app.controller.spec.ts index 27083bf..e946742 100644 --- a/apps/auth/src/app/app.controller.spec.ts +++ b/apps/auth/src/app/app.controller.spec.ts @@ -23,7 +23,7 @@ describe('AppController', () => { it('should do something', () => { const appController = app.get(AppController); expect(appController.login({ email: 'test@test.com' })).toBeDefined(); - expect(appService.sendLoginEmail).toHaveBeenCalled(); + expect(appService.login).toHaveBeenCalled(); }); }); }); diff --git a/apps/auth/src/app/app.module.ts b/apps/auth/src/app/app.module.ts index 4b6ec79..f38d194 100644 --- a/apps/auth/src/app/app.module.ts +++ b/apps/auth/src/app/app.module.ts @@ -32,7 +32,7 @@ import { ActivitiesModule } from './activities/activities.module'; EmailModule, WorkflowsModule.registerAsync({ imports: [ActivitiesModule], - useFactory: async (activitiesService: ActivitiesService) => ({ + useFactory: (activitiesService: ActivitiesService) => ({ activitiesService, workflowsPath: path.join(__dirname, '/workflows'), }), diff --git a/apps/auth/src/app/app.service.ts b/apps/auth/src/app/app.service.ts index adabd3c..8297c3a 100644 --- a/apps/auth/src/app/app.service.ts +++ b/apps/auth/src/app/app.service.ts @@ -15,17 +15,35 @@ import { loginUserWorkflow } from '../workflows'; @Injectable() export class AppService { readonly logger = new Logger(AppService.name); + private readonly taskQueue: string; constructor( private readonly configService: ConfigService, private readonly clientService: ClientService, private readonly authService: AuthService - ) {} + ) { + const taskQueue = this.configService.get('temporal.taskQueue'); + if (!taskQueue) { + throw new Error('Task queue not found'); + } + this.taskQueue = taskQueue; + } getWorkflowIdByEmail(email: string) { return `login-${email}`; } + getWorkflowClient() { + const workflowClient = this.clientService.client?.workflow; + if (!workflowClient) { + throw new HttpException( + 'The workflow client was not initialized correctly', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + return workflowClient; + } + /** * Initiates the login process by sending a verification email. * @param body AuthLoginDto containing the user's email. @@ -33,11 +51,10 @@ export class AppService { */ async login(body: AuthLoginDto) { this.logger.log(`sendLoginEmail(${body.email}) - sending email`); - const taskQueue = this.configService.get('temporal.taskQueue'); try { - await this.clientService.client?.workflow.start(loginUserWorkflow, { + await this.getWorkflowClient().start(loginUserWorkflow, { args: [body], - taskQueue, + taskQueue: this.taskQueue, workflowId: this.getWorkflowIdByEmail(body.email), searchAttributes: { Email: [body.email], @@ -72,7 +89,7 @@ export class AppService { const workflowId = this.getWorkflowIdByEmail(body.email); const description = await getWorkflowDescription( - this.clientService.client?.workflow, + this.getWorkflowClient(), workflowId ); const isLoginRunning = isWorkflowRunning(description); @@ -81,7 +98,7 @@ export class AppService { throw new HttpException('The code has expired', HttpStatus.BAD_REQUEST); } - const handle = this.clientService.client?.workflow.getHandle(workflowId); + const handle = this.getWorkflowClient().getHandle(workflowId); const result = await handle.executeUpdate(verifyLoginCodeUpdate, { args: [body.code], }); diff --git a/apps/auth/src/app/user/user.controller.ts b/apps/auth/src/app/user/user.controller.ts index 7f34919..0e3ae5e 100644 --- a/apps/auth/src/app/user/user.controller.ts +++ b/apps/auth/src/app/user/user.controller.ts @@ -14,7 +14,7 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { AuthUser, JwtAuthGuard, AuthenticatedUser } from '@projectx/core'; +import { type AuthUser, JwtAuthGuard, AuthenticatedUser } from '@projectx/core'; import { UserDto, UserStatus } from '@projectx/models'; import { UserService } from './user.service'; diff --git a/apps/auth/src/config/app.config.ts b/apps/auth/src/config/app.config.ts index 0b298ed..5ac89ca 100644 --- a/apps/auth/src/config/app.config.ts +++ b/apps/auth/src/config/app.config.ts @@ -1,10 +1,10 @@ import { registerAs } from '@nestjs/config'; export default registerAs('app', () => ({ - port: Number(process.env.AUTH_PORT) || 8081, - environment: process.env.NODE_ENV, apiPrefix: 'auth', - allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') ?? [], - logLevel: process.env.LOG_LEVEL ?? 'info', - jwtSecret: process.env.JWT_SECRET, + port: Number(process.env['AUTH_PORT']) || 8081, + environment: process.env['NODE_ENV'], + allowedOrigins: process.env['ALLOWED_ORIGINS']?.split(',') ?? [], + logLevel: process.env['LOG_LEVEL'] ?? 'info', + jwtSecret: process.env['JWT_SECRET'], })); diff --git a/apps/auth/src/config/env.config.ts b/apps/auth/src/config/env.config.ts index 91d3654..32eb1fb 100644 --- a/apps/auth/src/config/env.config.ts +++ b/apps/auth/src/config/env.config.ts @@ -12,19 +12,19 @@ import { export class EnvironmentVariables { @IsEnum(Environment) @IsDefined() - NODE_ENV: Environment; + NODE_ENV!: Environment; @IsInt() @Min(0) @Max(65535) @IsDefined() - AUTH_PORT: number; + AUTH_PORT!: number; @IsString() @IsNotEmpty() - JWT_SECRET: string; + JWT_SECRET!: string; @IsString() @IsNotEmpty() - SENDGRID_API_KEY: string; + SENDGRID_API_KEY!: string; } diff --git a/apps/auth/src/config/temporal.config.ts b/apps/auth/src/config/temporal.config.ts index 68f160f..8935711 100644 --- a/apps/auth/src/config/temporal.config.ts +++ b/apps/auth/src/config/temporal.config.ts @@ -1,7 +1,7 @@ import { registerAs } from '@nestjs/config'; export default registerAs('temporal', () => ({ - host: process.env.TEMPORAL_HOST, - namespace: process.env.TEMPORAL_NAMESPACE || 'default', + host: process.env['TEMPORAL_HOST'], + namespace: process.env['TEMPORAL_NAMESPACE'] || 'default', taskQueue: 'auth', })); diff --git a/apps/auth/src/workflows/login.workflow.ts b/apps/auth/src/workflows/login.workflow.ts index 546ea6b..a8aed40 100644 --- a/apps/auth/src/workflows/login.workflow.ts +++ b/apps/auth/src/workflows/login.workflow.ts @@ -55,6 +55,11 @@ export async function loginUserWorkflow( setHandler( verifyLoginCodeUpdate, async (code) => { + if (!state.code) { + throw ApplicationFailure.nonRetryable( + 'Login code not found', + ); + } const user = await verifyLoginCode(data.email, code, state.code); if (user) { state.user = user; diff --git a/apps/auth/tsconfig.app.json b/apps/auth/tsconfig.app.json index 8a1f5c0..a1badb0 100644 --- a/apps/auth/tsconfig.app.json +++ b/apps/auth/tsconfig.app.json @@ -1,12 +1,42 @@ { - "extends": "./tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", + "outDir": "dist", + "module": "nodenext", "types": ["node"], + "rootDir": "src", + "moduleResolution": "nodenext", + "tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo", + "experimentalDecorators": true, "emitDecoratorMetadata": true, "target": "es2021" }, "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} \ No newline at end of file + "exclude": [ + "out-tsc", + "dist", + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs" + ], + "references": [ + { + "path": "../../libs/backend/workflows/tsconfig.lib.json" + }, + { + "path": "../../libs/backend/db/tsconfig.lib.json" + }, + { + "path": "../../libs/models/tsconfig.lib.json" + }, + { + "path": "../../libs/backend/core/tsconfig.lib.json" + }, + { + "path": "../../libs/backend/email/tsconfig.lib.json" + } + ] +} diff --git a/apps/auth/tsconfig.json b/apps/auth/tsconfig.json index c1e2dd4..11f8ba4 100644 --- a/apps/auth/tsconfig.json +++ b/apps/auth/tsconfig.json @@ -1,16 +1,28 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.backend.base.json", "files": [], "include": [], "references": [ + { + "path": "../../libs/backend/workflows" + }, + { + "path": "../../libs/backend/db" + }, + { + "path": "../../libs/models" + }, + { + "path": "../../libs/backend/core" + }, + { + "path": "../../libs/backend/email" + }, { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.spec.json" } - ], - "compilerOptions": { - "esModuleInterop": true - } + ] } diff --git a/apps/auth/tsconfig.spec.json b/apps/auth/tsconfig.spec.json index 9b2a121..18d4e74 100644 --- a/apps/auth/tsconfig.spec.json +++ b/apps/auth/tsconfig.spec.json @@ -1,14 +1,22 @@ { - "extends": "./tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] + "outDir": "./out-tsc/jest", + "types": ["jest", "node"], + "module": "nodenext", + "moduleResolution": "nodenext", + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": [ "jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.app.json" + } ] } diff --git a/apps/auth/webpack.config.js b/apps/auth/webpack.config.js index 8a2eb71..37379bc 100644 --- a/apps/auth/webpack.config.js +++ b/apps/auth/webpack.config.js @@ -1,22 +1,54 @@ const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); const { join } = require('path'); +const nodeExternals = require('webpack-node-externals'); +const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin'); -module.exports = { - output: { - path: join(__dirname, '../../dist/apps/auth'), - }, - devtool: 'source-map', - plugins: [ - new NxAppWebpackPlugin({ - target: 'node', - compiler: 'tsc', - main: './src/main.ts', - tsConfig: './tsconfig.app.json', - assets: ['./src/assets', './src/workflows'], - optimization: false, - outputHashing: 'none', - generatePackageJson: true, - sourceMap: true - }), - ], -}; +module.exports = (options) => { + // Check if we're in development mode + const isDevelopment = options.mode === 'development' || process.env.NODE_ENV === 'development'; + + // Base configuration + const config = { + output: { + path: join(__dirname, '../../dist/apps/auth'), + }, + devtool: 'source-map', + plugins: [ + new NxAppWebpackPlugin({ + target: 'node', + compiler: 'tsc', + main: './src/main.ts', + tsConfig: './tsconfig.app.json', + assets: ['./src/assets', './src/workflows'], + optimization: false, + outputHashing: 'none', + generatePackageJson: true, + sourceMap: true + }), + ], + }; + + // Add HMR configuration in development mode + if (isDevelopment) { + console.log('Enabling Hot Module Replacement for NestJS in development mode'); + + // Modify configuration for development + config.watch = true; + config.entry = ['webpack/hot/poll?500', join(__dirname, './src/main.ts')]; + config.externals = [ + nodeExternals({ + allowlist: ['webpack/hot/poll?500'], + }), + ]; + + // Add HMR plugin + config.plugins.push( + new RunScriptWebpackPlugin({ + name: 'main.js', + autoRestart: true, + }) + ); + } + + return config; +}; \ No newline at end of file diff --git a/apps/order-e2e/eslint.config.js b/apps/order-e2e/eslint.config.js deleted file mode 100644 index df7cfc2..0000000 --- a/apps/order-e2e/eslint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -const baseConfig = require('../../eslint.config.js'); - -module.exports = [...baseConfig]; diff --git a/apps/order-e2e/jest.config.ts b/apps/order-e2e/jest.config.ts deleted file mode 100644 index 8cd5a7e..0000000 --- a/apps/order-e2e/jest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -export default { - displayName: 'order-e2e', - preset: '../../jest.preset.js', - globalSetup: '/src/support/global-setup.ts', - globalTeardown: '/src/support/global-teardown.ts', - setupFiles: ['/src/support/test-setup.ts'], - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': [ - 'ts-jest', - { - tsconfig: '/tsconfig.spec.json', - }, - ], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/order-e2e', -}; diff --git a/apps/order-e2e/project.json b/apps/order-e2e/project.json deleted file mode 100644 index bd3ae87..0000000 --- a/apps/order-e2e/project.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "order-e2e", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "application", - "implicitDependencies": ["order"], - "targets": { - "e2e": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{e2eProjectRoot}"], - "options": { - "jestConfig": "apps/order-e2e/jest.config.ts", - "passWithNoTests": true - }, - "dependsOn": ["order:build"] - } - } -} diff --git a/apps/order-e2e/src/order/order.spec.ts b/apps/order-e2e/src/order/order.spec.ts deleted file mode 100644 index e8ac2a6..0000000 --- a/apps/order-e2e/src/order/order.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'axios'; - -describe('GET /api', () => { - it('should return a message', async () => { - const res = await axios.get(`/api`); - - expect(res.status).toBe(200); - expect(res.data).toEqual({ message: 'Hello API' }); - }); -}); diff --git a/apps/order-e2e/src/support/global-setup.ts b/apps/order-e2e/src/support/global-setup.ts deleted file mode 100644 index c1f5144..0000000 --- a/apps/order-e2e/src/support/global-setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var __TEARDOWN_MESSAGE__: string; - -module.exports = async function () { - // Start services that that the app needs to run (e.g. database, docker-compose, etc.). - console.log('\nSetting up...\n'); - - // Hint: Use `globalThis` to pass variables to global teardown. - globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n'; -}; diff --git a/apps/order-e2e/src/support/global-teardown.ts b/apps/order-e2e/src/support/global-teardown.ts deleted file mode 100644 index 32ea345..0000000 --- a/apps/order-e2e/src/support/global-teardown.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable */ - -module.exports = async function () { - // Put clean up logic here (e.g. stopping services, docker-compose, etc.). - // Hint: `globalThis` is shared between setup and teardown. - console.log(globalThis.__TEARDOWN_MESSAGE__); -}; diff --git a/apps/order-e2e/src/support/test-setup.ts b/apps/order-e2e/src/support/test-setup.ts deleted file mode 100644 index 07f2870..0000000 --- a/apps/order-e2e/src/support/test-setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ - -import axios from 'axios'; - -module.exports = async function () { - // Configure axios for tests to use. - const host = process.env.HOST ?? 'localhost'; - const port = process.env.PORT ?? '3000'; - axios.defaults.baseURL = `http://${host}:${port}`; -}; diff --git a/apps/order-e2e/tsconfig.json b/apps/order-e2e/tsconfig.json deleted file mode 100644 index ed633e1..0000000 --- a/apps/order-e2e/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.spec.json" - } - ], - "compilerOptions": { - "esModuleInterop": true - } -} diff --git a/apps/order-e2e/tsconfig.spec.json b/apps/order-e2e/tsconfig.spec.json deleted file mode 100644 index d7f9cf2..0000000 --- a/apps/order-e2e/tsconfig.spec.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": ["jest.config.ts", "src/**/*.ts"] -} diff --git a/apps/order/.spec.swcrc b/apps/order/.spec.swcrc new file mode 100644 index 0000000..3b52a53 --- /dev/null +++ b/apps/order/.spec.swcrc @@ -0,0 +1,22 @@ +{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] +} diff --git a/apps/order/eslint.config.js b/apps/order/eslint.config.js deleted file mode 100644 index df7cfc2..0000000 --- a/apps/order/eslint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -const baseConfig = require('../../eslint.config.js'); - -module.exports = [...baseConfig]; diff --git a/apps/order/eslint.config.mjs b/apps/order/eslint.config.mjs new file mode 100644 index 0000000..b7f6277 --- /dev/null +++ b/apps/order/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [...baseConfig]; diff --git a/apps/order/jest.config.ts b/apps/order/jest.config.ts index 88bd33f..c417db5 100644 --- a/apps/order/jest.config.ts +++ b/apps/order/jest.config.ts @@ -1,10 +1,24 @@ +/* eslint-disable */ +import { readFileSync } from 'fs'; + +// Reading the SWC compilation config for the spec files +const swcJestConfig = JSON.parse( + readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8') +); + +// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves +swcJestConfig.swcrc = false; + export default { - displayName: 'order', + displayName: '@projectx/order', preset: '../../jest.preset.js', + globalSetup: '/src/support/global-setup.ts', + globalTeardown: '/src/support/global-teardown.ts', + setupFiles: ['/src/support/test-setup.ts'], testEnvironment: 'node', transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], }, moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/apps/order', + coverageDirectory: 'test-output/jest/coverage', }; diff --git a/apps/order/project.json b/apps/order/project.json index c0eaba8..c002382 100644 --- a/apps/order/project.json +++ b/apps/order/project.json @@ -19,7 +19,6 @@ "apps/order/src/assets", "apps/order/src/workflows" ], - "isolatedConfig": true, "webpackConfig": "apps/order/webpack.config.js" }, "configurations": { @@ -27,7 +26,10 @@ "mode": "development" }, "production": { - "mode": "production" + "mode": "production", + "optimization": true, + "extractLicenses": true, + "inspect": false } } }, @@ -40,12 +42,15 @@ }, "configurations": { "development": { - "buildTarget": "order:build:development" + "buildTarget": "order:build:development", + "watch": true, + "inspect": true }, "production": { - "buildTarget": "order:build:production" + "buildTarget": "order:build:production", + "watch": false } } } } -} +} \ No newline at end of file diff --git a/apps/order/src/app/app.controller.ts b/apps/order/src/app/app.controller.ts index 3c020d7..0d168f4 100644 --- a/apps/order/src/app/app.controller.ts +++ b/apps/order/src/app/app.controller.ts @@ -10,7 +10,8 @@ import { HttpCode, Delete, Req, - RawBodyRequest, + type RawBodyRequest, + BadRequestException, } from '@nestjs/common'; import { ApiBearerAuth, @@ -21,7 +22,7 @@ import { ApiHeader, ApiResponse, } from '@nestjs/swagger'; -import { AuthenticatedUser, AuthUser, JwtAuthGuard } from '@projectx/core'; +import { AuthenticatedUser, type AuthUser, JwtAuthGuard } from '@projectx/core'; import { CreateOrderDto, OrderStatusResponseDto } from '@projectx/models'; import { AppService } from './app.service'; @@ -39,7 +40,7 @@ export class AppController { @Post() async createOrder( @AuthenticatedUser() userDto: AuthUser, - @Body() orderDto: CreateOrderDto + @Body() orderDto: CreateOrderDto, ) { return this.appService.createOrder(userDto, orderDto); } @@ -78,7 +79,8 @@ export class AppController { @ApiOperation({ summary: 'Handle Stripe webhook events', - description: 'Endpoint for receiving webhook events from Stripe for payment processing', + description: + 'Endpoint for receiving webhook events from Stripe for payment processing', }) @ApiHeader({ name: 'stripe-signature', @@ -97,9 +99,12 @@ export class AppController { @Post('/webhook') async handleStripeWebhook( @Req() request: RawBodyRequest, - @Headers('stripe-signature') signature: string + @Headers('stripe-signature') signature: string, ) { // Validate and process the webhook + if (!request.rawBody) { + throw new BadRequestException('Request body is empty'); + } return this.appService.handleWebhook(request.rawBody, signature); } } diff --git a/apps/order/src/app/app.service.ts b/apps/order/src/app/app.service.ts index b7d85aa..71e263d 100644 --- a/apps/order/src/app/app.service.ts +++ b/apps/order/src/app/app.service.ts @@ -34,12 +34,29 @@ import { createOrder } from '../workflows/order.workflow'; @Injectable() export class AppService { private readonly logger = new Logger(AppService.name); - + private readonly taskQueue: string; constructor( private readonly configService: ConfigService, private readonly clientService: ClientService, private readonly stripeService: StripeService - ) {} + ) { + const taskQueue = this.configService.get('temporal.taskQueue'); + if (!taskQueue) { + throw new Error('Task queue not found'); + } + this.taskQueue = taskQueue; + } + + getWorkflowClient() { + const workflowClient = this.clientService.client?.workflow; + if (!workflowClient) { + throw new HttpException( + 'The workflow client was not initialized correctly', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + return workflowClient; + } getWorkflowIdByReferenceId(referenceId: string) { return `order-${referenceId}`; @@ -47,7 +64,6 @@ export class AppService { async createOrder(user: AuthUser, orderDto: CreateOrderDto) { this.logger.log(`createOrder(${user.email}) - creating order`); - const taskQueue = this.configService.get('temporal.taskQueue'); try { // Start workflow with order data const workflowData: OrderWorkflowData = { @@ -60,7 +76,7 @@ export class AppService { { workflowId: this.getWorkflowIdByReferenceId(orderDto.referenceId), args: [workflowData], - taskQueue, + taskQueue: this.taskQueue, workflowIdConflictPolicy: WorkflowIdConflictPolicy.FAIL, searchAttributes: { UserId: [user.id], @@ -70,7 +86,7 @@ export class AppService { ); const state = - await this.clientService.client?.workflow.executeUpdateWithStart( + await this.getWorkflowClient().executeUpdateWithStart( createOrderUpdate, { startWorkflowOperation, @@ -109,7 +125,7 @@ export class AppService { const workflowId = this.getWorkflowIdByReferenceId(referenceId); const description = await getWorkflowDescription( - this.clientService.client?.workflow, + this.getWorkflowClient(), workflowId ); @@ -121,7 +137,7 @@ export class AppService { throw new HttpException('Order has expired', HttpStatus.GONE); } - const handle = this.clientService.client?.workflow.getHandle(workflowId); + const handle = this.getWorkflowClient().getHandle(workflowId); const state = await handle.query(getOrderStateQuery); return state; } @@ -129,7 +145,7 @@ export class AppService { async cancelOrder(referenceId: string) { this.logger.log(`cancelOrder(${referenceId}) - cancelling order`); const workflowId = this.getWorkflowIdByReferenceId(referenceId); - const handle = this.clientService.client?.workflow.getHandle(workflowId); + const handle = this.getWorkflowClient().getHandle(workflowId); await handle.signal(cancelWorkflowSignal); } @@ -163,7 +179,7 @@ export class AppService { // Get workflow handle const workflowId = this.getWorkflowIdByReferenceId(referenceId); - const handle = this.clientService.client?.workflow.getHandle(workflowId); + const handle = this.getWorkflowClient().getHandle(workflowId); // Convert Stripe event to PaymentWebhookEvent const webhookEvent: PaymentWebhookEvent = { @@ -188,7 +204,7 @@ export class AppService { // Return true to indicate the webhook was received return { received: true }; } catch (err) { - this.logger.error(`handleWebhook(${signature}) - Webhook Error: ${err.message}`); + this.logger.error(`handleWebhook(${signature}) - Webhook Error: ${err}`); throw new BadRequestException('Webhook Error', { cause: err, }); diff --git a/apps/order/src/config/app.config.ts b/apps/order/src/config/app.config.ts index b827560..15afc7b 100644 --- a/apps/order/src/config/app.config.ts +++ b/apps/order/src/config/app.config.ts @@ -1,10 +1,10 @@ import { registerAs } from '@nestjs/config'; export default registerAs('app', () => ({ - port: Number(process.env.ORDER_PORT) || 8082, - environment: process.env.NODE_ENV, + port: Number(process.env['ORDER_PORT']) || 8082, + environment: process.env['NODE_ENV'], apiPrefix: 'order', - allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') ?? [], - logLevel: process.env.LOG_LEVEL ?? 'info', - jwtSecret: process.env.JWT_SECRET, + allowedOrigins: process.env['ALLOWED_ORIGINS']?.split(',') ?? [], + logLevel: process.env['LOG_LEVEL'] ?? 'info', + jwtSecret: process.env['JWT_SECRET'], })); diff --git a/apps/order/src/config/env.config.ts b/apps/order/src/config/env.config.ts index 7057af6..4241cc2 100644 --- a/apps/order/src/config/env.config.ts +++ b/apps/order/src/config/env.config.ts @@ -12,27 +12,27 @@ import { export class EnvironmentVariables { @IsEnum(Environment) @IsDefined() - NODE_ENV: Environment; + NODE_ENV!: Environment; @IsInt() @Min(0) @Max(65535) @IsDefined() - ORDER_PORT: number; + ORDER_PORT!: number; @IsString() @IsNotEmpty() - JWT_SECRET: string; + JWT_SECRET!: string; @IsString() @IsNotEmpty() - STRIPE_SECRET_KEY: string; + STRIPE_SECRET_KEY!: string; @IsString() @IsNotEmpty() - STRIPE_WEBHOOK_SECRET: string; + STRIPE_WEBHOOK_SECRET!: string; @IsString() @IsNotEmpty() - SENDGRID_API_KEY: string; + SENDGRID_API_KEY!: string; } diff --git a/apps/order/src/config/temporal.config.ts b/apps/order/src/config/temporal.config.ts index d4d449c..075000f 100644 --- a/apps/order/src/config/temporal.config.ts +++ b/apps/order/src/config/temporal.config.ts @@ -1,7 +1,7 @@ import { registerAs } from '@nestjs/config'; export default registerAs('temporal', () => ({ - host: process.env.TEMPORAL_HOST, - namespace: process.env.TEMPORAL_NAMESPACE || 'default', + host: process.env['TEMPORAL_HOST'], + namespace: process.env['TEMPORAL_NAMESPACE'] || 'default', taskQueue: 'order', })); diff --git a/apps/order/src/workflows/order.workflow.ts b/apps/order/src/workflows/order.workflow.ts index 83dc1f0..9a00b38 100644 --- a/apps/order/src/workflows/order.workflow.ts +++ b/apps/order/src/workflows/order.workflow.ts @@ -49,11 +49,9 @@ export enum OrderStatus { Failed = 'Failed', } -const initialState: OrderStatusResponseDto = { +const initialState: Partial = { status: OrderStatus.Pending, - orderId: undefined, referenceId: '', - clientSecret: undefined, }; export async function createOrder( @@ -62,10 +60,10 @@ export async function createOrder( ): Promise { state.referenceId = data.order.referenceId; // Define references to child workflows - let processPaymentWorkflow: ChildWorkflowHandle; + let processPaymentWorkflow: ChildWorkflowHandle | undefined; // Attach queries, signals and updates - setHandler(getOrderStateQuery, () => state); + setHandler(getOrderStateQuery, () => state as OrderStatusResponseDto); setHandler(cancelWorkflowSignal, () => { log.info('Requesting order cancellation'); if (!state?.orderId) { @@ -88,8 +86,10 @@ export async function createOrder( const { order, clientSecret } = await createOrderActivity(data); state.orderId = order.id; state.referenceId = order.referenceId; - state.clientSecret = clientSecret; - return state; + if (clientSecret) { + state.clientSecret = clientSecret; + } + return state as OrderStatusResponseDto; }); // Wait the order to be ready to be processed @@ -104,7 +104,7 @@ export async function createOrder( const processPaymentResult = await processPaymentWorkflow.result(); if (processPaymentResult.status !== OrderProcessPaymentStatus.SUCCESS) { // Report payment failure before throwing the error - await reportPaymentFailed(state.orderId); + await reportPaymentFailed(state.orderId as number); state.status = OrderStatus.Failed; throw ApplicationFailure.nonRetryable( OrderWorkflowNonRetryableErrors.UNKNOWN_ERROR, @@ -113,7 +113,7 @@ export async function createOrder( } processPaymentWorkflow = undefined; state.status = OrderStatus.Confirmed; - await reportPaymentConfirmed(state.orderId); + await reportPaymentConfirmed(state.orderId as number); } // TODO: Second step - Ship the order diff --git a/apps/order/tsconfig.app.json b/apps/order/tsconfig.app.json index eecd9b6..8d879f8 100644 --- a/apps/order/tsconfig.app.json +++ b/apps/order/tsconfig.app.json @@ -1,12 +1,45 @@ { - "extends": "./tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", + "outDir": "dist", + "module": "nodenext", "types": ["node"], + "rootDir": "src", + "moduleResolution": "nodenext", + "tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo", + "experimentalDecorators": true, "emitDecoratorMetadata": true, "target": "es2021" }, "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] + "exclude": [ + "out-tsc", + "dist", + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs" + ], + "references": [ + { + "path": "../../libs/backend/workflows/tsconfig.lib.json" + }, + { + "path": "../../libs/backend/db/tsconfig.lib.json" + }, + { + "path": "../../libs/models/tsconfig.lib.json" + }, + { + "path": "../../libs/backend/core/tsconfig.lib.json" + }, + { + "path": "../../libs/backend/email/tsconfig.lib.json" + }, + { + "path": "../../libs/backend/payment/tsconfig.lib.json" + } + ] } diff --git a/apps/order/tsconfig.json b/apps/order/tsconfig.json index c1e2dd4..ad9449d 100644 --- a/apps/order/tsconfig.json +++ b/apps/order/tsconfig.json @@ -3,14 +3,29 @@ "files": [], "include": [], "references": [ + { + "path": "../../libs/backend/workflows" + }, + { + "path": "../../libs/backend/db" + }, + { + "path": "../../libs/models" + }, + { + "path": "../../libs/backend/payment" + }, + { + "path": "../../libs/backend/core" + }, + { + "path": "../../libs/backend/email" + }, { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.spec.json" } - ], - "compilerOptions": { - "esModuleInterop": true - } + ] } diff --git a/apps/order/webpack.config.js b/apps/order/webpack.config.js index a27a9f0..5db3536 100644 --- a/apps/order/webpack.config.js +++ b/apps/order/webpack.config.js @@ -1,22 +1,54 @@ const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); const { join } = require('path'); +const nodeExternals = require('webpack-node-externals'); +const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin'); -module.exports = { - output: { - path: join(__dirname, '../../dist/apps/order'), - }, - devtool: 'source-map', - plugins: [ - new NxAppWebpackPlugin({ - target: 'node', - compiler: 'tsc', - main: './src/main.ts', - tsConfig: './tsconfig.app.json', - assets: ['./src/assets', './src/workflows'], - optimization: false, - outputHashing: 'none', - generatePackageJson: true, - sourceMap: true - }), - ], -}; +module.exports = (options) => { + // Check if we're in development mode + const isDevelopment = options.mode === 'development' || process.env.NODE_ENV === 'development'; + + // Base configuration + const config = { + output: { + path: join(__dirname, '../../dist/apps/order'), + }, + devtool: 'source-map', + plugins: [ + new NxAppWebpackPlugin({ + target: 'node', + compiler: 'tsc', + main: './src/main.ts', + tsConfig: './tsconfig.app.json', + assets: ['./src/assets', './src/workflows'], + optimization: false, + outputHashing: 'none', + generatePackageJson: true, + sourceMap: true + }), + ], + }; + + // Add HMR configuration in development mode + if (isDevelopment) { + console.log('Enabling Hot Module Replacement for NestJS in development mode'); + + // Modify configuration for development + config.watch = true; + config.entry = ['webpack/hot/poll?500', join(__dirname, './src/main.ts')]; + config.externals = [ + nodeExternals({ + allowlist: ['webpack/hot/poll?500'], + }), + ]; + + // Add HMR plugin + config.plugins.push( + new RunScriptWebpackPlugin({ + name: 'main.js', + autoRestart: true, + }) + ); + } + + return config; +}; \ No newline at end of file diff --git a/apps/product-e2e/eslint.config.js b/apps/product-e2e/eslint.config.js deleted file mode 100644 index df7cfc2..0000000 --- a/apps/product-e2e/eslint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -const baseConfig = require('../../eslint.config.js'); - -module.exports = [...baseConfig]; diff --git a/apps/product-e2e/jest.config.ts b/apps/product-e2e/jest.config.ts deleted file mode 100644 index 03e7107..0000000 --- a/apps/product-e2e/jest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -export default { - displayName: 'product-e2e', - preset: '../../jest.preset.js', - globalSetup: '/src/support/global-setup.ts', - globalTeardown: '/src/support/global-teardown.ts', - setupFiles: ['/src/support/test-setup.ts'], - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': [ - 'ts-jest', - { - tsconfig: '/tsconfig.spec.json', - }, - ], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/product-e2e', -}; diff --git a/apps/product-e2e/project.json b/apps/product-e2e/project.json deleted file mode 100644 index 1b31b44..0000000 --- a/apps/product-e2e/project.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "product-e2e", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "application", - "implicitDependencies": ["product"], - "targets": { - "e2e": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{e2eProjectRoot}"], - "options": { - "jestConfig": "apps/product-e2e/jest.config.ts", - "passWithNoTests": true - }, - "dependsOn": ["product:build"] - } - } -} diff --git a/apps/product-e2e/src/product/product.spec.ts b/apps/product-e2e/src/product/product.spec.ts deleted file mode 100644 index e8ac2a6..0000000 --- a/apps/product-e2e/src/product/product.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'axios'; - -describe('GET /api', () => { - it('should return a message', async () => { - const res = await axios.get(`/api`); - - expect(res.status).toBe(200); - expect(res.data).toEqual({ message: 'Hello API' }); - }); -}); diff --git a/apps/product-e2e/src/support/global-setup.ts b/apps/product-e2e/src/support/global-setup.ts deleted file mode 100644 index c1f5144..0000000 --- a/apps/product-e2e/src/support/global-setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var __TEARDOWN_MESSAGE__: string; - -module.exports = async function () { - // Start services that that the app needs to run (e.g. database, docker-compose, etc.). - console.log('\nSetting up...\n'); - - // Hint: Use `globalThis` to pass variables to global teardown. - globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n'; -}; diff --git a/apps/product-e2e/src/support/global-teardown.ts b/apps/product-e2e/src/support/global-teardown.ts deleted file mode 100644 index 32ea345..0000000 --- a/apps/product-e2e/src/support/global-teardown.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable */ - -module.exports = async function () { - // Put clean up logic here (e.g. stopping services, docker-compose, etc.). - // Hint: `globalThis` is shared between setup and teardown. - console.log(globalThis.__TEARDOWN_MESSAGE__); -}; diff --git a/apps/product-e2e/src/support/test-setup.ts b/apps/product-e2e/src/support/test-setup.ts deleted file mode 100644 index 07f2870..0000000 --- a/apps/product-e2e/src/support/test-setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ - -import axios from 'axios'; - -module.exports = async function () { - // Configure axios for tests to use. - const host = process.env.HOST ?? 'localhost'; - const port = process.env.PORT ?? '3000'; - axios.defaults.baseURL = `http://${host}:${port}`; -}; diff --git a/apps/product-e2e/tsconfig.json b/apps/product-e2e/tsconfig.json deleted file mode 100644 index ed633e1..0000000 --- a/apps/product-e2e/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.spec.json" - } - ], - "compilerOptions": { - "esModuleInterop": true - } -} diff --git a/apps/product-e2e/tsconfig.spec.json b/apps/product-e2e/tsconfig.spec.json deleted file mode 100644 index d7f9cf2..0000000 --- a/apps/product-e2e/tsconfig.spec.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": ["jest.config.ts", "src/**/*.ts"] -} diff --git a/apps/product/.spec.swcrc b/apps/product/.spec.swcrc new file mode 100644 index 0000000..3b52a53 --- /dev/null +++ b/apps/product/.spec.swcrc @@ -0,0 +1,22 @@ +{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] +} diff --git a/apps/product/eslint.config.js b/apps/product/eslint.config.js deleted file mode 100644 index df7cfc2..0000000 --- a/apps/product/eslint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -const baseConfig = require('../../eslint.config.js'); - -module.exports = [...baseConfig]; diff --git a/apps/product/eslint.config.mjs b/apps/product/eslint.config.mjs new file mode 100644 index 0000000..b7f6277 --- /dev/null +++ b/apps/product/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [...baseConfig]; diff --git a/apps/product/project.json b/apps/product/project.json index a011aa8..c47e74c 100644 --- a/apps/product/project.json +++ b/apps/product/project.json @@ -18,7 +18,6 @@ "assets": [ "apps/product/src/assets" ], - "isolatedConfig": true, "webpackConfig": "apps/product/webpack.config.js" }, "configurations": { @@ -26,26 +25,31 @@ "mode": "development" }, "production": { - "mode": "production" + "mode": "production", + "optimization": true, + "extractLicenses": true, + "inspect": false } } }, "serve": { "executor": "@nx/js:node", "defaultConfiguration": "development", - "dependsOn": ["build"], "options": { "buildTarget": "product:build", "runBuildTargetDependencies": false }, "configurations": { "development": { - "buildTarget": "product:build:development" + "buildTarget": "product:build:development", + "watch": true, + "inspect": true }, "production": { - "buildTarget": "product:build:production" + "buildTarget": "product:build:production", + "watch": false } } } } -} +} \ No newline at end of file diff --git a/apps/product/src/config/app.config.ts b/apps/product/src/config/app.config.ts index 4cf9c33..b9acb05 100644 --- a/apps/product/src/config/app.config.ts +++ b/apps/product/src/config/app.config.ts @@ -1,10 +1,10 @@ import { registerAs } from '@nestjs/config'; export default registerAs('app', () => ({ - port: Number(process.env.PRODUCT_PORT) || 8083, - environment: process.env.NODE_ENV, + port: Number(process.env['PRODUCT_PORT']) || 8083, + environment: process.env['NODE_ENV'], apiPrefix: 'product', - allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') ?? [], - logLevel: process.env.LOG_LEVEL ?? 'info', - jwtSecret: process.env.JWT_SECRET, + allowedOrigins: process.env['ALLOWED_ORIGINS']?.split(',') ?? [], + logLevel: process.env['LOG_LEVEL'] ?? 'info', + jwtSecret: process.env['JWT_SECRET'], })); \ No newline at end of file diff --git a/apps/product/src/config/env.config.ts b/apps/product/src/config/env.config.ts index d034b8d..57f49f6 100644 --- a/apps/product/src/config/env.config.ts +++ b/apps/product/src/config/env.config.ts @@ -12,15 +12,15 @@ import { export class EnvironmentVariables { @IsEnum(Environment) @IsDefined() - NODE_ENV: Environment; + NODE_ENV!: Environment; @IsInt() @Min(0) @Max(65535) @IsDefined() - PRODUCT_PORT: number; + PRODUCT_PORT!: number; @IsString() @IsNotEmpty() - JWT_SECRET: string; + JWT_SECRET!: string; } diff --git a/apps/product/tsconfig.app.json b/apps/product/tsconfig.app.json index eecd9b6..1493ea6 100644 --- a/apps/product/tsconfig.app.json +++ b/apps/product/tsconfig.app.json @@ -1,12 +1,36 @@ { - "extends": "./tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", + "outDir": "dist", + "module": "nodenext", "types": ["node"], + "rootDir": "src", + "moduleResolution": "nodenext", + "tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo", + "experimentalDecorators": true, "emitDecoratorMetadata": true, "target": "es2021" }, "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] + "exclude": [ + "out-tsc", + "dist", + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs" + ], + "references": [ + { + "path": "../../libs/backend/db/tsconfig.lib.json" + }, + { + "path": "../../libs/backend/core/tsconfig.lib.json" + }, + { + "path": "../../libs/models/tsconfig.lib.json" + } + ] } diff --git a/apps/product/tsconfig.json b/apps/product/tsconfig.json index c1e2dd4..a6cef5c 100644 --- a/apps/product/tsconfig.json +++ b/apps/product/tsconfig.json @@ -3,14 +3,20 @@ "files": [], "include": [], "references": [ + { + "path": "../../libs/backend/db" + }, + { + "path": "../../libs/backend/core" + }, + { + "path": "../../libs/models" + }, { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.spec.json" } - ], - "compilerOptions": { - "esModuleInterop": true - } + ] } diff --git a/apps/product/tsconfig.spec.json b/apps/product/tsconfig.spec.json index 9b2a121..18d4e74 100644 --- a/apps/product/tsconfig.spec.json +++ b/apps/product/tsconfig.spec.json @@ -1,14 +1,22 @@ { - "extends": "./tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] + "outDir": "./out-tsc/jest", + "types": ["jest", "node"], + "module": "nodenext", + "moduleResolution": "nodenext", + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": [ "jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.app.json" + } ] } diff --git a/apps/product/webpack.config.js b/apps/product/webpack.config.js index 8a9aa23..84d0a12 100644 --- a/apps/product/webpack.config.js +++ b/apps/product/webpack.config.js @@ -1,20 +1,54 @@ const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); const { join } = require('path'); +const nodeExternals = require('webpack-node-externals'); +const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin'); -module.exports = { - output: { - path: join(__dirname, '../../dist/apps/product'), - }, - plugins: [ - new NxAppWebpackPlugin({ - target: 'node', - compiler: 'tsc', - main: './src/main.ts', - tsConfig: './tsconfig.app.json', - assets: ['./src/assets'], - optimization: false, - outputHashing: 'none', - generatePackageJson: true, - }), - ], +module.exports = (options) => { + // Check if we're in development mode + const isDevelopment = options.mode === 'development' || process.env.NODE_ENV === 'development'; + + // Base configuration + const config = { + output: { + path: join(__dirname, '../../dist/apps/product'), + }, + devtool: 'source-map', + plugins: [ + new NxAppWebpackPlugin({ + target: 'node', + compiler: 'tsc', + main: './src/main.ts', + tsConfig: './tsconfig.app.json', + assets: ['./src/assets'], + optimization: false, + outputHashing: 'none', + generatePackageJson: true, + sourceMap: true + }), + ], + }; + + // Add HMR configuration in development mode + if (isDevelopment) { + console.log('Enabling Hot Module Replacement for NestJS in development mode'); + + // Modify configuration for development + config.watch = true; + config.entry = ['webpack/hot/poll?500', join(__dirname, './src/main.ts')]; + config.externals = [ + nodeExternals({ + allowlist: ['webpack/hot/poll?500'], + }), + ]; + + // Add HMR plugin + config.plugins.push( + new RunScriptWebpackPlugin({ + name: 'main.js', + autoRestart: true, + }) + ); + } + + return config; }; diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 9ca4842..a70276c 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -2,3 +2,4 @@ build public/build .env +.react-router diff --git a/apps/web/app/app-nav.tsx b/apps/web/app/app-nav.tsx new file mode 100644 index 0000000..6814043 --- /dev/null +++ b/apps/web/app/app-nav.tsx @@ -0,0 +1,14 @@ +import { NavLink } from 'react-router'; + +export function AppNav() { + return ( + + ); +} diff --git a/apps/web/app/cookies/auth.server.ts b/apps/web/app/cookies/auth.server.ts index 565dcca..e237c27 100644 --- a/apps/web/app/cookies/auth.server.ts +++ b/apps/web/app/cookies/auth.server.ts @@ -1,13 +1,14 @@ import { UserDto } from '@projectx/models'; -import { createCookieSessionStorage, redirect } from '@remix-run/node'; +import { createCookieSessionStorage, redirect } from 'react-router'; import { plainToInstance } from 'class-transformer'; -import _ from 'lodash'; +import isEmpty from 'lodash/isEmpty'; +import isObject from 'lodash/isObject'; -import { sessionSecret } from '~/config/app.config.server'; +import { sessionSecret } from '../config/app.config.server'; const authStorage = createCookieSessionStorage({ cookie: { - name: '__login', + name: '__auth', secure: process.env.NODE_ENV !== 'development', secrets: [sessionSecret], sameSite: 'lax', @@ -27,9 +28,9 @@ export async function getAuthSession(request: Request) { return { getAuthUser: () => { const user = session.get(USER_KEY); - return _.isEmpty(user) + return isEmpty(user) ? undefined - : plainToInstance(UserDto, _.isObject(user) ? user : JSON.parse(user), { + : plainToInstance(UserDto, isObject(user) ? user : JSON.parse(user), { excludeExtraneousValues: true, }) as unknown as UserDto; }, diff --git a/apps/web/app/cookies/session.server.ts b/apps/web/app/cookies/session.server.ts index 1e27b67..e11fbd6 100644 --- a/apps/web/app/cookies/session.server.ts +++ b/apps/web/app/cookies/session.server.ts @@ -1,4 +1,4 @@ -import { createCookie, createCookieSessionStorage } from '@remix-run/node'; +import { createCookie, createCookieSessionStorage } from 'react-router'; import { CSRF } from "remix-utils/csrf/server"; import { sessionSecret } from "~/config/app.config.server"; diff --git a/apps/web/app/entry.client.tsx b/apps/web/app/entry.client.tsx new file mode 100644 index 0000000..068d0be --- /dev/null +++ b/apps/web/app/entry.client.tsx @@ -0,0 +1,19 @@ +/** + * By default, React Router will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx react-router reveal` ✨ + * For more information, see https://reactrouter.com/explanation/special-files#entryclienttsx + */ + +import { HydratedRouter } from 'react-router/dom'; +import { startTransition, StrictMode } from 'react'; +import { hydrateRoot } from 'react-dom/client'; + +// Ensure the router context is properly initialized before hydration +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/apps/web/app/entry.server.tsx b/apps/web/app/entry.server.tsx index 946feb0..b973a7a 100644 --- a/apps/web/app/entry.server.tsx +++ b/apps/web/app/entry.server.tsx @@ -1,124 +1,52 @@ -import { PassThrough } from "node:stream"; -import type { EntryContext } from "@remix-run/node"; -import { createReadableStreamFromReadable, installGlobals } from "@remix-run/node"; -import { RemixServer } from '@remix-run/react'; +/** + * By default, React Router will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://reactrouter.com/explanation/special-files#entryservertsx + */ + +import { PassThrough } from 'node:stream'; + +import type { AppLoadContext, EntryContext } from 'react-router'; +import { createReadableStreamFromReadable } from '@react-router/node'; +import { ServerRouter } from 'react-router'; import { isbot } from 'isbot'; +import type { RenderToPipeableStreamOptions } from 'react-dom/server'; import { renderToPipeableStream } from 'react-dom/server'; import { getEnv } from '~/config/env.server'; import { securityHeaders } from '~/config/security.server'; global.ENV = getEnv(); -installGlobals(); -const ABORT_DELAY = 5_000; +export const streamTimeout = 5_000; export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, - remixContext: EntryContext, -) { - return isBotRequest(request.headers.get("user-agent")) - ? handleBotRequest( - request, - responseStatusCode, - responseHeaders, - remixContext - ) - : handleBrowserRequest( - request, - responseStatusCode, - responseHeaders, - remixContext - ); -} - -// We have some Remix apps in the wild already running with isbot@3 so we need -// to maintain backwards compatibility even though we want new apps to use -// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev. -function isBotRequest(userAgent: string | null) { - if (!userAgent) { - return false; - } - - return isbot(userAgent); -} - -function handleBotRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext + routerContext: EntryContext, + loadContext: AppLoadContext ) { return new Promise((resolve, reject) => { let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onAllReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); + const userAgent = request.headers.get('user-agent'); - responseHeaders.set("Content-Type", "text/html"); - for (const key in securityHeaders) { - responseHeaders.set(key, securityHeaders[key]); - } + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + const readyOption: keyof RenderToPipeableStreamOptions = + (userAgent && isbot(userAgent)) || routerContext.isSpaMode + ? 'onAllReady' + : 'onShellReady'; - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }) - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - } - ); - - setTimeout(abort, ABORT_DELAY); - }); -} - -function handleBrowserRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - return new Promise((resolve, reject) => { - let shellRendered = false; const { pipe, abort } = renderToPipeableStream( - , + , { - onShellReady() { + [readyOption]() { shellRendered = true; const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); - responseHeaders.set("Content-Type", "text/html"); + responseHeaders.set('Content-Type', 'text/html'); for (const key in securityHeaders) { responseHeaders.set(key, securityHeaders[key]); } @@ -147,6 +75,8 @@ function handleBrowserRequest( } ); - setTimeout(abort, ABORT_DELAY); + // Abort the rendering stream after the `streamTimeout` so it has time to + // flush down the rejected boundaries + setTimeout(abort, streamTimeout + 1000); }); -} \ No newline at end of file +} diff --git a/apps/web/app/hooks/useDehydratedState.ts b/apps/web/app/hooks/useDehydratedState.ts new file mode 100644 index 0000000..8c42505 --- /dev/null +++ b/apps/web/app/hooks/useDehydratedState.ts @@ -0,0 +1,58 @@ +import type { DehydratedState } from '@tanstack/react-query' + +import merge from 'deepmerge' + +import { useMatches } from 'react-router' + +// Define the type for match data +interface MatchData { + dehydratedState?: DehydratedState; + [key: string]: unknown; +} + +/** + * Retrieves and merges dehydrated query states from all matched routes. + * + * @description + * This hook is used to hydrate TanStack Query data that was prefetched in route loaders. + * It works by collecting all `dehydratedState` objects from route `loader` data + * and merging them into a single state for the QueryClient to hydrate. + * + * @example + * // In your route loader: + * export async function loader() { + * const queryClient = new QueryClient(); + * await queryClient.prefetchQuery(["products"], getProducts); + * return json({ dehydratedState: dehydrate(queryClient) }); + * } + * + * // In your route component: + * export default function ProductsRoute() { + * const { data } = useQuery({ queryKey: ["products"], queryFn: getProducts }); + * + * return ( + *
+ * {data?.map((product) => ( + * + * ))} + *
+ * ); + * } + * + * @returns {DehydratedState | undefined} The merged dehydrated state or undefined if no state exists + */ +export function useDehydratedState(): DehydratedState | undefined { + const matches = useMatches() + + const dehydratedState = matches + .map((match) => { + return (match.data as MatchData)?.dehydratedState + }) + .filter((state): state is DehydratedState => Boolean(state)) + + return dehydratedState.length + ? dehydratedState.reduce((accumulator, currentValue) => + merge(accumulator, currentValue), + { mutations: [], queries: [] }) + : undefined +} \ No newline at end of file diff --git a/apps/web/app/pages/LoginPage.tsx b/apps/web/app/pages/LoginPage.tsx index c48dbf4..4407817 100644 --- a/apps/web/app/pages/LoginPage.tsx +++ b/apps/web/app/pages/LoginPage.tsx @@ -1,6 +1,6 @@ import { EnvelopeIcon } from '@heroicons/react/24/outline'; import { classnames } from '@projectx/ui'; -import { useFetcher } from '@remix-run/react'; +import { useFetcher } from 'react-router'; import { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; import OtpInput from 'react-otp-input'; diff --git a/apps/web/app/providers/cart.tsx b/apps/web/app/providers/cart.tsx index a911044..672a1f9 100644 --- a/apps/web/app/providers/cart.tsx +++ b/apps/web/app/providers/cart.tsx @@ -1,9 +1,9 @@ -import React, { ComponentType, PropsWithChildren } from 'react'; +import { ComponentType } from 'react'; import { CartProvider } from 'react-use-cart'; const CART_ID = 'shopping-cart'; -export function withCartProvider( +export function withCartProvider( WrappedComponent: ComponentType, ): ComponentType { return function (props: T) { diff --git a/apps/web/app/providers/query.tsx b/apps/web/app/providers/query.tsx index d7da01a..57ac998 100644 --- a/apps/web/app/providers/query.tsx +++ b/apps/web/app/providers/query.tsx @@ -4,12 +4,12 @@ import { QueryClientProvider, } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { useState, type ComponentType, type PropsWithChildren } from 'react'; +import { useState, type ComponentType } from 'react'; import { createPortal } from 'react-dom'; import { ClientOnly } from 'remix-utils/client-only'; -import { useDehydratedState } from 'use-dehydrated-state'; +import { useDehydratedState } from '~/hooks/useDehydratedState'; -export function withQueryClientProvider( +export function withQueryClientProvider( WrappedComponent: ComponentType ): ComponentType { return function (props: T) { diff --git a/apps/web/app/providers/workflows/internal/useOrderWorkflow.ts b/apps/web/app/providers/workflows/internal/useOrderWorkflow.ts index f0ccf51..4183f71 100644 --- a/apps/web/app/providers/workflows/internal/useOrderWorkflow.ts +++ b/apps/web/app/providers/workflows/internal/useOrderWorkflow.ts @@ -1,4 +1,4 @@ -import { useLocation } from '@remix-run/react'; +import { useLocation } from 'react-router'; import { UseQueryOptions, useQueries } from '@tanstack/react-query'; import { toast } from 'react-toastify'; import { AxiosError } from 'axios'; diff --git a/apps/web/app/providers/workflows/store/store.tsx b/apps/web/app/providers/workflows/store/store.tsx index 1771411..b479006 100644 --- a/apps/web/app/providers/workflows/store/store.tsx +++ b/apps/web/app/providers/workflows/store/store.tsx @@ -7,7 +7,7 @@ import React, { } from 'react'; import { initialState, reducer } from './reducer'; -import { StoreReducer, ContextProps } from './types'; +import { ContextProps } from './types'; export const GlobalContext = createContext( [initialState, () => null] as ContextProps, @@ -19,7 +19,7 @@ export type StoreProviderProps = PropsWithChildren; export const StoreProvider: React.FC = ({ children, }) => { - const [state, dispatch] = useReducer(reducer, { + const [state, dispatch] = useReducer(reducer, { ...initialState, }); diff --git a/apps/web/app/root.tsx b/apps/web/app/root.tsx index 2b003e5..0a1e6fa 100644 --- a/apps/web/app/root.tsx +++ b/apps/web/app/root.tsx @@ -1,41 +1,38 @@ +import React from 'react'; import type { UserDto } from '@projectx/models'; -import type { - MetaFunction, - LinksFunction, - LoaderFunction, - LoaderFunctionArgs, -} from '@remix-run/node'; + import { - isRouteErrorResponse, - json, + data, Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, - useLoaderData, + type MetaFunction, + type LinksFunction, + useRouteLoaderData, + isRouteErrorResponse, useRouteError, -} from '@remix-run/react'; -import { PropsWithChildren } from 'react'; +} from 'react-router'; import { AuthenticityTokenProvider } from 'remix-utils/csrf/react'; -import { ToastContainer } from 'react-toastify'; -import { getEnv } from '~/config/env.server'; -import { csrf } from '~/cookies/session.server'; -import twStyles from '~/tailwind.css'; -import { - withQueryClientProvider, - withCartProvider, - withAuthProvider, - withStoreProvider, - useWorkflows, -} from '~/providers'; -import { getAuthSession } from '~/cookies/auth.server'; import { THEME } from './constants'; +import { AppNav } from './app-nav'; +import { useWorkflows, withAuthProvider, withCartProvider, withQueryClientProvider, withStoreProvider } from './providers'; +import { ToastContainer } from 'react-toastify'; +import { getEnv } from './config/env.server'; +import { csrf } from './cookies/session.server'; +import { getAuthSession } from './cookies/auth.server'; +import { Route } from './+types/root'; +import '../styles.css'; // Import global styles here + +export const meta: MetaFunction = () => [ + { + title: 'ProjectX App', + }, +]; export const links: LinksFunction = () => [ - { rel: 'stylesheet', href: twStyles }, { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, { rel: 'preconnect', @@ -48,22 +45,7 @@ export const links: LinksFunction = () => [ }, ]; -export const meta: MetaFunction = () => [ - { - title: 'ProjectX App', - }, -]; - -type LoaderData = { - theme: string; - user?: UserDto; - csrfToken: string; - accessToken?: string; - isAuthenticated: boolean; - ENV: ReturnType; -}; - -export const loader: LoaderFunction = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ request }: Route.LoaderArgs) => { const [csrfToken, cookieHeader] = await csrf.commitToken(); const theme = request.headers.get('Cookie')?.includes('theme=dark') ? THEME.DARK @@ -71,7 +53,7 @@ export const loader: LoaderFunction = async ({ request }: LoaderFunctionArgs) => const { getAuthUser, getAuthAccessToken } = await getAuthSession(request); const accessToken = getAuthAccessToken(); const user = getAuthUser(); - return json( + return data( { user, theme, @@ -82,61 +64,77 @@ export const loader: LoaderFunction = async ({ request }: LoaderFunctionArgs) => }, { headers: { - 'Set-Cookie': cookieHeader as string, + ...(cookieHeader ? { 'Set-Cookie': cookieHeader } : {}), }, } ); }; -export type AppProps = PropsWithChildren< - Omit ->; -function App({ csrfToken, theme, user, accessToken, ENV }: AppProps) { - // Connect Temporal workflows to your app - useWorkflows({ accessToken: accessToken as string, email: user?.email as string }); - +export function Layout({ children }: { children: React.ReactNode }) { + const data = useRouteLoaderData('root'); + const { theme } = data ?? {}; return ( - - - - - - - - - - - + + + + + + + + + + + {children} + + {ENV && (