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 <noreply@dpsifr.com>
This commit is contained in:
David Snelling 2025-09-17 14:53:54 -07:00
parent 14ccbf6556
commit 7ab090fedf
3 changed files with 204 additions and 76 deletions

View file

@ -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<void> {
// 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 {

View file

@ -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')
}
/**

View file

@ -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<string> | null = null
/**
* Load version from package.json in Node.js environment
*/
async function loadVersionFromPackageJson(): Promise<string> {
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<string> {
if (cachedVersion) {
return cachedVersion
}
if (!versionPromise) {
versionPromise = loadVersionFromPackageJson()
}
const version = await versionPromise
cachedVersion = version
return version
}
/**