@hyperfrontend/features/server

Server

Dev server and debug UI for testing host/hostee interactions — per-app static hosting plus display-mode, resize, message-log, and security controls — and the production static server behind hf serve.

Quick start

Start a dev server from a resolved hf-dev.config.*:

import { resolveDevConfig, startDevServer } from '@hyperfrontend/features/server'

const config = await resolveDevConfig({ cwd: process.cwd(), flags })
const handle = await startDevServer(config)

console.log(handle.debugUrl) // http://localhost:4280/
handle.apps.forEach((app) => console.log(app.name, app.url))

await handle.close()

Or serve a built site for production:

hf serve --root dist/site --port 8080

How it serves

A URL ending in / serves that directory's index.html, so a multi-page build's index.html and host/index.html load at / and /host/; the unslashed /host answers 301 to /host/.

Each configured app is served by its own static server bound to its port, so apps load at distinct origins, letting the host/hostee message channel and security envelope be exercised cross-origin, exactly as in production. The debug UI is hosted at / on a separate control server (default port 4280, movable with the config's debug.port or the --port flag, which wins), which also exposes the running-app manifest at /__apps and the compiled debug-UI assets under /__debug/. Resolution fails loudly when an app's port equals the enabled debug UI's port, naming both.

Production serving

hf serve serves one directory over one port, and its production behavior is fixed: text responses are brotli- or gzip-compressed per the client's Accept-Encoding (compressed bytes cached per file), every file carries a weak ETag and a matching If-None-Match answers 304, directory URLs resolve to index.html with the 301 redirect described above, dotfiles and the hf-serve.config.* file answer 404, only GET/HEAD are answered (anything else gets 405), a failure inside the pipeline answers 500 instead of crashing the server, each request writes one access-log line, and SIGINT/SIGTERM close the server gracefully.

What varies is data, declared in hf-serve.config.*: the served root, the listen port and host, the log switch, and ordered headers rules; each rule matches by optional path prefix/suffix, and later rules override earlier ones one header at a time:

{
  "root": "dist/site",
  "port": 8080,
  "headers": [{ "suffix": ".html", "headers": { "Cache-Control": "no-cache" } }]
}

No config is required at all: with none found, the working directory is served on port 4284 on every interface. The file is selected by --config, else (when --root names a directory carrying one) the artifact's own <root>/hf-serve.config.json, else discovered in the working directory. A platform-assigned PORT environment variable overrides the config's port, and the --root/--port/--host flags override everything, so on a host that injects PORT, hf serve --root <dir> needs no port flag at all.

Deliberately absent: SPA rewrites, extensionless .html rewriting, directory listings, Range requests, and CORS. Symlinked paths answer 404, and each response is read whole into memory (feature artifacts are small; there is no streaming path). The escape hatch is code, not config: custom steps prepend to the built-in pipeline (method guard → compression → header rules → file serving; rules sit inside compression so a rule-set Cache-Control: no-transform or Content-Type shapes what the compressor sees), so a step sees every request first and every response last, and either answers itself or transforms what next() returns:

import type { ServeStep } from '@hyperfrontend/features/server'
import { resolveServeConfig, startStaticServer } from '@hyperfrontend/features/server'

const cors: ServeStep = (_request, _context, next) => {
  const response = next()
  return { ...response, headers: { ...response.headers, 'Access-Control-Allow-Origin': '*' } }
}

const config = await resolveServeConfig({ cwd: process.cwd(), flags })
const handle = await startStaticServer(config, { steps: [cors] })

Steps are deliberately not expressible in the config file: config files stay data.

API

ExportPurpose
resolveDevConfigResolve hf-dev.config.* + CLI flags into concrete app servers.
startDevServerStart the app servers and the debug-UI control server.
validateDevConfig / validateApps / validateDevAppRuntime validation of the config shape.
createStaticHandler / serveFileStatic-file request handling used by the app servers.
resolveServeConfigResolve hf-serve.config.* + CLI flags into a concrete serving plan.
startStaticServer / createServeListenerStart the production static server, or host its request listener elsewhere.
buildServeSteps / buildCompressionStep / runStepsThe built-in ServeStep pipeline and the runner custom steps compose with.
validateServeConfig / validateHeaderRuleRuntime validation of the serve-config shape.

The config schemas (apps/debug and root/port/host/headers/log) and the defineDevConfig() / defineServeConfig() authoring helpers live in the main @hyperfrontend/features entry.

API Reference

ƒ Functions

§function

buildCompressionStep(): ServeStep

Builds the compression step: 200-status text responses at or above the threshold are brotli- or gzip-encoded per the client's Accept-Encoding, with compressed bytes cached per file so a static deployment compresses each asset once per encoding.
HEAD requests skip compression so their advertised Content-Length stays the identity size, and a Cache-Control: no-transform set by a header rule is honored. Bodiless answers for compressible resources (HEAD, 304) still carry Vary: Accept-Encoding so caches keep encodings apart.

Returns

ServeStep
A step that encodes eligible response bodies.

Example

Composing the step into a custom pipeline

const steps = [buildCompressionStep(), terminalStep]
§function

buildServeSteps(config: ResolvedServeConfig, deps: ServeStepDeps): ServeStep[]

Builds the built-in serve pipeline, outermost step first: method guard, compression, header rules, then the terminal file-serving step.
Header rules sit inside compression so a rule's headers shape what the compressor sees: a rule-set Cache-Control: no-transform suppresses encoding and a rule-set Content-Type decides compressibility. Custom steps prepend to this chain, so a plugin sees every request first and every response last.

Parameters

NameTypeDescription
§config
ResolvedServeConfig
The resolved serving plan.
§deps
ServeStepDeps
Injectable file-system boundaries.
(default: {})

Returns

ServeStep[]
The ordered built-in steps.

Example

Assembling the default pipeline

const steps = buildServeSteps(resolved, {})
§function

confineDecodedPath(root: string, decoded: string): string

Confines an already-decoded request path to an absolute path under root, rejecting any path that escapes the root via ...

Parameters

NameTypeDescription
§root
string
The absolute directory the path is confined to.
§decoded
string
The percent-decoded request path, query string stripped.

Returns

string
The confined absolute path, or null when the path escapes root.

Example

Rejecting a traversal attempt

confineDecodedPath('/abs/dist', '/../secret') // null
§function

contentTypeFor(filePath: string): string

Maps a file path to its Content-Type, falling back to octet-stream.

Parameters

NameTypeDescription
§filePath
string
The path whose extension selects the MIME type.

Returns

string
The matching content type.

Example

Looking up a stylesheet's type

contentTypeFor('/abs/dist/app.css') // 'text/css; charset=utf-8'
§function

createServeListener(config: ResolvedServeConfig, deps: StaticServeDeps): (req: IncomingMessage, res: ServerResponse) => void

Builds the request listener that runs every request through the serve pipeline: any custom steps first, then the built-ins (method guard, compression, header rules, file serving). A step that throws answers 500 rather than crashing the server.
This is the replacement seam: anything that can call this listener — Node's http.createServer, a test harness, or another runtime adapter — can host the pipeline unchanged.

Parameters

NameTypeDescription
§config
ResolvedServeConfig
The resolved serving plan.
§deps
StaticServeDeps
Optional file-system, logging, and pipeline overrides.
(default: {})

Returns

(req: IncomingMessage, res: ServerResponse) => void
A request handler suitable for http.createServer.

Example

Hosting the pipeline on a hand-made server

const server = createServer(createServeListener(resolved))
§function

createStaticHandler(root: string, deps: StaticHandlerDeps): (req: IncomingMessage, res: ServerResponse) => void

Builds an HTTP request handler that serves static files from a single root.

Parameters

NameTypeDescription
§root
string
The absolute directory files are served from.
§deps
StaticHandlerDeps
Optional file-system overrides.
(default: {})

Returns

(req: IncomingMessage, res: ServerResponse) => void
A request handler suitable for http.createServer.

Example

Serving a compiled app directory

const server = createServer(createStaticHandler('/abs/dist'))
§function

decodeRequestPath(path: string): string

Percent-decodes a request path, treating malformed encodings as unservable rather than letting decodeURIComponent throw inside a request handler.

Parameters

NameTypeDescription
§path
string
The request path with the query string already stripped.

Returns

string
The decoded path, or null when the encoding is malformed.

Example

Rejecting a malformed encoding

decodeRequestPath('/%') // null
§function

directoryLocation(root: string, confined: string, url: string): string

Builds the slashed directory URL to redirect an unslashed request to.
The location is derived from the resolved path rather than the raw request, so it is always a single-slash path relative to this server's own root: a request that writes an authority into the URL (//example.com/../host) is answered with the directory it actually resolved to, never with a location pointing off this origin.

Parameters

NameTypeDescription
§root
string
The absolute directory files are served from.
§confined
string
The resolved absolute path, already confined to root.
§url
string
The raw request URL, read only for its query string.

Returns

string
The root-relative directory URL, ending in a slash.

Example

Redirecting a directory request

directoryLocation('/srv', '/srv/host', '/host?debug=1') // '/host/?debug=1'
§function

headerValue(headers: Readonly<Record<string, string>>, name: string): string

Reads a response header by case-insensitive name.

Parameters

NameTypeDescription
§headers
Readonly<Record<string, string>>
The response headers.
§name
string
The header name to read, in any case.

Returns

string
The header value, or undefined when absent.

Example

Reading a content type set in any case

headerValue({ 'content-type': 'text/html' }, 'Content-Type') // 'text/html'
§function

negotiateEncoding(header: string): "br" | "gzip"

Parses an Accept-Encoding header into the preferred supported encoding.
Preference is brotli, then gzip: the order every mainstream browser also ranks them in. A * token stands in for both. Encodings disabled with q=0 are never chosen.

Parameters

NameTypeDescription
§header
string
The raw Accept-Encoding value.

Returns

"br" | "gzip"
'br', 'gzip', or null when neither is acceptable.

Example

Negotiating a browser's default header

negotiateEncoding('gzip, deflate, br') // 'br'
§function

plainResponse(status: number, text: string): StaticResponse

Builds a plain-text response with the conventional minimal error shape.

Parameters

NameTypeDescription
§status
number
The HTTP status code.
§text
string
The body text.

Returns

StaticResponse
The response value.

Example

Building a 404

plainResponse(404, 'Not Found')
§function

requestPath(url: string): string

Extracts the path portion of a request URL, defaulting a missing URL to / and dropping any query string.

Parameters

NameTypeDescription
§url
string
The raw request URL (req.url), which may be undefined.

Returns

string
The path with no query string.

Example

Stripping a query string

requestPath('/app.js?v=2') // '/app.js'
§function

resolveDevConfig(options: ResolveDevConfigOptions): Promise<ResolvedDevConfig>

Resolves the effective hf-dev.config.* into concrete app servers and debug settings, applying config file < flags precedence: --apps replaces the apps array, --port overrides the config's debug.port for the debug UI, and --config selects the file.

Parameters

NameTypeDescription
§options
ResolveDevConfigOptions
The working directory, parsed flags, and injectable deps.

Returns

Promise<ResolvedDevConfig>
The resolved apps, debug toggles, debug port, and source path.

Example

Resolving a discovered dev config

const resolved = await resolveDevConfig({ cwd: process.cwd(), flags })
§function

resolveServeConfig(options: ResolveServeConfigOptions): Promise<ResolvedServeConfig>

Resolves the effective hf-serve.config.* into a concrete serving plan, applying defaults < config file < PORT environment variable < flags precedence: --root sets the served directory, --port/--host the listen address, and --config selects the file. A platform-assigned PORT beats a port baked into the served artifact's config, and an explicit --port still beats both, so hf serve --root <dir> works without a port flag wherever the platform injects one. Unlike the dev server, serving is valid with no config at all: the working directory is served with defaults.

Parameters

NameTypeDescription
§options
ResolveServeConfigOptions
The working directory, parsed flags, and injectable deps.

Returns

Promise<ResolvedServeConfig>
The resolved root, listen address, header rules, and source path.

Example

Resolving a static-serve config

const resolved = await resolveServeConfig({ cwd: process.cwd(), flags })
§function

runSteps(steps: unknown, request: StaticRequest, context: ServeStepContext): StaticResponse

Runs a request through the pipeline, each step delegating inward via next(). A pipeline whose steps all delegate past the end answers 404, so a custom chain without a terminal step still returns a response.

Parameters

NameTypeDescription
§steps
unknown
The ordered steps, outermost first.
§request
StaticRequest
The request to answer.
§context
ServeStepContext
The shared per-request context.

Returns

StaticResponse
The response the outermost step settled on.

Example

Running a request through a custom step and the built-ins

const response = runSteps([...customSteps, ...builtInSteps], request, { config })
§function

serveFile(root: string, urlPath: string, res: ServerResponse, deps: StaticHandlerDeps): void

Serves a single static file from root to the response, sending 403 on a traversal attempt and 404 when the file is missing.
A URL ending in / serves that directory's index.html, so / and /host/ reach the pages a multi-page build emits as index.html and host/index.html. A directory URL written without the trailing slash (/host) answers 301 to the slashed form, keeping relative asset URLs on the page resolvable.

Parameters

NameTypeDescription
§root
string
The absolute directory files are served from.
§urlPath
string
The request path (with or without a query string).
§res
ServerResponse
The HTTP response to write.
§deps
StaticHandlerDeps
Optional file-system overrides.
(default: {})

Examples

Serving an app's `index.html`

serveFile('/abs/dist', '/', res)

Serving a companion page from a multi-page build

serveFile('/abs/dist', '/host/', res) // sends /abs/dist/host/index.html
§function

startDevServer(config: ResolvedDevConfig, deps: DevServerDeps): Promise<DevServerHandle>

Starts the dev server: one static server per app (each on its own port for a distinct origin) plus, when enabled, the control server hosting the debug UI.

Parameters

NameTypeDescription
§config
ResolvedDevConfig
The resolved dev-server config.
§deps
DevServerDeps
Optional server-creation, asset-location, and file-system overrides.
(default: {})

Returns

Promise<DevServerHandle>
A handle exposing the running apps, the manifest, the debug URL, and a teardown.

Example

Starting a dev server from a resolved config

const handle = await startDevServer(resolved)
console.log(handle.debugUrl)
await handle.close()
§function

startStaticServer(config: ResolvedServeConfig, deps: StaticServeDeps): Promise<StaticServerHandle>

Starts the production static server on the resolved address and serves the root until closed.

Parameters

NameTypeDescription
§config
ResolvedServeConfig
The resolved serving plan.
§deps
StaticServeDeps
Optional server-creation, file-system, logging, and pipeline overrides.
(default: {})

Returns

Promise<StaticServerHandle>
A handle exposing the bound address and a teardown.

Example

Serving a built site

const handle = await startStaticServer(resolved)
console.log(handle.url)
await handle.close()
§function

validateApps(value: unknown, sourcePath: string): DevAppConfig[]

Validates an unknown value as a dev-server apps array.

Parameters

NameTypeDescription
§value
unknown
The candidate apps array.
§sourcePath
string
The file the array came from, used in error messages.

Returns

DevAppConfig[]
The validated, non-empty apps array.

Example

Validating an apps array loaded from `--apps`

const apps = validateApps([{ name: 'clock', outputDir: './dist' }], '/p/apps.json')
§function

validateDevApp(value: unknown, index: number, sourcePath: string): DevAppConfig

Validates one app entry, asserting name/outputDir strings and an optional numeric port.

Parameters

NameTypeDescription
§value
unknown
The candidate app entry.
§index
number
The entry's index, used to locate problems in the error message.
§sourcePath
string
The file the entry came from, used in the error message.

Returns

DevAppConfig
The validated app entry.

Example

Validating a single app entry

const app = validateDevApp({ name: 'clock', outputDir: './dist' }, 0, '/p/hf-dev.config.json')
§function

validateDevConfig(value: unknown, sourcePath: string): DevConfig

Validates an unknown value as a DevConfig.

Parameters

NameTypeDescription
§value
unknown
The loaded config value.
§sourcePath
string
The config path, used in error messages.

Returns

DevConfig
The validated config.

Example

Validating a loaded dev config

const config = validateDevConfig({ apps: [{ name: 'clock', outputDir: './dist' }] }, '/p/hf-dev.config.json')
§function

validateHeaderRule(value: unknown, index: number, sourcePath: string): ServeHeaderRule

Validates one header rule, asserting optional prefix/suffix strings and a string-valued headers record.

Parameters

NameTypeDescription
§value
unknown
The candidate header rule.
§index
number
The rule's index, used to locate problems in the error message.
§sourcePath
string
The file the rule came from, used in the error message.

Returns

ServeHeaderRule
The validated header rule.

Example

Validating a single header rule

const rule = validateHeaderRule({ suffix: '.html', headers: { 'Cache-Control': 'no-cache' } }, 0, '/p/hf-serve.config.json')
§function

validateServeConfig(value: unknown, sourcePath: string): ServeConfig

Validates an unknown value as a ServeConfig.

Parameters

NameTypeDescription
§value
unknown
The loaded config value.
§sourcePath
string
The config path, used in error messages.

Returns

ServeConfig
The validated config.

Example

Validating a loaded serve config

const config = validateServeConfig({ root: 'dist/site' }, '/p/hf-serve.config.json')

Interfaces

§interface

DevManifest

The manifest the debug UI reads to discover the running apps and its own toggles.

Properties

§readonly apps:unknown
Each running app's name and origin URL.
§readonly debug:ResolvedDevDebug
The resolved debug-UI toggles.
§interface

DevManifestApp

A running app as advertised to the debug UI.

Properties

§readonly name:string
App name, matched against the feature name.
§readonly url:string
The origin URL the app is served from.
§interface

DevServerApp

A single running app static server.

Properties

§readonly name:string
App name, matched against the feature name.
§readonly port:number
The port the app is actually listening on.
§readonly url:string
The origin URL the app is served from.
§interface

DevServerDeps

Injectable boundaries for startDevServer, defaulted for production.

Properties

§readonly assetRoot?:string
Directory the compiled debug-UI assets are read from; defaults to the assets shipped beside this module.
§readonly createServer?:(handler: (req: IncomingMessage, res: ServerResponse) => void) => Server
Creates an HTTP server from a request handler.
§readonly isFile?:(filePath: string) => boolean
Reports whether a path is a readable file.
§readonly readFile?:(filePath: string) => Buffer
Reads a file's bytes.
§interface

DevServerHandle

A running dev server: the app servers, the debug UI, and a teardown.

Properties

§readonly apps:unknown
The running app static servers.
§readonly debugUrl?:string
The debug-UI URL, present only when the debug UI is enabled.
§readonly manifest:DevManifest
The manifest exposed to the debug UI.
§interface

ResolvedDevApp

A single dev-server app with its serving directory and port resolved to concrete values.

Properties

§readonly name:string
App name, matched against the feature name.
§readonly outputDir:string
Absolute directory the built app is served from.
§readonly port:number
Port the app's static server listens on.
§interface

ResolvedDevConfig

A fully-resolved hf-dev.config.*: concrete app servers plus debug settings.

Properties

§readonly apps:unknown
The app static servers to start.
§readonly debug:ResolvedDevDebug
The resolved debug-UI toggles.
§readonly debugPort:number
Port the debug-UI control server listens on.
§readonly sourcePath:string
Absolute path of the config file that was loaded.
§interface

ResolvedDevDebug

Fully-resolved debug-UI toggles with every option defaulted.

Properties

§readonly enabled:boolean
Whether the debug UI is served at all.
§readonly messageLog:boolean
Whether the message-log panel is shown.
§readonly securityView:boolean
Whether the security-inspector panel is shown.
§interface

ResolveDevConfigDeps

Injectable boundaries for resolveDevConfig, defaulted for production.

Properties

§readonly discover?:(directory: string, baseName: string) => string
Discovers the dev-server config file under a directory.
§readonly loadConfig?:(absolutePath: string) => Promise<unknown>
Loads a resolved config or apps file.
§interface

ResolveDevConfigOptions

Inputs for resolveDevConfig.

Properties

§readonly cwd:string
Working directory the config and apps paths resolve against.
§readonly discover?:(directory: string, baseName: string) => string
Discovers the dev-server config file under a directory.
§readonly flags:CliFlags
Parsed CLI flags, applied with the highest precedence.
§readonly loadConfig?:(absolutePath: string) => Promise<unknown>
Loads a resolved config or apps file.
§interface

ResolvedServeConfig

A fully-resolved hf-serve.config.*: concrete root, listen address, and header rules.

Properties

§readonly headers:unknown
Ordered header rules, later rules overriding earlier ones per header.
§readonly host?:string
Interface the server binds; every interface when undefined.
§readonly log:boolean
Whether each request is access-logged.
§readonly port:number
Port the server listens on.
§readonly root:string
Absolute directory served as the site root.
§readonly sourcePath?:string
Absolute path of the config file that was loaded, absent when serving with pure defaults.
§interface

ResolveServeConfigDeps

Injectable boundaries for resolveServeConfig, defaulted for production.

Properties

§readonly discover?:(directory: string, baseName: string) => string
Discovers the static-server config file under a directory.
§readonly env?:Record<string, string | undefined>
Environment variables consulted for the PORT fallback.
§readonly exists?:(path: string) => boolean
Reports whether a path exists.
§readonly loadConfig?:(absolutePath: string) => Promise<unknown>
Loads a config file.
§interface

ResolveServeConfigOptions

Inputs for resolveServeConfig.

Properties

§readonly cwd:string
Working directory the config and root paths resolve against.
§readonly discover?:(directory: string, baseName: string) => string
Discovers the static-server config file under a directory.
§readonly env?:Record<string, string | undefined>
Environment variables consulted for the PORT fallback.
§readonly exists?:(path: string) => boolean
Reports whether a path exists.
§readonly flags:CliFlags
Parsed CLI flags, applied with the highest precedence.
§readonly loadConfig?:(absolutePath: string) => Promise<unknown>
Loads a config file.
§interface

ServeStepContext

Per-request state the steps share while a request runs through the pipeline.

Properties

§readonly config:ResolvedServeConfig
The resolved serving plan.
§filePath?:string
Absolute path of the file the terminal step resolved, recorded for path-matched header rules.
§interface

ServeStepDeps

Injectable file-system boundaries for the built-in serve steps, defaulted for production.

Properties

§readonly isFile?:(filePath: string) => boolean
Reports whether a path is a readable file.
§readonly readFile?:(filePath: string) => Buffer
Reads a file's bytes.
§readonly stat?:(filePath: string) => FileStats
Reads a file's stats, or null when the path is unreadable.
§interface

StaticHandlerDeps

Injectable file-system boundaries for the static handler, defaulted for production.

Properties

§readonly isFile?:(filePath: string) => boolean
Reports whether a path is a readable file.
§readonly readFile?:(filePath: string) => Buffer
Reads a file's bytes.
§interface

StaticRequest

The immutable view of one incoming request the serve pipeline reads.

Properties

§readonly headers:IncomingHttpHeaders
The raw request headers.
§readonly method:string
The HTTP method, uppercase.
§readonly url:string
The raw request URL, path plus query string.
§interface

StaticResponse

The value a serve step returns: a complete response, ready to write.

Properties

§readonly body:Buffer<ArrayBufferLike>
The response body, or null for a bodiless response.
§readonly headers:Readonly<Record<string, string>>
Response headers by exact name.
§readonly status:number
The HTTP status code.
§interface

StaticServeDeps

Injectable boundaries for startStaticServer, defaulted for production.

Properties

§readonly createServer?:(handler: (req: IncomingMessage, res: ServerResponse) => void) => Server
Creates an HTTP server from a request handler.
§readonly isFile?:(filePath: string) => boolean
Reports whether a path is a readable file.
§readonly log?:(line: string) => void
Sink for one access-log line per request.
§readonly readFile?:(filePath: string) => Buffer
Reads a file's bytes.
§readonly stat?:(filePath: string) => FileStats
Reads a file's stats, or null when the path is unreadable.
§readonly steps?:unknown
Custom steps prepended to the built-in pipeline; the first step sees every request first.
§interface

StaticServerHandle

A running static server: its address and a teardown.

Properties

§readonly port:number
The port the server is actually listening on.
§readonly url:string
An origin URL the site is reachable at: the bound interface, or localhost for a wildcard bind.

Types

§type

ServeStep

One link in the serve pipeline. A step may answer the request itself or delegate to the rest of the pipeline with next() and transform what comes back: the innermost step serves the file, so next() always yields a complete response.
type ServeStep = (request: StaticRequest, context: ServeStepContext, next: () => StaticResponse) => StaticResponse