Output layout¶
ocx-catalog build writes a rendered site plus every configured source's
mirrored wire data into one --out directory, in a fixed pipeline order.
This page documents that order and the resulting tree shape from
src/build/engine.ts, src/sources/mirror.ts, and src/viewmodel/.
Build pipeline order¶
- Load config —
loadConfig(configPath)parses and validatescatalog.config.json. Any shape error surfaces here, before anything is written to disk. - Resolve sources —
resolveCatalog(loaded.sources, loaded.configDir)(src/build/sources_pipeline.ts) reads every configured source, resolves its label, and merges their packages into routes, a per-package description lookup, and the mergedcatalog.jsonbytes. Runs before the scratch root exists, so a source failure leaves nothing behind and nothing partial ever reaches--out. - Create the scratch root — a self-sweeping build directory under the
consumer's own
node_modules/.cache/ocx-catalog/(or.ocx-catalog/if nonode_modulesexists yet). - Synthesize pages — one page per resolved package, plus the always-on
index.md/404.md, the docs mount (whendocsis set), and the public-assets mount (whenpublicDiris set). - Generate config — writes the scratch root's
.vitepress/configand theme shim:brand/navverbatim,brand.logo/cssresolved against the config directory,siteUrl/description/faviconverbatim, and the description lookup baked in as static JSON. - VitePress build —
assertOutDirSafe(scratchRoot, outDir)runs first (refusing an--outequal to, an ancestor of, or a descendant of the scratch root), thenvitepress build(scratchRoot, { outDir })renders the static site into the caller's own output directory. - Emit the catalog tree —
emitCatalogTree()writes every source's wire tree,_headers, and the mergeddata/catalog/catalog.jsoninto--out. - Dispose — the scratch root is cleaned up in a
finally, on both success and failure.
Step 7 runs after the VitePress build, deliberately never before or
interleaved with it: --out is VitePress's own output directory, so
anything written there earlier is at the mercy of the build's own
emptyOutDir/publicDir handling. Writing the mirror, _headers, and
catalog data only after the build guarantees they are the final,
uncontested content at those paths — including a deliberate last-write-wins
overwrite of data/catalog/catalog.json, described below.
Annotated dist/ tree¶
dist/
├── index.html # VitePress-rendered pages: package grid, per-package detail pages, ...
├── <namespace>/<package>/… # one route per resolved package from the root: true source, depth-N to match its namespace + package segments
├── <label>/<namespace>/<package>/… # same, for a non-root source — its route is qualified with its own label
├── assets/ # VitePress's own built JS/CSS bundle
├── p/ # present only when some sources[] entry sets root: true — that source's own wire tree
│ └── <namespace>/<package>.json
│ └── o/sha256/<hex>.<ext> # CAS blobs: desc.readme (.md), desc.logo (.svg/.png), OCI image indices (.json)
├── config.json # the root source's own wire config.json, if it published one
├── c/
│ └── index.json # the root source's own sparse-index enumeration, if it published one
├── index/
│ └── <label>/ # one such directory per configured source, INCLUDING the root: true one
│ ├── p/** # that source's own wire tree, byte-identical copy
│ ├── config.json # if that source published one
│ ├── c/
│ │ └── index.json # if that source published one
│ └── data/
│ └── catalog/
│ └── catalog.json # this source's OWN single-source catalog — non-root sources only, see below
├── data/
│ └── catalog/
│ └── catalog.json # the MERGED, multi-source catalog — the last write to this path, see below
└── _headers # Cloudflare Pages / Netlify header rules, see below
Every wire path a source contributes (config.json, c/index.json, and
everything under p/) is copied to index/<label>/ unconditionally, for
every configured source including the root: true one. The root: true
source's tree is additionally copied verbatim to the site root
(dist/p/**, dist/config.json, dist/c/index.json) — legacy-compat with
today's index.ocx.sh deploy shape, never a substitute for its
index/<label>/ copy.
catalog.json placement is asymmetric between root and non-root sources,
and it's easy to over-generalize: mirrorSources() writes each source's
own, single-source catalog to exactly one path — data/catalog/catalog.json
for the root: true source, or index/<label>/data/catalog/catalog.json
for every other source, never both. The root source therefore has no
index/<label>/data/catalog/catalog.json of its own; only non-root sources
get that path. The root source's single-source catalog briefly lands at
data/catalog/catalog.json, but emitCatalogTree() calls mirrorSources()
first and then overwrites that same path with the merged, multi-source
catalog — a deliberate last-write-wins, not an accident of ordering, so the
theme's single site-root fetch always sees every configured source's
packages, not just the root source's own.
_headers¶
renderHeaders() (src/sources/mirror.ts) emits one Cloudflare
Pages/Netlify-format block per mirrored prefix, blank-line separated:
/p/*
Content-Security-Policy: sandbox
X-Content-Type-Options: nosniff
/index/<label>/p/*
Content-Security-Policy: sandbox
X-Content-Type-Options: nosniff
One /index/<label>/p/* block is emitted per distinct source.label —
every configured source, root or not, since every source's mirror copy is
untrusted verbatim wire content. The leading /p/* block is emitted only
when some source sets root: true; otherwise dist/p/** doesn't exist and
the rule would be dead weight.
Note
_headers is honored only by Cloudflare Pages and Netlify. On any
other host it ships as an inert file. See
Hosting and headers.
Size and skip rules¶
- CAS asset size cap —
MAX_CAS_ASSET_BYTESinsrc/sources/mirror.tsis1024 * 1024(1 MiB). It applies only to CAS content assets — adesc.readme/desc.logoblob underp/.../o/sha256/, never a.jsonfile under that path. A blob exceeding the cap is skipped (not written) and a warning is printed to stderr:ocx-catalog: skipping oversized CAS asset <path> in "<label>": <n> bytes exceeds the 1048576-byte cap. Package roots and OCI image indices are never capped here — skipping one of those would dangle a package rather than degrade gracefully the way a missing logo/readme does. - Wire-asset extension filter —
WIRE_ASSET_EXTENSIONSinsrc/sources/walker.tsis{"json", "svg", "png", "md"}.path/gitsources filter their directory walk underp/to exactly this set, because apath/gitsource root is often a full repository checkout, not a hand-curated export: an unfiltered copy would otherwise leak an arbitrary same-origin file (.html,.js, ...) into the publicdist/index/<label>/p/mirror. Aurlsource needs no such filter — it only ever fetches these exact wire paths by digest, never an arbitrary directory entry.
Ownership¶
| Surface | Owner | Stability |
|---|---|---|
data/catalog/catalog.json, rendered HTML pages |
This package | Free to change shape between releases |
config.json, p/<namespace>/<package>.json, p/.../o/sha256/<hex>.json |
The index | Frozen wire contract — this package reads and mirrors it faithfully, never rewrites it |
See Index vs. catalog for the full distinction between what an index publishes and what this renderer builds around it.