Family
Three family packages produce or consume static file trees. None depends on another at runtime; they meet at file formats.
| You want to… | Use |
|---|---|
| Build a site from pinned sources through a reproducible pipeline (stages, templates, plugins, checks, exports) | sitefile |
| Generate a tree of files from code, in JSX: scaffolding, codegen, append-only logs | fileable |
Serve a built site from memory as a (Request) => Response handler, or pack a directory into one file |
packfile |
What works today
Section titled “What works today”Checked on 2026-11-07 against the npm releases @johnhenry/sitefile 0.0.0, @johnhenry/fileable 0.1.1 and
@johnhenry/packfile 0.2.0, and against each repository’s main, by building with both tools or by a test. “Fixed on
main, in the next release” means the change is merged but not published yet: the npm versions above still behave as
the rest of the row says.
| From | To | How | Status |
|---|---|---|---|
| fileable, loose output | Sitefile | COPY ./generated/ / |
Works |
fileable, encode="zip" |
Sitefile | IMPORT gen FROM ./site.zip AS tree, then COPY gen:/ / |
Works, same output as loose |
fileable, encode="wbn" |
Sitefile | IMPORT … AS tree |
Not yet (sitefile#141) |
| fileable tree, rendered during a build | Sitefile | a JavaScript TRANSFORM plugin calling fileable’s in-memory toFiles() |
An example in Sitefile’s repository (examples/fileable/, on main; not part of the npm package). fileable is vendored into the plugin folder, and cmd, local src= and env:// are refused in the sandbox |
fileable, encode="wbn" |
packfile | fromArchive(), createRouter() |
Works, with the file-name limits below; tested in fileable |
Sitefile, EXPORT / AS wbn BASE <url> |
packfile | fromWebBundle(buffer, { baseURL }), then createRouter() |
Works; tested in both repositories (packfile reads a bundle written by the published 0.0.0) |
Sitefile .wbn |
packfile fromArchive() |
with 0.0.0 / 0.2.0: gzip it, and export with BASE https://packfile.invalid/ |
Works with that workaround. Fixed on main, in the next release: EXPORT … AS wbn COMPRESSION gzip (or -o site.wbn.gz) writes the gzip (sitefile#143), and fromArchive() takes its base from a baseURL option or the bundle’s primary URL, so any BASE reads back (packfile#18) |
Sitefile HEADER / REDIRECT |
packfile createWebBundleRouter() |
— | Not applied in 0.0.0: only the /_headers and /_redirects files are in the bundle. Fixed on main, in the next release: each response carries its HEADER values, and exact-path 301/302/303/307/308 REDIRECTs are 3xx responses with location (sitefile#144). Splats, 200 rewrites and 404 rules stay in /_redirects only |
packfile toArchive() |
Sitefile | IMPORT … AS tree |
Not yet (sitefile#141) |
fileable → Sitefile
Section titled “fileable → Sitefile”COPY ./generated/ /IMPORT gen FROM ./generated/site.zip AS treeCOPY gen:/ /fileable’s encode="wbn" writes gzip around a Web Bundle, and IMPORT … AS tree reads only plain bundles, so it fails
with E_TYPE (“gzip content is not a tar archive”). Use the directory or zip form until sitefile#141 lands.
To render a fileable tree inside a build instead, without a directory or zip in between, see the plugin example in
Sitefile’s repository, examples/fileable/ (on main; it is an example, not part of the npm package). fileable’s cmd
attribute, local src= paths and env:// fail there with E_PLUGIN, since a plugin cannot start processes, read
outside its folder or see the environment.
Sitefile → packfile
Section titled “Sitefile → packfile”EXPORT / AS wbn BASE https://site.example/ writes a plain b2 Web Bundle. packfile reads it back into exactly the
exported files and serves it:
import { readFile } from 'node:fs/promises';import { createRouter } from '@johnhenry/packfile';import { fromWebBundle } from '@johnhenry/packfile/web-bundle';
const files = fromWebBundle(await readFile('site.wbn'), { baseURL: 'https://site.example/' });const handler = createRouter(files); // (Request | path) => Promise<Response>- The
baseURLmust be theBASEyou exported with. Entries under any other origin are skipped, so the wrong base reads back as an empty map, without an error. - Pass a path string for non-ASCII paths.
handler(new Request('https://site.example/docs/ünï/page.html'))returns 404 because the pathname isn’t percent-decoded;handler('/docs/ünï/page.html')works (packfile#20). - The bundle’s entry at the bare base URL is skipped. Sitefile stores the index page there as well as at
/index.html; read it asindex.html. HEADERandREDIRECTrules aren’t applied by 0.0.0. They’re in the bundle as/_headersand/_redirectsfiles, which packfile serves as ordinary files. Fixed on main, in the next release (sitefile#144): the bundle’s responses carry the HEADER values and exact-path REDIRECTs, whichcreateWebBundleRouter(new wbn.Bundle(bytes), { baseURL })serves.createRouter()serves a files map, so it still answers with the files only. Splat rules,200rewrites and404rules are only in/_redirects, for a host that reads it.
packfile’s own archive format (toArchive() / fromArchive()) is gzip around the same kind of bundle, under the fixed
base URL https://packfile.invalid/. With the published versions, a Sitefile bundle exported with that BASE is read
by fromArchive(bundle, { compressed: false }) as is, or by plain fromArchive() once gzipped; a bundle with any
other BASE reads back as an empty map. Fixed on main, in the next release: Sitefile writes the gzip itself
(EXPORT / AS wbn BASE <url> COMPRESSION gzip, or -o site.wbn.gz), and fromArchive() accepts { baseURL } or,
without it, uses the origin of the bundle’s primary URL. Sitefile writes a primary URL only when the site has
/index.html, and the derived base is the origin (https://site.example/), so a BASE with a path such as
https://site.example/blog/ gives keys like blog/index.html unless you pass that baseURL.
Sitefile’s test/packfile-interop.test.ts covers the files and bytes, a served page with its MIME type, a 404, and the
gzip relationship (on main, also the gzip export and the baked-in headers and redirects); packfile is a development
dependency of Sitefile for that test only. packfile’s test/sitefile-fixture.test.mjs reads a bundle written by the
published Sitefile 0.0.0.
fileable → packfile
Section titled “fileable → packfile”fileable’s encode="wbn" output is packfile’s archive format, so fromArchive() and createRouter() read it directly.
Limits:
- File names containing
#make thewbnrender throw, and names with?or%are dropped when packfile reads the archive (fileable#20). - An empty
<Dir>isn’t kept: a Web Bundle has no directory entries. createRouter()works outContent-Typefrom its own MIME table rather than the one fileable wrote; they agree on the common extensions.
For fileable trees served without writing anything to disk, see servable’s mount.