mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
feat: Enhance package root detection and version retrieval using ESM-compatible methods (#371)
This commit is contained in:
@@ -15,9 +15,26 @@ import {
|
||||
} from './services/sseService.js';
|
||||
import { initializeDefaultUser } from './models/User.js';
|
||||
import { sseUserContextMiddleware } from './middlewares/userContext.js';
|
||||
import { findPackageRoot } from './utils/path.js';
|
||||
import { getCurrentModuleDir } from './utils/moduleDir.js';
|
||||
|
||||
// Get the current working directory (will be project root in most cases)
|
||||
const currentFileDir = process.cwd() + '/src';
|
||||
/**
|
||||
* Get the directory of the current module
|
||||
* This is wrapped in a function to allow easy mocking in test environments
|
||||
*/
|
||||
function getCurrentFileDir(): string {
|
||||
// In test environments, use process.cwd() to avoid import.meta issues
|
||||
if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined) {
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
try {
|
||||
return getCurrentModuleDir();
|
||||
} catch {
|
||||
// Fallback for environments where import.meta might not be available
|
||||
return process.cwd();
|
||||
}
|
||||
}
|
||||
|
||||
export class AppServer {
|
||||
private app: express.Application;
|
||||
@@ -167,10 +184,11 @@ export class AppServer {
|
||||
private findFrontendDistPath(): string | null {
|
||||
// Debug flag for detailed logging
|
||||
const debug = process.env.DEBUG === 'true';
|
||||
const currentDir = getCurrentFileDir();
|
||||
|
||||
if (debug) {
|
||||
console.log('DEBUG: Current directory:', process.cwd());
|
||||
console.log('DEBUG: Script directory:', currentFileDir);
|
||||
console.log('DEBUG: Script directory:', currentDir);
|
||||
}
|
||||
|
||||
// First, find the package root directory
|
||||
@@ -205,51 +223,9 @@ export class AppServer {
|
||||
|
||||
// Helper method to find the package root (where package.json is located)
|
||||
private findPackageRoot(): string | null {
|
||||
const debug = process.env.DEBUG === 'true';
|
||||
|
||||
// Possible locations for package.json
|
||||
const possibleRoots = [
|
||||
// Standard npm package location
|
||||
path.resolve(currentFileDir, '..', '..'),
|
||||
// Current working directory
|
||||
process.cwd(),
|
||||
// When running from dist directory
|
||||
path.resolve(currentFileDir, '..'),
|
||||
// When installed via npx
|
||||
path.resolve(currentFileDir, '..', '..', '..'),
|
||||
];
|
||||
|
||||
// Special handling for npx
|
||||
if (process.argv[1] && process.argv[1].includes('_npx')) {
|
||||
const npxDir = path.dirname(process.argv[1]);
|
||||
possibleRoots.unshift(path.resolve(npxDir, '..'));
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log('DEBUG: Checking for package.json in:', possibleRoots);
|
||||
}
|
||||
|
||||
for (const root of possibleRoots) {
|
||||
const packageJsonPath = path.join(root, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
if (pkg.name === 'mcphub' || pkg.name === '@samanhappy/mcphub') {
|
||||
if (debug) {
|
||||
console.log(`DEBUG: Found package.json at ${packageJsonPath}`);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
} catch (e) {
|
||||
if (debug) {
|
||||
console.error(`DEBUG: Failed to parse package.json at ${packageJsonPath}:`, e);
|
||||
}
|
||||
// Continue to the next potential root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
// Use the shared utility function which properly handles ESM module paths
|
||||
const currentDir = getCurrentFileDir();
|
||||
return findPackageRoot(currentDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
src/utils/moduleDir.ts
Normal file
11
src/utils/moduleDir.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Get the directory of the current module
|
||||
* This is in a separate file to allow mocking in test environments
|
||||
*/
|
||||
export function getCurrentModuleDir(): string {
|
||||
const currentModuleFile = fileURLToPath(import.meta.url);
|
||||
return path.dirname(currentModuleFile);
|
||||
}
|
||||
@@ -1,10 +1,171 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { dirname } from 'path';
|
||||
import { getCurrentModuleDir } from './moduleDir.js';
|
||||
|
||||
// Project root directory - use process.cwd() as a simpler alternative
|
||||
const rootDir = process.cwd();
|
||||
|
||||
// Cache the package root for performance
|
||||
let cachedPackageRoot: string | null | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Initialize package root by trying to find it using the module directory
|
||||
* This should be called when the module is first loaded
|
||||
*/
|
||||
function initializePackageRoot(): void {
|
||||
// Skip initialization in test environments
|
||||
if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to get the current module's directory
|
||||
const currentModuleDir = getCurrentModuleDir();
|
||||
|
||||
// This file is in src/utils/path.ts (or dist/utils/path.js when compiled)
|
||||
// So package.json should be 2 levels up
|
||||
const possibleRoots = [
|
||||
path.resolve(currentModuleDir, '..', '..'), // dist -> package root
|
||||
path.resolve(currentModuleDir, '..'), // dist/utils -> dist -> package root
|
||||
];
|
||||
|
||||
for (const root of possibleRoots) {
|
||||
const packageJsonPath = path.join(root, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
if (pkg.name === 'mcphub' || pkg.name === '@samanhappy/mcphub') {
|
||||
cachedPackageRoot = root;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Continue checking
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If initialization fails, cachedPackageRoot remains undefined
|
||||
// and findPackageRoot will search normally
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on module load (unless in test environment)
|
||||
initializePackageRoot();
|
||||
|
||||
/**
|
||||
* Find the package root directory (where package.json is located)
|
||||
* This works correctly when the package is installed globally or locally
|
||||
* @param startPath Starting path to search from (defaults to checking module paths)
|
||||
* @returns The package root directory path, or null if not found
|
||||
*/
|
||||
export const findPackageRoot = (startPath?: string): string | null => {
|
||||
// Return cached value if available and no specific start path is requested
|
||||
if (cachedPackageRoot !== undefined && !startPath) {
|
||||
return cachedPackageRoot;
|
||||
}
|
||||
|
||||
const debug = process.env.DEBUG === 'true';
|
||||
|
||||
// Possible locations for package.json relative to the search path
|
||||
const possibleRoots: string[] = [];
|
||||
|
||||
if (startPath) {
|
||||
// When start path is provided (from fileURLToPath(import.meta.url))
|
||||
possibleRoots.push(
|
||||
// When in dist/utils (compiled code) - go up 2 levels
|
||||
path.resolve(startPath, '..', '..'),
|
||||
// When in dist/ (compiled code) - go up 1 level
|
||||
path.resolve(startPath, '..'),
|
||||
// Direct parent directories
|
||||
path.resolve(startPath)
|
||||
);
|
||||
}
|
||||
|
||||
// Try to use require.resolve to find the module location (works in CommonJS and ESM with createRequire)
|
||||
try {
|
||||
// In ESM, we can use import.meta.resolve, but it's async in some versions
|
||||
// So we'll try to find the module by checking the node_modules structure
|
||||
|
||||
// Check if this file is in a node_modules installation
|
||||
const currentFile = new Error().stack?.split('\n')[2]?.match(/\((.+?):\d+:\d+\)$/)?.[1];
|
||||
if (currentFile) {
|
||||
const nodeModulesIndex = currentFile.indexOf('node_modules');
|
||||
if (nodeModulesIndex !== -1) {
|
||||
// Extract the package path from node_modules
|
||||
const afterNodeModules = currentFile.substring(nodeModulesIndex + 'node_modules'.length + 1);
|
||||
const packageNameEnd = afterNodeModules.indexOf(path.sep);
|
||||
if (packageNameEnd !== -1) {
|
||||
const packagePath = currentFile.substring(0, nodeModulesIndex + 'node_modules'.length + 1 + packageNameEnd);
|
||||
possibleRoots.push(packagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
// Check module.filename location (works in Node.js when available)
|
||||
if (typeof __filename !== 'undefined') {
|
||||
const moduleDir = path.dirname(__filename);
|
||||
possibleRoots.push(
|
||||
path.resolve(moduleDir, '..', '..'),
|
||||
path.resolve(moduleDir, '..')
|
||||
);
|
||||
}
|
||||
|
||||
// Check common installation locations
|
||||
possibleRoots.push(
|
||||
// Current working directory (for development/tests)
|
||||
process.cwd(),
|
||||
// Parent of cwd
|
||||
path.resolve(process.cwd(), '..')
|
||||
);
|
||||
|
||||
if (debug) {
|
||||
console.log('DEBUG: Searching for package.json from:', startPath || 'multiple locations');
|
||||
console.log('DEBUG: Checking paths:', possibleRoots);
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
const uniqueRoots = [...new Set(possibleRoots)];
|
||||
|
||||
for (const root of uniqueRoots) {
|
||||
const packageJsonPath = path.join(root, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
if (pkg.name === 'mcphub' || pkg.name === '@samanhappy/mcphub') {
|
||||
if (debug) {
|
||||
console.log(`DEBUG: Found package.json at ${packageJsonPath}`);
|
||||
}
|
||||
// Cache the result if no specific start path was requested
|
||||
if (!startPath) {
|
||||
cachedPackageRoot = root;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue to the next potential root
|
||||
if (debug) {
|
||||
console.error(`DEBUG: Failed to parse package.json at ${packageJsonPath}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.warn('DEBUG: Could not find package root directory');
|
||||
}
|
||||
|
||||
// Cache null result as well to avoid repeated searches
|
||||
if (!startPath) {
|
||||
cachedPackageRoot = null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function getParentPath(p: string, filename: string): string {
|
||||
if (p.endsWith(filename)) {
|
||||
p = p.slice(0, -filename.length);
|
||||
@@ -40,22 +201,36 @@ export const getConfigFilePath = (filename: string, description = 'Configuration
|
||||
}
|
||||
|
||||
const potentialPaths = [
|
||||
...[
|
||||
// Prioritize process.cwd() as the first location to check
|
||||
path.resolve(process.cwd(), filename),
|
||||
// Use path relative to the root directory
|
||||
path.join(rootDir, filename),
|
||||
// If installed with npx, may need to look one level up
|
||||
path.join(dirname(rootDir), filename),
|
||||
],
|
||||
];
|
||||
|
||||
// Also check in the installed package root directory
|
||||
const packageRoot = findPackageRoot();
|
||||
if (packageRoot) {
|
||||
potentialPaths.push(path.join(packageRoot, filename));
|
||||
}
|
||||
|
||||
for (const filePath of potentialPaths) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
// If all paths do not exist, check if we have a fallback in the package root
|
||||
// If the file exists in the package root, use it as the default
|
||||
if (packageRoot) {
|
||||
const packageConfigPath = path.join(packageRoot, filename);
|
||||
if (fs.existsSync(packageConfigPath)) {
|
||||
console.log(`Using ${description} from package: ${packageConfigPath}`);
|
||||
return packageConfigPath;
|
||||
}
|
||||
}
|
||||
|
||||
// If all paths do not exist, use default path
|
||||
// Using the default path is acceptable because it ensures the application can proceed
|
||||
// even if the configuration file is missing. This fallback is particularly useful in
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { findPackageRoot } from './path.js';
|
||||
|
||||
/**
|
||||
* Gets the package version from package.json
|
||||
* @param searchPath Optional path to start searching from (defaults to cwd)
|
||||
* @returns The version string from package.json, or 'dev' if not found
|
||||
*/
|
||||
export const getPackageVersion = (): string => {
|
||||
export const getPackageVersion = (searchPath?: string): string => {
|
||||
try {
|
||||
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
|
||||
// Use provided path or fallback to current working directory
|
||||
const startPath = searchPath || process.cwd();
|
||||
|
||||
const packageRoot = findPackageRoot(startPath);
|
||||
if (!packageRoot) {
|
||||
console.warn('Could not find package root, using default version');
|
||||
return 'dev';
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(packageRoot, 'package.json');
|
||||
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
|
||||
const packageJson = JSON.parse(packageJsonContent);
|
||||
return packageJson.version || 'dev';
|
||||
|
||||
@@ -8,6 +8,11 @@ Object.assign(process.env, {
|
||||
DATABASE_URL: 'sqlite::memory:',
|
||||
});
|
||||
|
||||
// Mock moduleDir to avoid import.meta parsing issues in Jest
|
||||
jest.mock('../src/utils/moduleDir.js', () => ({
|
||||
getCurrentModuleDir: jest.fn(() => process.cwd()),
|
||||
}));
|
||||
|
||||
// Global test utilities
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
|
||||
Reference in New Issue
Block a user