OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-web-ui / lib / static.js

87 lines · javascript ref:
/**
 * Static File Server + SPA Fallback
 *
 * Serves files from a web/ directory. Falls back to index.html
 * for SPA routing (any path not matching a real file → index.html).
 */

import fs from 'bare-fs'
import path from 'bare-path'

const MIME_TYPES = {
  '.html': 'text/html',
  '.css': 'text/css',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon',
  '.woff2': 'font/woff2',
  '.woff': 'font/woff'
}

/**
 * Serve static files from a directory
 *
 * @param {string} webDir — root directory for static files
 * @param {Object} opts
 * @param {string} opts.prefix — URL prefix (e.g., '/web')
 * @param {boolean} opts.spaFallback — serve index.html for unmatched paths (default: true)
 * @returns {Function} handler(req, res) → boolean (true if handled)
 */
export function serveStatic (webDir, opts = {}) {
  const prefix = opts.prefix || '/web'
  const spaFallback = opts.spaFallback !== false
  const indexPath = path.join(webDir, 'index.html')

  return function handleStatic (req, res) {
    if (req.method !== 'GET') return false

    const urlPath = req.url.split('?')[0]

    // Serve files under prefix
    if (urlPath === prefix || urlPath === prefix + '/') {
      // Prefix root → serve index.html
      try {
        const data = fs.readFileSync(indexPath)
        res.writeHead(200, { 'Content-Type': 'text/html', 'Content-Length': data.length })
        res.end(data)
        return true
      } catch { return false }
    }

    if (urlPath.startsWith(prefix + '/')) {
      const relPath = urlPath.slice(prefix.length)
      const filePath = path.join(webDir, relPath)

      try {
        const data = fs.readFileSync(filePath)
        const ext = path.extname(filePath)
        res.writeHead(200, {
          'Content-Type': MIME_TYPES[ext] || 'application/octet-stream',
          'Content-Length': data.length
        })
        res.end(data)
        return true
      } catch {
        return false
      }
    }

    // SPA fallback — serve index.html for any non-API, non-static path
    if (spaFallback && !urlPath.startsWith('/api/') && !urlPath.includes('.')) {
      try {
        const data = fs.readFileSync(indexPath)
        res.writeHead(200, { 'Content-Type': 'text/html' })
        res.end(data)
        return true
      } catch {
        return false
      }
    }

    return false
  }
}