Getting Started / Repository Layout

Tracked and Generated Paths

Which paths the repository tracks, which it generates, and what each top-level directory holds.

Tracked Against Untracked

A deploy reads src/, assets/ and .interp/. The rest of the untracked bulk is build input, measurement state, or regenerable output.

Measure the counts rather than reading them here. git ls-files | wc -l and du -sh are the instruments. The shape of the table is what is meant to last; the numbers in it go stale.

path size tracked how it arrives on a clean clone delete?
src/ 4.5M yes committed no
tests/ 6.0M yes committed no
scripts/ 17M yes committed no
docs/ 1.1M yes committed no
assets/ 207M 5 files bun run hydrate; bun run build:local in full no
.interp/ 251M no bun install restores 2 of 3; bun run hydrate or build:local the rest no
.siblings/ -- no bun run build:local, only when the modules are not checked out beside this yes, regenerable
vendor/ 198M no not reproducible; restored from drupflare-cdn/vendor/. Not a deploy input never
drupal-src/ 173M no bun run fetch:drupal, which build.yml also calls yes, regenerable
experiments/ 340K yes committed no
.pack-backup/ 7.2M no bun run bake:pack no
.contrib-fixture/ 38M no bun run test:contrib, and only while it is running no
.trim-assets/ 37M no bash scripts/stage-edge-assets.sh yes, regenerable
typedoc/ 37M no bun run docs:build yes, regenerable
coverage/ -- no bun run test:coverage yes, regenerable
dist/ 74M no bun run release:payload yes, regenerable
.wrangler/ 209M no any wrangler dev or vitest run yes, regenerable
node_modules/ 950M no bun install yes, regenerable

.wrangler/ grows with every wrangler dev session and vitest run, and nothing prunes it. Delete it when disk space matters.

docker/ holds two rigs, which are unrelated. compose.yml is the nine-service integration rig (GreenMail, Postgres, MySQL, Redis, syslog, Gitea, Forgejo, Keycloak, GitLab). vps.yml with docker/vps/ is the comparison arm: nginx and PHP 8.5 FPM running the same drupal-src tree and the same assets/drupal/site.sqlite this project serves, so a measurement isolates the runtime. The arm mounts the tracked database read-only and copies it, because SQLite writes to the file it reads.

docs/

docs/ holds seven folders: getting-started/, operating/, architecture/, configuration/, security/, impact/ and audiences/. Each has an index.md that lists its pages, and a page longer than about 250 lines becomes a folder of its own with an index.md and sibling pages. docs/operating/recovery.md publishes as operating/recovery.html, and a folder's index publishes as <folder>/index.html. docs/README.md is a map for the repository and is not published.

The published navigation lists only the top-level entries, which are each folder's index and the technical report, and each page's contents list holds its ## headings only. A page is reached from its folder's index and from links in the text. A new page goes in the folder it belongs to and needs a # title and a lead paragraph, which becomes its description. A new folder needs an index.md and an entry in TOP in scripts/docs/pages.ts.

Moving or splitting a page means adding its old name to scripts/docs/moves.json, which writes a redirect page at the old URL. An entry is a new page name, or { "to": ..., "anchors": { ... } } where anchors sends a heading that moved to another page to that page, so a link to the heading keeps working. Fix every link to the old path as well. tests/node/docs-site.spec.ts fails on a folder with no index, a page with no lead or title, a moved page with no redirect, a redirect or anchor that does not resolve, and a relative link that does not resolve.

experiments/

73 files, 112,477 bytes, all committed, per git ls-files experiments: 63 wrangler configs (61 under experiments/wrangler/, including the old rig and bench configs moved there from the repo root, and one each in arena/ and duration/), plus the experiments/wrangler/README.md, the probe seams those directories pair with their configs, two binary seams under binary/, a loose probe-o3mbsjlj.ts at the top, and the data files from the boot-phase sweeps. They are kept for reproduction and are not maintained. .prettierignore excludes the directory from formatting.

assets/core/

