Skip to content

Environment Adaptability

The content system is designed to work across different JavaScript environments with intelligent adaptation to the capabilities and constraints of each environment. This document explains the environment architecture, detection mechanisms, and implementation strategies.

Environment Types

The system supports several key environments:

Node.js (Server)

Node.js environments provide:

  • Full filesystem access
  • No browser APIs
  • CommonJS or ESM module support
  • Server-side processing capabilities
  • No DOM or window object
  • Enhanced system-level capabilities

Browser (Client)

Browser environments provide:

  • DOM access and manipulation
  • Browser storage APIs (IndexedDB, localStorage)
  • Window and document objects
  • Limited filesystem access (via File API)
  • Cross-origin restrictions
  • Rendering capabilities

Edge/Serverless

Edge environments provide a hybrid model:

  • Limited filesystem access
  • Partial browser APIs
  • Execution time constraints
  • Stateless operation model
  • Restricted module loading
  • Optimized for network operations

Service Workers

Service workers provide specialized capabilities:

  • Offline operation
  • Background processing
  • Request interception
  • Cache management
  • Push notifications
  • Proxy-like behavior

Environment Detection Architecture

Detection Mechanisms

The system uses multiple detection strategies:

Feature Detection

Tests for specific features rather than environments:

typescript
// Feature detection for filesystem
function hasFileSystem() {
  try {
    return (
      typeof require === 'function' &&
      typeof require('fs').readFile === 'function'
    )
  } catch (error) {
    return false
  }
}

// Feature detection for IndexedDB
function hasIndexedDB() {
  return (
    typeof window !== 'undefined' && typeof window.indexedDB !== 'undefined'
  )
}

// Feature detection for ServiceWorker
function hasServiceWorker() {
  return typeof navigator !== 'undefined' && 'serviceWorker' in navigator
}

Environment Variables

Uses environment variables or globals:

typescript
// Environment variable detection
function isProduction() {
  return process?.env?.NODE_ENV === 'production'
}

// Browser detection
function isBrowser() {
  return typeof window !== 'undefined' && typeof document !== 'undefined'
}

// Node.js detection
function isNode() {
  return (
    typeof process !== 'undefined' &&
    process.versions != null &&
    process.versions.node != null
  )
}

Packager Metadata

Leverages packager-specific information:

typescript
// Webpack detection
function isWebpack() {
  return typeof __webpack_require__ !== 'undefined'
}

// Rollup detection
function isRollup() {
  return typeof import.meta !== 'undefined'
}

Environment Modules

Environment detection is centralized:

typescript
// Environment utility module
export const env = {
  // Environment types
  isNode: isNode(),
  isBrowser: isBrowser(),
  isServiceWorker: isServiceWorker(),
  isEdge: isEdge(),

  // Feature detection
  hasFileSystem: hasFileSystem(),
  hasIndexedDB: hasIndexedDB(),
  hasLocalStorage: hasLocalStorage(),
  hasServiceWorker: hasServiceWorkerSupport(),

  // Environment characteristics
  isServer: isNode() || isEdge(),
  isClient: isBrowser() || isServiceWorker(),

  // Runtime detection
  isDevelopment: process?.env?.NODE_ENV === 'development',
  isProduction: process?.env?.NODE_ENV === 'production',
  isTest: process?.env?.NODE_ENV === 'test',
}

Implementing Environment-Specific Behavior

Adapter Selection

Adapters are selected based on environment:

typescript
import { env } from '@lib/utils/env'
import { createFileSystemAdapter } from '@lib/content/adapters/node'
import { createIndexedDBAdapter } from '@lib/content/adapters/browser'
import { createMemoryAdapter } from '@lib/content/adapters/common'

function createDefaultAdapter() {
  // Select appropriate adapter based on environment
  if (env.hasFileSystem) {
    return createFileSystemAdapter({ basePath: '/content' })
  } else if (env.hasIndexedDB) {
    return createIndexedDBAdapter({ databaseName: 'content-db' })
  } else {
    // Fallback to memory adapter
    return createMemoryAdapter()
  }
}

// Create content store with environment-appropriate adapter
const store = createContentStore({
  adapter: createDefaultAdapter(),
})

Conditional Imports

Use environment-based imports:

typescript
// Dynamic imports based on environment
async function loadEnvironmentSpecificModule() {
  if (env.isNode) {
    // Load Node.js-specific module
    const { createServerAdapter } = await import('@lib/content/adapters/node')
    return createServerAdapter()
  } else if (env.isBrowser) {
    // Load browser-specific module
    const { createBrowserAdapter } = await import(
      '@lib/content/adapters/browser'
    )
    return createBrowserAdapter()
  } else {
    // Load common module
    const { createFallbackAdapter } = await import(
      '@lib/content/adapters/common'
    )
    return createFallbackAdapter()
  }
}

