mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -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';
|
} from './services/sseService.js';
|
||||||
import { initializeDefaultUser } from './models/User.js';
|
import { initializeDefaultUser } from './models/User.js';
|
||||||
import { sseUserContextMiddleware } from './middlewares/userContext.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 {
|
export class AppServer {
|
||||||
private app: express.Application;
|
private app: express.Application;
|
||||||
@@ -167,10 +184,11 @@ export class AppServer {
|
|||||||
private findFrontendDistPath(): string | null {
|
private findFrontendDistPath(): string | null {
|
||||||
// Debug flag for detailed logging
|
// Debug flag for detailed logging
|
||||||
const debug = process.env.DEBUG === 'true';
|
const debug = process.env.DEBUG === 'true';
|
||||||
|
const currentDir = getCurrentFileDir();
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log('DEBUG: Current directory:', process.cwd());
|
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
|
// 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)
|
// Helper method to find the package root (where package.json is located)
|
||||||
private findPackageRoot(): string | null {
|
private findPackageRoot(): string | null {
|
||||||
const debug = process.env.DEBUG === 'true';
|
// Use the shared utility function which properly handles ESM module paths
|
||||||
|
const currentDir = getCurrentFileDir();
|
||||||
// Possible locations for package.json
|
return findPackageRoot(currentDir);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
|
import { getCurrentModuleDir } from './moduleDir.js';
|
||||||
|
|
||||||
// Project root directory - use process.cwd() as a simpler alternative
|
// Project root directory - use process.cwd() as a simpler alternative
|
||||||
const rootDir = process.cwd();
|
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 {
|
function getParentPath(p: string, filename: string): string {
|
||||||
if (p.endsWith(filename)) {
|
if (p.endsWith(filename)) {
|
||||||
p = p.slice(0, -filename.length);
|
p = p.slice(0, -filename.length);
|
||||||
@@ -40,22 +201,36 @@ export const getConfigFilePath = (filename: string, description = 'Configuration
|
|||||||
}
|
}
|
||||||
|
|
||||||
const potentialPaths = [
|
const potentialPaths = [
|
||||||
...[
|
|
||||||
// Prioritize process.cwd() as the first location to check
|
// Prioritize process.cwd() as the first location to check
|
||||||
path.resolve(process.cwd(), filename),
|
path.resolve(process.cwd(), filename),
|
||||||
// Use path relative to the root directory
|
// Use path relative to the root directory
|
||||||
path.join(rootDir, filename),
|
path.join(rootDir, filename),
|
||||||
// If installed with npx, may need to look one level up
|
// If installed with npx, may need to look one level up
|
||||||
path.join(dirname(rootDir), filename),
|
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) {
|
for (const filePath of potentialPaths) {
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
return 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
|
// If all paths do not exist, use default path
|
||||||
// Using the default path is acceptable because it ensures the application can proceed
|
// 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
|
// even if the configuration file is missing. This fallback is particularly useful in
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { findPackageRoot } from './path.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the package version from package.json
|
* 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
|
* @returns The version string from package.json, or 'dev' if not found
|
||||||
*/
|
*/
|
||||||
export const getPackageVersion = (): string => {
|
export const getPackageVersion = (searchPath?: string): string => {
|
||||||
try {
|
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 packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
|
||||||
const packageJson = JSON.parse(packageJsonContent);
|
const packageJson = JSON.parse(packageJsonContent);
|
||||||
return packageJson.version || 'dev';
|
return packageJson.version || 'dev';
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ Object.assign(process.env, {
|
|||||||
DATABASE_URL: 'sqlite::memory:',
|
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
|
// Global test utilities
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
|||||||
Reference in New Issue
Block a user