BundleRoute

Serving for a directory of built front-end files: hashed assets, an entry document, and per-file-class caching.

BundleRoute is the RouteVariant.bundle member of the RouteBase family. It serves any directory of servable files, deriving cache headers from what kind of file each one is — immutable for build-hashed assets, revalidated for the entry document, and a configurable fallback for everything else. Its distinguishing behaviour is the entry-document fallback: a path that resolves to no file is answered with the entry HTML instead of a 404, which is what makes a single-page app survive a hard refresh on a client route. That fallback is the main use, not the only one — StaticRoute remains the plainer choice when a bundle's caching and fallback behaviour is not wanted.

new BundleRoute("/*", "./dist");
Contents
  1. BundleRoute

BundleRoute

class

class BundleRoute<E extends string = string>extends RouteBase<never, never, never, BundleRouteRes, E>

Serves a directory of built files.

Register it on a wildcard endpoint so every path below it reaches the route. A request resolves in three steps: the endpoint prefix is stripped to a sub-path by BundleRoute.resolveSubPath, the sub-path is joined onto BundleRoute.dir by BundleRoute.resolveTargetPath, and BundleRoute.resolveFile serves the result if it exists. When it does not and the request is not for an HTML file, the entry document named by BundleRoute.indexHtmlPath is served instead.

Files are matched against BundleRoute.definition to pick their Cache-Control: the entry document, anything under BundleRoute.assetsDirPath, and everything else each get their own policy.

Paths that escape BundleRoute.dir are rejected by BundleRoute.isTraversalAttempt before the filesystem is touched, so .. segments cannot reach files outside the directory.

Type parameters

BundleRoute.constructor()

constructor();
constructor(endpoint: E, dir: string, definition?: BundleRouteDefinition);
constructor(endpoint?: E, dir?: string, definition?: BundleRouteDefinition)

Creates a bundle route for subclasses, which declare BundleRoute.endpoint and BundleRoute.dir as class fields and call RouteBase.register themselves.

Creates a bundle route and registers it on the nearest App.

Parameters

BundleRoute.dir

dir!: string

Directory the files are served from. Every path resolved by the route is confined to it.

BundleRoute.ignore

ignore: Array<string>;

Sub-paths that are answered with the entry document rather than the file they name. A trailing * makes a pattern a prefix match; leading slashes are optional.

Useful when a client-side route collides with a real file in the directory, or to keep a build artefact from being reachable.

BundleRoute.definition

definition: BundleRouteDefinition;

The bundle's layout and caching policy. Defaults to DEFAULT_DEFINITION.

BundleRoute.indexHtmlPath

get indexHtmlPath(): string

Path of the entry document within BundleRoute.dir.

Returns — The path from BundleRouteDefinition.indexHtml, or the DEFAULT_DEFINITION value when it declares none.

BundleRoute.assetsDirPath

get assetsDirPath(): string

Path of the hashed-assets directory within BundleRoute.dir.

Returns — The path from BundleRouteDefinition.assetsDir, or the DEFAULT_DEFINITION value when it declares none.

BundleRoute.getEndpoints()

getEndpoints(): Array<string>

Lists every file this route can serve, as sub-paths relative to BundleRoute.dir.

Walks BundleRoute.dir recursively and returns each file's path with a leading /, matching what BundleRoute.resolveSubPath would produce for a request reaching that file. Useful for generating a sitemap or verifying what a deployed bundle actually contains.

Returns — The sub-paths of every file under BundleRoute.dir.

BundleRoute.onFileNotFound

onFileNotFound: (subPath: string) => MaybePromise<BundleRouteRes>;

Decides what to serve when a request resolves to no readable file — an HTML file that is genuinely missing, a directory with no entry document, or a path rejected by BundleRoute.isTraversalAttempt.

Replace it to serve a custom 404 page or redirect instead of throwing.

Parameters

Returns — The BundleRouteRes to send instead.

Throws — Exception with Status.NOT_FOUND by default.

BundleRoute.resolveFile()

protected resolveFile(targetPath: string): XFile | null

Resolves a filesystem path to a readable XFile, applying the entry-document fallback.

A missing non-HTML path falls back to the entry document, which is what serves client-side routes. A missing HTML path does not fall back — asking for a specific document that does not exist is a real 404 rather than a client route.

Parameters

Returns — The XFile to serve, or null when nothing readable was found, which sends the request to BundleRoute.onFileNotFound.

BundleRoute.resolveSubPath()

protected resolveSubPath(pathname: string): string

Strips the route's own prefix from a request pathname, leaving the path relative to BundleRoute.dir.

Both /* and * endpoint suffixes are handled, and a pathname that does not start with the prefix is returned untouched. The result is percent-decoded, since URL.pathname leaves escapes like %20 intact and filesystem paths need the literal characters.

Parameters

Returns — The bundle-relative sub-path, "" or / for the route root.

BundleRoute.resolveTargetPath()

protected resolveTargetPath(subPath: string): string

Joins a sub-path onto BundleRoute.dir to get the file to read.

The route root maps to the entry document, as does any sub-path matching BundleRoute.ignore. The result is not yet known to be safe — BundleRoute.isTraversalAttempt checks it before it is opened.

Parameters

Returns — The joined filesystem path.

BundleRoute.isTraversalAttempt()

protected isTraversalAttempt(targetPath: string): boolean

Reports whether a resolved path escapes BundleRoute.dir.

Both sides are fully resolved before comparison, so .. segments and encoded variants are normalised away rather than matched textually. The separator check keeps a sibling directory sharing the root's name prefix from passing.

Parameters

Returns — true when the path lies outside the served directory, in which case the request goes to BundleRoute.onFileNotFound without the file being opened.

BundleRoute.resolveResponseData()

protected resolveResponseData(file: XFile, ): Tuple<ReadableStream | Uint8Array, Record<string, string>>

Produces the response body and headers for a resolved file.

Cache-Control is chosen by matching the file against BundleRoute.definition: the entry document first, then anything under BundleRoute.assetsDirPath, then BundleRouteDefinition.fallback. No fallback means no header.

Non-HTML files are streamed and carry an inline Content-Disposition, so large assets are never buffered. HTML is read into memory instead, which lets it carry an exact Content-Length — the entry document is small and served constantly, so the length is worth more than the streaming.

Parameters

Returns — A Tuple of the body and the headers to set on Res.headers.

BundleRoute.variant

override readonly variant: RouteVariant

Marks this route as RouteVariant.bundle for App route compilation.

BundleRoute.method

override readonly method: Method

Bundles answer Method.GET only.

BundleRoute.endpoint

override endpoint!: E

The path the directory is served under. Use a wildcard so nested paths reach the route; BundleRoute.resolveSubPath strips the wildcard suffix.

BundleRoute.config

override readonly config?: RouteConfig<never, never, never, BundleRouteRes>

Bundles take no params, search or body, so no RouteConfig schemas apply.

BundleRoute.handler

override handler: ContextHandler<never, never, never, BundleRouteRes>

Serves the file a request resolves to.

Resolves the sub-path and target path, rejects traversal attempts, reads the file, then sets the headers from BundleRoute.resolveResponseData on Context.res and returns the body. Anything that fails to resolve goes to BundleRoute.onFileNotFound.

Parameters

Returns — The file body — a stream for non-HTML, bytes for HTML — or whatever BundleRoute.onFileNotFound produced.