From a5ce47ffdb7eb442cb8904657b194b56ebdb38b1 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Wed, 17 Sep 2025 14:53:54 -0700 Subject: [PATCH] feat: add browser environment compatibility support - Remove Node.js-specific imports from module level - Use dynamic imports with isNode() environment checks - Wrap all file system operations in conditional blocks - Add fallback values for browser environments - Ensure code works in both Node.js and browser contexts This change enables Brainy to run in browser environments without requiring Node.js polyfills, making it truly universal. Co-Authored-By: dpsifr --- src/augmentations/configResolver.ts | 97 ++++++++++++++++++----------- src/embeddings/EmbeddingManager.ts | 94 +++++++++++++++++++++------- src/utils/version.ts | 89 +++++++++++++++++++++----- 3 files changed, 204 insertions(+), 76 deletions(-) diff --git a/src/augmentations/configResolver.ts b/src/augmentations/configResolver.ts index fa467d5b..5052f6b5 100644 --- a/src/augmentations/configResolver.ts +++ b/src/augmentations/configResolver.ts @@ -8,10 +8,8 @@ * - Default values from schema */ -import { existsSync, readFileSync } from 'node:fs' -import { join } from 'node:path' -import { homedir } from 'node:os' import { JSONSchema } from './manifest.js' +import { isNode } from '../utils/environment.js' /** * Configuration source priority (highest to lowest) @@ -53,14 +51,32 @@ export class AugmentationConfigResolver { private resolved: any = {} constructor(private options: ConfigResolverOptions) { + // Default config paths - include home directory paths only in Node.js + const defaultConfigPaths = [ + '.brainyrc', + '.brainyrc.json', + 'brainy.config.json' + ] + + // Add home directory paths in Node.js environments + if (isNode()) { + try { + const nodeRequire = typeof require !== 'undefined' ? require : null + if (nodeRequire) { + const path = nodeRequire('node:path') + const os = nodeRequire('node:os') + defaultConfigPaths.push( + path.join(os.homedir(), '.brainy', 'config.json'), + path.join(os.homedir(), '.brainyrc') + ) + } + } catch { + // Silently continue without home directory paths + } + } + this.options = { - configPaths: [ - '.brainyrc', - '.brainyrc.json', - 'brainy.config.json', - join(homedir(), '.brainy', 'config.json'), - join(homedir(), '.brainyrc') - ], + configPaths: defaultConfigPaths, envPrefix: `BRAINY_AUG_${options.augmentationId.toUpperCase()}_`, allowUndefined: true, ...options @@ -133,32 +149,41 @@ export class AugmentationConfigResolver { */ private loadFromFiles(): void { // Skip in browser environment - if (typeof process === 'undefined' || typeof window !== 'undefined') { + if (!isNode()) { return } - - for (const configPath of this.options.configPaths || []) { - try { - if (existsSync(configPath)) { - const content = readFileSync(configPath, 'utf8') - const config = this.parseConfigFile(content, configPath) - - // Extract augmentation-specific configuration - const augConfig = this.extractAugmentationConfig(config) - - if (augConfig && Object.keys(augConfig).length > 0) { - this.sources.push({ - priority: ConfigPriority.FILE, - source: `file:${configPath}`, - config: augConfig - }) - break // Use first found config file + + try { + const nodeRequire = typeof require !== 'undefined' ? require : null + if (!nodeRequire) return + + const fs = nodeRequire('node:fs') + + for (const configPath of this.options.configPaths || []) { + try { + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, 'utf8') + const config = this.parseConfigFile(content, configPath) + + // Extract augmentation-specific configuration + const augConfig = this.extractAugmentationConfig(config) + + if (augConfig && Object.keys(augConfig).length > 0) { + this.sources.push({ + priority: ConfigPriority.FILE, + source: `file:${configPath}`, + config: augConfig + }) + break // Use first found config file + } } + } catch (error) { + // Silently ignore file errors + console.debug(`Failed to load config from ${configPath}:`, error) } - } catch (error) { - // Silently ignore file errors - console.debug(`Failed to load config from ${configPath}:`, error) } + } catch { + // Silently continue if require fails } } @@ -205,7 +230,7 @@ export class AugmentationConfigResolver { */ private loadFromEnvironment(): void { // Skip in browser environment - if (typeof process === 'undefined' || !process.env) { + if (!isNode() || !process.env) { return } @@ -442,16 +467,16 @@ export class AugmentationConfigResolver { */ async saveToFile(filepath?: string, format: 'json' = 'json'): Promise { // Skip in browser environment - if (typeof process === 'undefined' || typeof window !== 'undefined') { + if (!isNode()) { throw new Error('Cannot save configuration files in browser environment') } - + const fs = await import('node:fs') const path = await import('node:path') - + const configPath = filepath || this.options.configPaths?.[0] || '.brainyrc' const augId = this.options.augmentationId - + // Load existing config if it exists let fullConfig: any = {} try { diff --git a/src/embeddings/EmbeddingManager.ts b/src/embeddings/EmbeddingManager.ts index 7c3f6c58..bc10f51a 100644 --- a/src/embeddings/EmbeddingManager.ts +++ b/src/embeddings/EmbeddingManager.ts @@ -18,8 +18,7 @@ import { Vector, EmbeddingFunction } from '../coreTypes.js' import { pipeline, env } from '@huggingface/transformers' -import { existsSync } from 'node:fs' -import { join } from 'node:path' +import { isNode } from '../utils/environment.js' // Types export type ModelPrecision = 'q8' | 'fp32' @@ -124,13 +123,24 @@ export class EmbeddingManager { env.cacheDir = modelsPath env.allowLocalModels = true env.useFSCache = true - - // Check if models exist locally - const modelPath = join(modelsPath, ...this.modelName.split('/')) - const hasLocalModels = existsSync(modelPath) - - if (hasLocalModels) { - console.log('✅ Using cached models from:', modelPath) + + // Check if models exist locally (only in Node.js) + if (isNode()) { + try { + const nodeRequire = typeof require !== 'undefined' ? require : null + if (nodeRequire) { + const path = nodeRequire('node:path') + const fs = nodeRequire('node:fs') + const modelPath = path.join(modelsPath, ...this.modelName.split('/')) + const hasLocalModels = fs.existsSync(modelPath) + + if (hasLocalModels) { + console.log('✅ Using cached models from:', modelPath) + } + } + } catch { + // Silently continue if require fails + } } // Configure pipeline options for the selected precision @@ -256,24 +266,60 @@ export class EmbeddingManager { /** * Get models directory path + * Note: In browser environments, returns a simple default path + * In Node.js, checks multiple locations for the models directory */ private getModelsPath(): string { - // Check various possible locations - const paths = [ - process.env.BRAINY_MODELS_PATH, - './models', - join(process.cwd(), 'models'), - join(process.env.HOME || '', '.brainy', 'models') - ] - - for (const path of paths) { - if (path && existsSync(path)) { - return path - } + // In browser environments, use a default path + if (!isNode()) { + return './models' + } + + // Node.js-specific model path resolution + // Cache the result for performance + if (!this.modelsPathCache) { + this.modelsPathCache = this.resolveModelsPathSync() + } + return this.modelsPathCache + } + + private modelsPathCache: string | null = null + + private resolveModelsPathSync(): string { + // For Node.js environments, we can safely assume these modules exist + // TypeScript will handle the imports at build time + // At runtime, these will only be called if isNode() is true + + // Default fallback path + const defaultPath = './models' + + try { + // Create a conditional require function that only works in Node + const nodeRequire = typeof require !== 'undefined' ? require : null + if (!nodeRequire) return defaultPath + + const fs = nodeRequire('node:fs') + const path = nodeRequire('node:path') + + const paths = [ + process.env.BRAINY_MODELS_PATH, + './models', + path.join(process.cwd(), 'models'), + path.join(process.env.HOME || '', '.brainy', 'models') + ] + + for (const p of paths) { + if (p && fs.existsSync(p)) { + return p + } + } + + // Default Node.js path + return path.join(process.cwd(), 'models') + } catch { + // Fallback if require fails + return defaultPath } - - // Default - return join(process.cwd(), 'models') } /** diff --git a/src/utils/version.ts b/src/utils/version.ts index 1a2ca5e6..1be7a249 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -2,32 +2,89 @@ * Version utilities for Brainy */ -import { readFileSync } from 'node:fs' -import { join, dirname } from 'node:path' -import { fileURLToPath } from 'node:url' +import { isNode } from './environment.js' -// Get package.json path relative to this file -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) -const packageJsonPath = join(__dirname, '../../package.json') +// Default version - this should be replaced at build time +const DEFAULT_VERSION = '3.5.1' let cachedVersion: string | null = null +let versionPromise: Promise | null = null + +/** + * Load version from package.json in Node.js environment + */ +async function loadVersionFromPackageJson(): Promise { + if (!isNode()) { + return DEFAULT_VERSION + } + + try { + // Dynamic imports for Node.js modules - modern approach + const [{ readFileSync }, { join, dirname }, { fileURLToPath }] = await Promise.all([ + import('node:fs'), + import('node:path'), + import('node:url') + ]) + + const __filename = fileURLToPath(import.meta.url) + const __dirname = dirname(__filename) + const packageJsonPath = join(__dirname, '../../package.json') + + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) + return packageJson.version || DEFAULT_VERSION + } catch (error) { + // Silently fall back to default version + return DEFAULT_VERSION + } +} /** * Get the current Brainy package version + * In Node.js, attempts to read from package.json + * In browser, returns the default version * @returns The current version string */ export function getBrainyVersion(): string { - if (!cachedVersion) { - try { - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) - cachedVersion = packageJson.version - } catch { - // Fallback version if package.json can't be read - cachedVersion = '2.7.1' - } + if (cachedVersion) { + return cachedVersion } - return cachedVersion! + + // In browser or if we need immediate response, return default + if (!isNode()) { + cachedVersion = DEFAULT_VERSION + return cachedVersion + } + + // For Node.js, try to load synchronously first time + // This is a compromise for backward compatibility + if (!versionPromise) { + versionPromise = loadVersionFromPackageJson() + versionPromise.then(version => { + cachedVersion = version + }) + } + + // Return default while loading + return cachedVersion || DEFAULT_VERSION +} + +/** + * Get the current Brainy package version asynchronously + * Guaranteed to attempt loading from package.json in Node.js + * @returns Promise resolving to the current version string + */ +export async function getBrainyVersionAsync(): Promise { + if (cachedVersion) { + return cachedVersion + } + + if (!versionPromise) { + versionPromise = loadVersionFromPackageJson() + } + + const version = await versionPromise + cachedVersion = version + return version } /**