fuzzyworld-tools / files / docs / apps.md
apps.md
bb7e1f57 2026-11-06 Pavel Safronov | 1 | # Apps (apps.fuzzyworld.net) |
| 2 | ||
| 3 | Each app lives at `apps.fuzzyworld.net/<app>/` and is a static build uploaded by `fw deploy`. | |
| 4 | ||
| 5 | ## Making a repo deploy as an app | |
| 6 | ||
| 7 | Add to the app repo's `package.json`: | |
| 8 | ||
| 9 | ```jsonc | |
| 10 | "scripts": { "deploy": "fw deploy" }, | |
| 11 | "fuzzyworld": { | |
| 12 | "app": "my-game", // URL: apps.fuzzyworld.net/my-game/ (required) | |
| 13 | "title": "My Game", // apps index | |
| 14 | "description": "One line.", // apps index | |
| 15 | "repo": "my-game", // DreamHost repo name, if different from app | |
| 16 | "prebuild": ["pnpm gen-assets"], // commands before checks and build | |
| 17 | "checks": ["typecheck", "test"], // scripts that must pass (default: whichever of typecheck/lint/test exist) | |
| 18 | "build": "make public", // default: `<pm> run build` | |
| 19 | "out": "dist", // default: dist, else public, else build | |
| 20 | "keep": ["my-game.apk"], // files on the server a deploy must not delete or replace (e.g. a download built elsewhere) | |
| 21 | "extra": ["sim/*.js"], // build-output files every later deploy keeps: uploaded and updated, never deleted (e.g. old rules versions) | |
| 22 | "replay": "replay.html?report={record}" // a page of this app that plays a bug report's run record (see issues.md) | |
| 23 | }, | |
| 24 | "devDependencies": { "fuzzyworld-tools": "git+https://code.fuzzyworld.net/fuzzyworld-tools.git#semver:^1.0.0" } | |
| 25 | ``` | |
| 26 | ||
| 27 | For vite, add the plugin. It sets `base` to `/<app>/` for builds (the dev server is unchanged). `FW_APP`, set by | |
| 28 | `fw deploy --as`, overrides the app name. | |
| 29 | ||
| 30 | ```ts | |
| 31 | import fuzzyworld from 'fuzzyworld-tools/vite'; | |
| 32 | export default defineConfig({ plugins: [fuzzyworld()] }); | |
| 33 | ``` | |
| 34 | ||
| 35 | ## `fw deploy` | |
| 36 | ||
| 37 | Run it with `pnpm run deploy` or `npm run deploy`. | |
| 38 | ||
| 39 | 1. **Clone.** Clones branch `main` of `dh:git/<repo>.git` into a temp dir. It builds what is pushed, never your working copy (`--ref <branch>` for another branch, `--here` to build the current directory as is). | |
| 40 | 2. **Install** with the lockfile's package manager (`pnpm install --frozen-lockfile`, `npm ci`, `yarn install --frozen-lockfile`). | |
| 41 | 3. **Prebuild, checks, build.** Any failure stops the deploy. | |
| 42 | 4. **Check the output.** Every same-site URL in `index.html`, the web manifest's `id`/`scope`/`start_url`, and every `serviceWorker.register(...)` script and scope must stay inside `/<app>/`. That keeps apps on the shared origin from capturing each other, and makes each one install separately on Android. Any problem stops the deploy. | |
| 43 | 5. **Upload.** `rsync --delete` to `apps.fuzzyworld.net/<app>/`. | |
| 44 | - `keep` paths are excluded. | |
| 45 | - `extra` files are protected from `--delete` by an rsync filter: uploaded and updated, never removed. | |
| 46 | - It writes `.fuzzyworld.json` (commit, time, and the `replay` template; not web-readable). | |
| 47 | - It refreshes the apps index and the repo's code page. | |
| 48 | 6. **Print the URL.** | |
| 49 | ||
| 50 | `--dry-run` does everything except upload. | |
| 51 | ||
| 52 | `--as NAME` publishes the app at `apps.fuzzyworld.net/NAME/` instead: a second copy of the same repo (for example | |
| 53 | `explodorama-next` built from `main` while `/explodorama/` is deployed from a tag with `--ref`). The build gets | |
| 54 | `FW_APP=NAME`, which the vite plugin uses for `base`, and the index title gets a suffix. | |
| 55 | ||
| 56 | ## The apps index | |
| 57 | ||
| 58 | A table ordered by the newest commit of each app's repo (not by deploy date: an app can be redeployed often | |
| 59 | without its code changing, so the deploy date has its own column). | |
| 60 | ||
| 61 | Columns: screenshot thumbnail (if the runner has taken one), name, description, last commit, deployed, and a source | |
| 62 | link for apps from public repos. The code index on code.fuzzyworld.net is likewise a table of public repos. | |
| 63 | ||
| 64 | Apps hosted elsewhere are listed from `server/site-apps/extra.json` (today: engye on Cloudflare Pages; | |
| 65 | `apps.fuzzyworld.net/engye/...` redirects there). | |
| 66 | ||
| 67 | ## Screenshots | |
| 68 | ||
| 69 | After a successful pipeline on the default branch, an app is built the way `fw deploy` builds it, served at | |
| 70 | `/<app>/` inside a Playwright container with no network, and photographed. The pictures go to | |
| 71 | `apps.fuzzyworld.net/_shots/<app>/` and appear on the apps index and the repo's summary page. They need a runner | |
| 72 | with the `chrome` tag ([ci.md](ci.md#tags)). | |
| 73 | ||
| 74 | ```jsonc | |
| 75 | "fuzzyworld": { | |
| 76 | "screenshots": [{ "path": "", "size": "phone", "wait": 1500 }, { "path": "?demo", "size": "desktop" }], | |
| 77 | // sizes: phone 390×844, tablet 820×1180, desktop 1280×800. Default: one phone shot of the start page. | |
| 78 | // or a command that writes its own PNGs to $FW_SHOTS_DIR, for apps that need clicks first: | |
| 79 | // "screenshots": "pnpm shots", | |
| 80 | // or false: no screenshots | |
| 81 | } | |
| 82 | ``` | |
| 83 | ||
| 84 | Without runner screenshots, the images in a public repo's `screenshots/` folder are shown instead. |