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:
// 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:
// 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:
// Webpack detection
function isWebpack() {
return typeof __webpack_require__ !== 'undefined'
}
// Rollup detection
function isRollup() {
return typeof import.meta !== 'undefined'
}
Environment Modules
Environment detection is centralized:
// 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:
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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
{
"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:
// 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:
// 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:
// 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:
// 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:
// 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
- Centralize Detection: Keep environment detection in a central utility module
- Feature Detection: Use feature detection over environment detection when possible
- Graceful Degradation: Provide fallbacks when environment-specific features are unavailable
- Isomorphic Core: Keep core logic environment-agnostic
- Explicit Dependencies: Don’t rely on global objects without checks
- Conditional Imports: Use dynamic imports for environment-specific modules
- Testing: Test in all target environments with appropriate mocking
- Documentation: Document environment-specific behavior and requirements
- Error Handling: Provide helpful errors when required environment features are missing
- Configuration: Allow environment-specific configuration via environment variables or settings