diff --git a/ReleaseNotes.json b/ReleaseNotes.json index 96cf00d..b4343c8 100644 --- a/ReleaseNotes.json +++ b/ReleaseNotes.json @@ -29,6 +29,9 @@ "Removed all `.js` extensions from backend imports (handlers, middleware, routes, services)", "Added `tsup.config.ts` with optimized ESM output configuration", "Updated build script from `tsc` to `tsup` in package.json", + "Created centralized path utilities in `src/backend/utils/paths.ts`", + "Replaced duplicated `__dirname` logic with `resolveFromRoot()` function", + "Path resolution now environment-aware (dev vs production)", "Maintained development mode with nodemon for hot-reload support" ] }, diff --git a/src/backend/index.ts b/src/backend/index.ts index 24b1e39..00ab5f7 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -3,8 +3,6 @@ import cors from 'cors'; import helmet from 'helmet'; import compression from 'compression'; import path from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; import { setTimeout } from 'timers'; import apiRoutes from './routes/api'; import apiKeyAuth from './middleware/auth'; @@ -13,9 +11,7 @@ import config from './services/config'; import logger from './services/logger'; import { generateRuntimeConfig } from './services/runtimeConfig'; import { healthCheckHandler } from './handlers/healthHandler'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +import { paths } from './utils/paths'; const app = express(); @@ -72,7 +68,7 @@ const basePath = config.basePath.endsWith('/') : config.basePath; // Serve static frontend files -const frontendPath = path.join(__dirname, '../frontend'); +const frontendPath = paths.frontendDir; // Generate runtime configuration (env.js) at startup generateRuntimeConfig(frontendPath, basePath); diff --git a/src/backend/routes/api.ts b/src/backend/routes/api.ts index a9bdc6e..53abb06 100644 --- a/src/backend/routes/api.ts +++ b/src/backend/routes/api.ts @@ -1,8 +1,7 @@ import { Router, Request, Response } from 'express'; import Joi from 'joi'; import { readFileSync } from 'fs'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; +import paths from '../utils/paths'; import geoIPService from '../services/geoip'; import logger from '../services/logger'; import config from '../services/config'; @@ -13,9 +12,6 @@ import { ErrorResponse, } from '../types/index'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - const router = Router(); // Validation schemas @@ -276,10 +272,8 @@ router.get('/version', (_req: Request, res: Response): void => { // Get release notes router.get('/release-notes', (_req: Request, res: Response): void => { try { - const releaseNotesPath = join(__dirname, '../../../ReleaseNotes.json'); - const releaseNotesContent = readFileSync(releaseNotesPath, 'utf-8'); + const releaseNotesContent = readFileSync(paths.releaseNotesFile, 'utf-8'); const releaseNotes = JSON.parse(releaseNotesContent); - logger.debug('Release notes retrieved successfully'); res.json(releaseNotes); } catch (error) { @@ -294,10 +288,8 @@ router.get('/release-notes', (_req: Request, res: Response): void => { // Get overview router.get('/overview', (_req: Request, res: Response): void => { try { - const overviewPath = join(__dirname, '../../../Overview.json'); - const overviewContent = readFileSync(overviewPath, 'utf-8'); + const overviewContent = readFileSync(paths.overviewFile, 'utf-8'); const overview = JSON.parse(overviewContent); - logger.debug('Overview retrieved successfully'); res.json(overview); } catch (error) { diff --git a/src/backend/services/config.ts b/src/backend/services/config.ts index df8c4b3..5d3f1cb 100644 --- a/src/backend/services/config.ts +++ b/src/backend/services/config.ts @@ -1,14 +1,8 @@ import { Config, LogLevel, LOG_LEVELS } from '../types/index'; -import path from 'path'; import { config as dotenvConfig } from 'dotenv'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import { paths } from '../utils/paths'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Load .env file from root directory -dotenvConfig({ path: path.join(__dirname, '../../../.env') }); +dotenvConfig({ path: paths.envFile }); // Parse log level from environment variable const parseLogLevel = (level?: string): LogLevel => { diff --git a/src/backend/utils/paths.ts b/src/backend/utils/paths.ts new file mode 100644 index 0000000..123fe9a --- /dev/null +++ b/src/backend/utils/paths.ts @@ -0,0 +1,48 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +const isProduction = process.env.NODE_ENV === 'production'; + +const findProjectRoot = (): string => { + // In production (Docker), process.cwd() is /app (project root) + if (isProduction) { + return process.cwd(); + } + + if (import.meta.url) { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const rootPath = path.resolve(__dirname, '../../..'); + return rootPath; + } + + throw new Error( + 'import.meta.url is required in development to resolve project root' + ); +}; + +const projectRootPath = findProjectRoot(); +console.log('[paths] Project root resolved:', projectRootPath); + +const resolveFilePath = (...relativePath: string[]): string => { + return path.join(projectRootPath, ...relativePath); +}; + +const envPath = resolveFilePath('.env'); +const releaseNotesPath = resolveFilePath('ReleaseNotes.json'); +const overviewPath = resolveFilePath('Overview.json'); +const frontendPrefix = isProduction ? 'dist' : 'src'; +const frontendPath = resolveFilePath(frontendPrefix, 'frontend'); + +const paths = { + projectRoot: projectRootPath, + envFile: envPath, + releaseNotesFile: releaseNotesPath, + overviewFile: overviewPath, + frontendDir: frontendPath, +}; + +console.log('[paths] App paths resolved:', paths); + +export { paths }; +export default paths;