Feature-Based Branching

Conditionally execute code based on available features:

typescript
// Feature-based code branching
function createStorage() {
  if (env.hasIndexedDB) {
    return createIndexedDBStorage()
  } else if (env.hasLocalStorage) {
    return createLocalStorage()
  } else if (env.hasFileSystem) {
    return createFileSystemStorage()
  } else {
    return createMemoryStorage()
  }
}

// Use appropriate logging depending on environment
function createLogger() {
  if (env.isNode) {
    return createConsoleLogger()
  } else if (env.isBrowser && env.isDevelopment) {
    return createBrowserDevLogger()
  } else if (env.isBrowser && env.isProduction) {
    return createBrowserProdLogger()
  } else {
    return createNullLogger()
  }
}

Isomorphic Implementation

Universal Modules

Create modules that work in any environment:

typescript
// Universal content store that works in any environment
export function createUniversalContentStore(options = {}) {
  // Auto-detect environment if not specified
  const environment =
    options.environment ||
    (env.isNode ? 'node' : env.isBrowser ? 'browser' : 'memory')

  // Create appropriate adapter
  let adapter

  switch (environment) {
    case 'node':
      adapter = createFileSystemAdapter(options.adapter)
      break
    case 'browser':
      adapter = createIndexedDBAdapter(options.adapter)
      break
    default:
      adapter = createMemoryAdapter(options.adapter)
  }

  // Create store with environment-appropriate adapter
  return createContentStore({
    ...options,
    adapter,
  })
}

Capability-Based Design

Design interfaces based on capabilities, not environments:

typescript
// Capability-based design pattern
export function createContentStore(options = {}) {
  const capabilities = detectCapabilities()

  // Configure based on capabilities
  const configuration = {
    persistence: capabilities.hasPersistence ? 'full' : 'memory',
    concurrency: capabilities.hasConcurrency ? options.concurrency || 5 : 1,
    caching: capabilities.hasCaching ? options.caching || 'memory' : 'none',
    watching: capabilities.hasWatcher ? options.watching || 'auto' : 'none',
  }

  // Create appropriate components
  const components = {
    adapter: createAdapter(configuration),
    middleware: createMiddleware(configuration),
    events: createEventEmitter(configuration),
    cache: createCache(configuration),
  }

  // Assemble store
  return assembleStore(components, options)
}

// Capability detection
function detectCapabilities() {
  return {
    hasPersistence:
      env.hasFileSystem || env.hasIndexedDB || env.hasLocalStorage,
    hasConcurrency:
      env.isNode ||
      (env.isBrowser && typeof window.requestIdleCallback !== 'undefined'),
    hasCaching: true, // All environments support some form of caching
    hasWatcher: env.isNode || env.isBrowser,
  }
}

Environment Shims

Provide environment shims for missing features:

typescript
// FileSystem shim for browser environments
export const fsShim = {
  readFile: async (path, options) => {
    // In Node.js, delegate to real fs
    if (env.isNode) {
      const fs = require('fs/promises')
      return fs.readFile(path, options)
    }

    // In browser, use fetch or IndexedDB
    if (env.isBrowser) {
      if (path.startsWith('http')) {
        const response = await fetch(path)
        return options === 'utf8'
          ? await response.text()
          : await response.arrayBuffer()
      } else {
        // Access from IndexedDB
        return await readFromIndexedDB(path)
      }
    }

    throw new Error('FileSystem operations not supported in this environment')
  },

  writeFile: async (path, data, options) => {
    // Implementation for different environments
  },

  exists: async path => {
    // Implementation for different environments
  },
}

Environment-Specific Features

Node.js Features

Specialized functionality for Node.js:

typescript
// Node.js-specific enhancements
if (env.isNode) {
  // Add filesystem watching
  store.watchFileSystem = (pattern, callback) => {
    const chokidar = require('chokidar')
    const watcher = chokidar.watch(pattern, {
      persistent: true,
      ignoreInitial: true,
    })

    watcher.on('all', (event, path) => {
      // Convert file events to content events
      const eventMap = {
        add: 'create',
        change: 'update',
        unlink: 'delete',
      }

      const contentEvent = eventMap[event]
      if (contentEvent) {
        callback(path, null, contentEvent)
      }
    })

    return () => watcher.close()
  }

  // Add process monitoring
  store.onProcessExit = callback => {
    process.on('exit', callback)
    process.on('SIGINT', () => {
      callback()
      process.exit()
    })

    return () => {
      process.removeListener('exit', callback)
      process.removeListener('SIGINT', callback)
    }
  }
}

