**feat: improve TensorFlow.js compatibility and remove unnecessary patches**

- Removed outdated Node.js v24 compatibility patches for `TextEncoder` and `TextDecoder` in `cli-wrapper.js` and other modules since TensorFlow.js now supports Node.js v24+ natively.
- Introduced a utility module `src/utils/tensorflowUtils.ts` for TensorFlow.js compatibility, defining global `PlatformNode` and related utilities.
- Enhanced `fileSystemStorage.ts` with improved Node.js module loading mechanisms for better compatibility across environments.
- Simplified dependency requirements by reducing the minimum Node.js version to `>=23.0.0`.
- Updated `package.json`, `cli-package/package.json`, and `README.md` versions to `0.9.25`.
- Harmonized `cli-package` and main package dependencies to ensure version alignment.
- Improved global compatibility with TensorFlow.js in mixed Node.js environments.

This update modernizes TensorFlow.js integration, reduces maintenance efforts, and aligns compatibility with current Node.js and TensorFlow.js capabilities.
This commit is contained in:
David Snelling 2025-07-02 16:16:19 -07:00
parent 16748436d1
commit eb46d816d7
20 changed files with 428 additions and 168 deletions

View file

@ -306,7 +306,7 @@ export async function createMemoryAugmentation(
// Otherwise, select based on environment
// Use the global isNode variable from the environment detection
const isNodeEnv = globalThis.__ENV__?.isNode || (
const isNodeEnv = (globalThis as any).__ENV__?.isNode || (
typeof process !== 'undefined' &&
process.versions != null &&
process.versions.node != null

20
src/global.d.ts vendored
View file

@ -1,20 +0,0 @@
/**
* Global type declarations for Brainy
*/
// Extend the globalThis interface to include the __ENV__ property
declare global {
var __ENV__: {
isBrowser: boolean
isNode: string | false
isServerless: boolean
}
// Window interface extensions (worker-specific functions removed)
interface Window {
importTensorFlow?: () => Promise<any>
}
}
// This export is needed to make this file a module
export {}

View file

@ -1,9 +1,52 @@
import { GraphVerb, HNSWNoun, StorageAdapter } from '../coreTypes.js'
// We'll dynamically import Node.js built-in modules
// Import Node.js built-in modules
// Using require for compatibility with Node.js
let fs: any
let path: any
// Initialize these modules immediately if in Node.js environment
if (
typeof process !== 'undefined' &&
process.versions &&
process.versions.node
) {
try {
// Use require for Node.js built-in modules
fs = require('fs')
path = require('path')
// Verify that path has the required methods
if (!path || typeof path.resolve !== 'function') {
throw new Error('path module is missing required methods')
}
} catch (e) {
console.warn('Failed to load Node.js modules with require:', e)
// Try using dynamic import as a fallback
try {
// Use a synchronous approach to ensure modules are loaded before continuing
const pathModule = require('node:path')
const fsModule = require('node:fs')
path = pathModule
fs = fsModule
// Verify that path has the required methods
if (!path || typeof path.resolve !== 'function') {
throw new Error(
'path module from node:path is missing required methods'
)
}
} catch (nodeImportError) {
console.warn(
'Failed to load Node.js modules with node: prefix:',
nodeImportError
)
}
}
}
// Constants for directory and file names
const ROOT_DIR = 'brainy-data'
const NOUNS_DIR = 'nouns'
@ -60,37 +103,125 @@ export class FileSystemStorage implements StorageAdapter {
}
try {
// Dynamically import Node.js built-in modules
try {
// Import the modules
const fsModule = await import('fs')
const pathModule = await import('path')
// Assign to our module-level variables
fs = fsModule.default || fsModule
path = pathModule.default || pathModule
// Now set up the directory paths
const rootDir = this.rootDir || process.cwd()
this.rootDir = path.resolve(rootDir, ROOT_DIR)
this.nounsDir = path.join(this.rootDir, NOUNS_DIR)
this.verbsDir = path.join(this.rootDir, VERBS_DIR)
this.metadataDir = path.join(this.rootDir, METADATA_DIR)
// Set up noun type directory paths
this.personDir = path.join(this.nounsDir, PERSON_DIR)
this.placeDir = path.join(this.nounsDir, PLACE_DIR)
this.thingDir = path.join(this.nounsDir, THING_DIR)
this.eventDir = path.join(this.nounsDir, EVENT_DIR)
this.conceptDir = path.join(this.nounsDir, CONCEPT_DIR)
this.contentDir = path.join(this.nounsDir, CONTENT_DIR)
this.defaultDir = path.join(this.nounsDir, DEFAULT_DIR)
} catch (importError) {
throw new Error(
`Failed to import Node.js modules: ${importError}. This adapter requires a Node.js environment.`
// Check if fs and path modules are available and have required methods
if (!fs || !path || typeof path.resolve !== 'function') {
console.log(
'Node.js modules not properly loaded, attempting to load them now'
)
// Try multiple approaches to load the modules
const loadAttempts = [
// Attempt 1: Use require
async () => {
console.log('Attempting to load Node.js modules with require()')
const fsModule = require('fs')
const pathModule = require('path')
if (!pathModule || typeof pathModule.resolve !== 'function') {
throw new Error('path.resolve is not a function after require()')
}
return { fs: fsModule, path: pathModule }
},
// Attempt 2: Use require with node: prefix
async () => {
console.log(
'Attempting to load Node.js modules with require("node:...")'
)
const fsModule = require('node:fs')
const pathModule = require('node:path')
if (!pathModule || typeof pathModule.resolve !== 'function') {
throw new Error(
'path.resolve is not a function after require("node:path")'
)
}
return { fs: fsModule, path: pathModule }
},
// Attempt 3: Use dynamic import
async () => {
console.log(
'Attempting to load Node.js modules with dynamic import'
)
const fsModule = await import('fs')
const pathModule = await import('path')
const fsResolved = fsModule.default || fsModule
const pathResolved = pathModule.default || pathModule
if (!pathResolved || typeof pathResolved.resolve !== 'function') {
throw new Error(
'path.resolve is not a function after dynamic import'
)
}
return { fs: fsResolved, path: pathResolved }
},
// Attempt 4: Use dynamic import with node: prefix
async () => {
console.log(
'Attempting to load Node.js modules with dynamic import("node:...")'
)
const fsModule = await import('node:fs')
const pathModule = await import('node:path')
const fsResolved = fsModule.default || fsModule
const pathResolved = pathModule.default || pathModule
if (!pathResolved || typeof pathResolved.resolve !== 'function') {
throw new Error(
'path.resolve is not a function after dynamic import("node:path")'
)
}
return { fs: fsResolved, path: pathResolved }
}
]
// Try each loading method until one succeeds
let lastError = null
for (const loadAttempt of loadAttempts) {
try {
const modules = await loadAttempt()
fs = modules.fs
path = modules.path
console.log('Successfully loaded Node.js modules')
break
} catch (error) {
lastError = error
console.warn(`Module loading attempt failed:`, error)
// Continue to the next attempt
}
}
// If all attempts failed, throw an error
if (!fs || !path || typeof path.resolve !== 'function') {
throw new Error(
`Failed to import Node.js modules after multiple attempts: ${lastError}. This adapter requires a Node.js environment.`
)
}
}
// Now set up the directory paths
const rootDir = this.rootDir || process.cwd()
this.rootDir = path.resolve(rootDir, ROOT_DIR)
this.nounsDir = path.join(this.rootDir, NOUNS_DIR)
this.verbsDir = path.join(this.rootDir, VERBS_DIR)
this.metadataDir = path.join(this.rootDir, METADATA_DIR)
// Set up noun type directory paths
this.personDir = path.join(this.nounsDir, PERSON_DIR)
this.placeDir = path.join(this.nounsDir, PLACE_DIR)
this.thingDir = path.join(this.nounsDir, THING_DIR)
this.eventDir = path.join(this.nounsDir, EVENT_DIR)
this.conceptDir = path.join(this.nounsDir, CONCEPT_DIR)
this.contentDir = path.join(this.nounsDir, CONTENT_DIR)
this.defaultDir = path.join(this.nounsDir, DEFAULT_DIR)
// Create directories if they don't exist
await this.ensureDirectoryExists(this.rootDir)
await this.ensureDirectoryExists(this.nounsDir)

View file

@ -1,14 +1,35 @@
// Type declarations for TensorFlow.js models
/**
* Type definitions for TensorFlow.js compatibility
* This file exports type definitions for TensorFlow.js utilities
*/
// This file is a placeholder for TensorFlow.js model types
// We're using type assertions in the code instead of module declarations
// Define some basic types that might be useful
export interface TensorflowModel {
load(): Promise<any>;
embed(data: string[]): any;
dispose(): void;
// Define the shape of the util object used for TensorFlow.js compatibility
export interface TensorFlowUtilObject {
isFloat32Array: (arr: unknown) => boolean
isTypedArray: (arr: unknown) => boolean
[key: string]: unknown
}
// Export a dummy constant to make this a proper module
export const tensorflowModelsLoaded = true;
// Define the shape of the PlatformNode object that might exist in global
export interface PlatformNodeObject {
isFloat32Array?: (arr: unknown) => boolean
isTypedArray?: (arr: unknown) => boolean
[key: string]: unknown
}
// Define the shape of the tf object that might exist in global
export interface TensorFlowObject {
util?: TensorFlowUtilObject
[key: string]: unknown
}
// Extend the Window and WorkerGlobalScope interfaces to include the importTensorFlow function
declare global {
interface Window {
importTensorFlow?: () => Promise<any>
}
interface WorkerGlobalScope {
importTensorFlow?: () => Promise<any>
}
}

View file

@ -18,7 +18,7 @@ export const environment = {
// Make environment information available globally
if (typeof globalThis !== 'undefined') {
globalThis.__ENV__ = environment
;(globalThis as any).__ENV__ = environment
}
// Log the detected environment

View file

@ -20,6 +20,23 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
private use: any = null
private backend: string = 'cpu' // Default to CPU
/**
* Add polyfills and patches for TensorFlow.js compatibility
* This addresses issues with TensorFlow.js in Node.js environments
*/
private addNodeCompatibilityPolyfills(): void {
// Only apply in Node.js environment
if (
typeof process === 'undefined' ||
!process.versions ||
!process.versions.node
) {
return
}
// No compatibility patches needed - TensorFlow.js now works correctly with Node.js 24+
}
/**
* Initialize the embedding model
*/
@ -42,6 +59,9 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
originalWarn(message, ...optionalParams)
}
// Add polyfills for TensorFlow.js compatibility
this.addNodeCompatibilityPolyfills()
// TensorFlow.js will use its default EPSILON value
// Dynamically import TensorFlow.js core module and backends

View file

@ -0,0 +1,80 @@
/**
* Utility functions for TensorFlow.js compatibility
* This module provides utility functions that TensorFlow.js might need
* and avoids the need to use globalThis.util
*/
// Import the TensorFlowUtilObject interface
import type {
TensorFlowUtilObject,
PlatformNodeObject
} from '../types/tensorflowTypes.js'
// Define a global PlatformNode class for TensorFlow.js compatibility
// This is needed because TensorFlow.js creates its own PlatformNode instance
// and we need to ensure it uses the correct TextEncoder/TextDecoder
if (
typeof global !== 'undefined' &&
typeof process !== 'undefined' &&
process.versions &&
process.versions.node &&
process.versions.node.split('.')[0] >= '23'
) {
try {
// Define the PlatformNode class that TensorFlow.js will use
class PlatformNode {
util: any
textEncoder: any
textDecoder: any
constructor() {
// Create a util object with the necessary methods
this.util = {
isFloat32Array,
isTypedArray,
// Use the global TextEncoder/TextDecoder directly
TextEncoder: typeof TextEncoder !== 'undefined' ? TextEncoder : null,
TextDecoder: typeof TextDecoder !== 'undefined' ? TextDecoder : null
}
// Initialize TextEncoder/TextDecoder directly from globals
this.textEncoder = new TextEncoder()
this.textDecoder = new TextDecoder()
}
}
// Assign the PlatformNode class to the global object
;(global as any).PlatformNode = PlatformNode
// Also create an instance and assign it to global.platformNode (lowercase p)
// Some TensorFlow.js code might look for this
;(global as any).platformNode = new PlatformNode()
console.log(
'Defined global PlatformNode class for TensorFlow.js compatibility'
)
} catch (error) {
console.warn('Failed to define global PlatformNode class:', error)
}
}
/**
* Check if an array is a Float32Array
* @param arr - The array to check
* @returns True if the array is a Float32Array
*/
export function isFloat32Array(arr: unknown): boolean {
return !!(
arr instanceof Float32Array ||
(arr && Object.prototype.toString.call(arr) === '[object Float32Array]')
)
}
/**
* Check if an array is a TypedArray
* @param arr - The array to check
* @returns True if the array is a TypedArray
*/
export function isTypedArray(arr: unknown): boolean {
return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView))
}

View file

@ -3,4 +3,4 @@
* Do not modify this file directly.
*/
export const VERSION = '0.9.22';
export const VERSION = '0.9.25';