24 MB, 4,028 files, produced by bun run assets:static from step 13 of the source build. It is the browser-fetchable half of the Drupal tree (css, js, fonts and images under core/) copied to where Workers Assets serves it at the /core/** URLs Drupal already emits.

All three PHP packers skip those extensions, because PHP never opens them. Nothing serves a file out of the PHP MEMFS over HTTP, so these files reach the browser only through this directory. Workers Assets content-hashes, caches and compresses what it serves, and a cache hit never reaches the Worker.

assets/.assetsignore denies by default and negates /core/. tests/unit/runtime/assets-ignore.spec.ts asserts that the negation reaches into the directory and that every /core/** URL a prefilled page references answers 200. The tracked assets/_headers sets the cache lifetimes of that tree; see Static Asset Cache Lifetimes in docs/configuration/index.md.

vendor/

wrangler deploy does not read it. The canonical config aliases ./runtime/php-binary.js to src/runtime/php-binary-raw.ts, which imports the interpreter from .interp/. vendor/ reaches a bundle only through the unaliased default seam src/runtime/php-binary.ts and the src/probes/** configs, so it is a local store of hand-built binaries kept for reproducing past measurements.

Never delete, move or overwrite anything under it. A local deletion is recoverable, but the recovery is not free. All 34 vendor files are in drupflare-cdn, each matching on size and on ETag-against-md5. They are 34 of the manifest's 35 keys; the 35th is assets/drupal/site.sqlite. Four retired vendor/ keys sit in the manifest's archived list.

vendor/php8.3.wasm, vendor/php8.4.wasm and their -web.mjs glue are the only reproducible entries: bun run vendor copies them out of the php-wasm npm package. They cost 27 MB and are backed up, and no change is recommended.

drupal-src/

The Drupal tree the packers read, and the only path that satisfies DRUPAL_ROOT. bun run fetch:drupal produces it: the release tarball for SHIPPED_CORE_VERSION from ftp.drupal.org, extracted with the drupal-<version>/ wrapper stripped, then completed with the four contributed modules the tarball does not carry. DRUPAL_VERSION overrides the version; --force replaces a tree that is present at a different one. A second run on a satisfied tree downloads nothing.

.github/workflows/build.yml calls the same script, resolving the version with bun scripts/fetch-drupal-tree.ts --print-version, so a developer and the gate lane hold the same tree. tests/node/drupal-tree.spec.ts fails if the workflow grows a second producer.

The tarball is core only. SHIPPED_LOCK_VERSIONS names admin_toolbar, ctools, pathauto and token beyond it, so the script installs those four at their pinned versions with composer; tests/node/shipped-lock.spec.ts compares the resulting composer.lock against the baked map.

Four things need it. assets:twig, assets:core, assets:pack and assets:static read it as their build input. bun run test:php and the sibling PHP suites in ../drupflare and ../rom take it as DRUPAL_ROOT. composer analyze resolves \Drupal\Core\... against it through phpstan.neon. An IDE indexes it for the same reason. Everything except the packers degrades to skipped or unresolved without it, and none of them fails loudly, so the tree being absent is quiet.

.siblings/

bun run assets:driver reads the sibling checkouts of drupflare/drupflare, drupflare/rom and drupflare/stream-http directly and packs them into assets/driver.json. The repository keeps no copy of the modules.

Composer never runs on the edge, so a composer require ships nothing: the packed tree is the vendor directory, and the packed copy is what executes. composer.json records provenance and lets a developer resolve the packages normally. Both packages are on Packagist as drupflare/rom and drupflare/drupflare. tests/node/driver-pack.spec.ts rebuilds assets/driver.json from the modules on disk and compares byte for byte, so a stale pack fails the gate.

The packer takes an allow-list of module-shaped paths (src, .info.yml, .install, .module, .services.yml), because a module repository is not a module and walking a checkout wholesale would pull node_modules/, vendor/ and coverage/ into the bundle. The machine name comes from the mount rather than the directory: ../rom provides cfw_do_sqlite.

Each of the three is resolved as DRUPFLARE_SRC / ROM_SRC / STREAM_HTTP_SRC, then ../<name>, then .siblings/<name>. CI sets the environment variables and checks the repositories out under .siblings/; bun run build:local clones into the same paths, and only the ones that are not already there.