Browser Features

Specialized functionality for browsers:

typescript
// Browser-specific enhancements
if (env.isBrowser) {
  // Add offline support
  store.enableOfflineSupport = async () => {
    if (!env.hasServiceWorker) {
      throw new Error('Service Worker not supported in this browser')
    }

    await navigator.serviceWorker.register('/service-worker.js')

    store.isOffline = () => !navigator.onLine

    // Listen for online/offline events
    window.addEventListener('online', () => {
      store.events.emit('connection:online')
    })

    window.addEventListener('offline', () => {
      store.events.emit('connection:offline')
    })

    // Sync when coming back online
    store.events.on('connection:online', async () => {
      await store.syncOfflineChanges()
    })
  }

  // Add tab synchronization
  store.enableTabSync = () => {
    const channel = new BroadcastChannel('content-store-sync')

    channel.addEventListener('message', event => {
      if (event.data.type === 'content-change') {
        store.events.emit(event.data.eventType, event.data.payload)
      }
    })

    // Intercept store operations to broadcast
    const originalWrite = store.write
    store.write = async (uri, content, options) => {
      const result = await originalWrite(uri, content, options)

      channel.postMessage({
        type: 'content-change',
        eventType: 'update',
        payload: { uri, content },
      })

      return result
    }

    return () => channel.close()
  }
}

Service Worker Features

Specialized functionality for service workers:

typescript
// Service Worker-specific enhancements
if (env.isServiceWorker) {
  // Add content caching
  store.setupContentCache = async () => {
    const CACHE_NAME = 'content-cache-v1'

    // Open content cache
    const cache = await caches.open(CACHE_NAME)

    // Cache all content access
    self.addEventListener('fetch', event => {
      if (event.request.url.includes('/content/')) {
        event.respondWith(
          caches.match(event.request).then(response => {
            if (response) {
              // Return cached response
              return response
            }

            // Fetch and cache
            return fetch(event.request).then(response => {
              // Clone the response for caching
              const clonedResponse = response.clone()

              caches.open(CACHE_NAME).then(cache => {
                cache.put(event.request, clonedResponse)
              })

              return response
            })
          })
        )
      }
    })

    // Add background sync for offline changes
    self.addEventListener('sync', event => {
      if (event.tag === 'content-sync') {
        event.waitUntil(syncOfflineChanges())
      }
    })
  }
}

Bundling and Build Strategies

Tree-Shaking Support

Implement code for tree-shaking:

typescript
// Environment modules designed for tree shaking
export const isNode =
  typeof process !== 'undefined' &&
  process.versions != null &&
  process.versions.node != null

export const isBrowser =
  typeof window !== 'undefined' && typeof document !== 'undefined'

// Import only what you need
import { isNode } from '@lib/utils/env'

if (isNode) {
  // This code will be tree-shaken in browser builds
  const fs = require('fs')
  // ...
}

Conditional Exports

Use package.json to provide environment-specific entry points:

json
{
  "name": "content-system",
  "exports": {
    ".": {
      "node": "./dist/node/index.js",
      "browser": "./dist/browser/index.js",
      "default": "./dist/index.js"
    },
    "./adapters": {
      "node": "./dist/node/adapters/index.js",
      "browser": "./dist/browser/adapters/index.js",
      "default": "./dist/adapters/index.js"
    }
  }
}

Build Configuration

Configure build tools for environment targeting:

javascript
// webpack.config.js
module.exports = [
  // Node.js build
  {
    name: 'node',
    entry: './src/index.js',
    target: 'node',
    output: {
      path: path.resolve(__dirname, 'dist/node'),
      filename: 'index.js',
      libraryTarget: 'commonjs2',
    },
    // ...other config
  },

  // Browser build
  {
    name: 'browser',
    entry: './src/index.js',
    target: 'web',
    output: {
      path: path.resolve(__dirname, 'dist/browser'),
      filename: 'index.js',
      libraryTarget: 'umd',
    },
    // ...other config
  },
]

Testing Across Environments

Environment Mocking

Mock environments for testing:

typescript
// Environment mock utility
export function mockEnvironment(environment) {
  // Save original environment
  const original = { ...env }

  // Apply mocked environment
  Object.assign(env, {
    isNode: environment === 'node',
    isBrowser: environment === 'browser',
    isServiceWorker: environment === 'service-worker',

    hasFileSystem: environment === 'node',
    hasIndexedDB: environment === 'browser',
    hasLocalStorage: environment === 'browser',

    isServer: environment === 'node' || environment === 'edge',
    isClient: environment === 'browser' || environment === 'service-worker',
  })

  // Return restore function
  return () => {
    Object.assign(env, original)
  }
}

// Usage in tests
test('adapter selection in Node.js', () => {
  const restore = mockEnvironment('node')

  const store = createContentStore()
  expect(store.adapter).toBeInstanceOf(FileSystemAdapter)

  restore()
})

test('adapter selection in browser', () => {
  const restore = mockEnvironment('browser')

  const store = createContentStore()
  expect(store.adapter).toBeInstanceOf(IndexedDBAdapter)

  restore()
})

Cross-Environment Test Suites

Test in multiple environments:

typescript
// Common test suite factory
function createAdapterTests(createAdapter) {
  return () => {
    test('read operation', async () => {
      const adapter = createAdapter()
      // Test implementation
    })

    test('write operation', async () => {
      const adapter = createAdapter()
      // Test implementation
    })

    // More tests...
  }
}

// Run tests in each environment
describe(
  'FileSystemAdapter (Node.js)',
  createAdapterTests(() => {
    mockEnvironment('node')
    return createFileSystemAdapter({ basePath: '/tmp/test' })
  })
)

describe(
  'IndexedDBAdapter (Browser)',
  createAdapterTests(() => {
    mockEnvironment('browser')
    return createIndexedDBAdapter({ databaseName: 'test-db' })
  })
)

describe(
  'MemoryAdapter (Universal)',
  createAdapterTests(() => {
    return createMemoryAdapter()
  })
)

Deployment Considerations

Environment Configuration

Configure for different deployment environments:

typescript
// Load environment configuration
export function loadEnvironmentConfig() {
  // Default configuration
  const defaultConfig = {
    storage: {
      basePath: '/content',
      maxSize: 1024 * 1024 * 10, // 10MB
    },
    cache: {
      ttl: 3600, // 1 hour
      maxItems: 1000,
    },
  }

  // Environment-specific overrides
  const envOverrides = {
    // Development
    development: {
      storage: {
        basePath: '/tmp/dev-content',
      },
      cache: {
        ttl: 60, // 1 minute
      },
    },

    // Production
    production: {
      storage: {
        maxSize: 1024 * 1024 * 100, // 100MB
      },
    },

    // Test
    test: {
      storage: {
        basePath: '/tmp/test-content',
      },
      cache: {
        enabled: false,
      },
    },
  }

  // Get current environment
  const currentEnv = process?.env?.NODE_ENV || 'development'

  // Merge configurations
  return {
    ...defaultConfig,
    ...(envOverrides[currentEnv] || {}),
  }
}

Runtime Environment Detection

Adapt to runtime environment:

typescript
// Initialize system with runtime environment detection
export async function initializeContentSystem() {
  // Detect environment
  const environment = env.isNode
    ? 'node'
    : env.isBrowser
      ? 'browser'
      : 'unknown'

  console.log(`Initializing content system in ${environment} environment`)

  // Load configuration
  const config = loadEnvironmentConfig()

  // Initialize appropriate components
  let adapter

  switch (environment) {
    case 'node':
      adapter = createFileSystemAdapter({
        basePath: config.storage.basePath,
      })
      break

    case 'browser':
      if (env.hasIndexedDB) {
        adapter = createIndexedDBAdapter({
          databaseName: 'content-store',
          maxSize: config.storage.maxSize,
        })
      } else if (env.hasLocalStorage) {
        adapter = createLocalStorageAdapter({
          prefix: 'content:',
        })
      } else {
        adapter = createMemoryAdapter()
        console.warn('Using in-memory storage - content will not persist!')
      }
      break

    default:
      adapter = createMemoryAdapter()
      console.warn('Unknown environment - using in-memory storage')
  }

  // Create store with environment-appropriate components
  const store = createContentStore({
    adapter,
    middleware: createEnvironmentMiddleware(environment, config),
  })

  // Initialize environment-specific features
  if (environment === 'node') {
    await initializeNodeFeatures(store, config)
  } else if (environment === 'browser') {
    await initializeBrowserFeatures(store, config)
  }

  return store
}

Best Practices

  1. Centralize Detection: Keep environment detection in a central utility module
  2. Feature Detection: Use feature detection over environment detection when possible
  3. Graceful Degradation: Provide fallbacks when environment-specific features are unavailable
  4. Isomorphic Core: Keep core logic environment-agnostic
  5. Explicit Dependencies: Don’t rely on global objects without checks
  6. Conditional Imports: Use dynamic imports for environment-specific modules
  7. Testing: Test in all target environments with appropriate mocking
  8. Documentation: Document environment-specific behavior and requirements
  9. Error Handling: Provide helpful errors when required environment features are missing
  10. Configuration: Allow environment-specific configuration via environment variables or settings

Released under the MIT License.