v3: comments say what the code can't — the documentation pass wanted JSDoc on every export and every field of an exported interface, which filled the code with restatements ("The dump.", "Whether it is loading.", "The props of X.", "Renders the X.") and module docs that told a reader nothing ("The hot and new feeds, and the props every main feed of the index takes."); some were wrong as well as vague. Every comment in api/, shared/ and src/ is rewritten under one rule, now stated in the README's "Comments" section: every module and export still has a JSDoc, and it gives context (who uses it and where), usage (what a caller must know: a precondition, what it leaves to the caller, a side effect) or its place in the chain (where its input comes from, where its output goes); a field, parameter, private function or local gets one only when it adds to its parent's, and context about a whole type goes on the type; a type's doc is phrased from the value's side ("Returned to Y by X when Z", "Passed to Y by X"); full sentences, as short as they can be, no label-colon-list; // goes wherever the code isn't obvious and says why, and the section banners and narration go. Every claim was checked against the code, which turned up some forty comments that were wrong: DumpCard's claimed it was only used by the hot and new feeds, the WebSocket welcome claimed to be a socket's first message (presence comes first), UpdateUserRequest said null removes a description (it keeps it), deletedDumpIds leaves out that it also holds dumps made private, THEME_COLOR claimed to mirror index.html's, and the like. The routers' JSDoc now says where they are mounted and what becomes of a thrown APIException. doc-lint.ts lets a field without a doc through; a module or export without one still fails. No code changes; 204 backend and 628 frontend tests pass, and doc:lint, deno check, deno lint and tsc are clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
111
README.md
111
README.md
@@ -1,6 +1,10 @@
|
||||
# gerbeur
|
||||
|
||||
A small invite-only social platform for sharing links and files. Users can post URLs and media (YouTube, SoundCloud, Bandcamp, images, audio, video, …), vote, comment, follow each other, build playlists, and search content. A real-time WebSocket layer handles live presence, vote counts, and notifications. The UI is localized (English and French) and ships multiple visual themes.
|
||||
A small invite-only social platform for sharing links and files. Users can post
|
||||
URLs and media (YouTube, SoundCloud, Bandcamp, images, audio, video, …), vote,
|
||||
comment, follow each other, build playlists, and search content. A real-time
|
||||
WebSocket layer handles live presence, vote counts, and notifications. The UI is
|
||||
localized (English and French) and ships multiple visual themes.
|
||||
|
||||
## Stack
|
||||
|
||||
@@ -30,13 +34,16 @@ cp .env.example .env
|
||||
deno task dev
|
||||
```
|
||||
|
||||
This starts both the API server (port 8000, with file watching) and the Vite dev server (port 3000) concurrently.
|
||||
This starts both the API server (port 8000, with file watching) and the Vite dev
|
||||
server (port 3000) concurrently.
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000). On first run a default `admin` / `admin` account is created — change the password immediately.
|
||||
Open [http://localhost:3000](http://localhost:3000). On first run a default
|
||||
`admin` / `admin` account is created — change the password immediately.
|
||||
|
||||
### Environment variables
|
||||
|
||||
See [`.env.example`](.env.example) for the full list with descriptions. Key variables:
|
||||
See [`.env.example`](.env.example) for the full list with descriptions. Key
|
||||
variables:
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
||||
@@ -58,7 +65,10 @@ See [`.env.example`](.env.example) for the full list with descriptions. Key vari
|
||||
|
||||
### Docker (recommended)
|
||||
|
||||
The standard deployment runs API and frontend in a single container. The API server (Oak) serves the compiled frontend as static files, so both share the same origin — no `VITE_API_*` build args needed. Set `GERBEUR_PUBLIC_URL` to the externally-visible URL; it is automatically allowed for HTTP/WebSocket requests.
|
||||
The standard deployment runs API and frontend in a single container. The API
|
||||
server (Oak) serves the compiled frontend as static files, so both share the
|
||||
same origin — no `VITE_API_*` build args needed. Set `GERBEUR_PUBLIC_URL` to the
|
||||
externally-visible URL; it is automatically allowed for HTTP/WebSocket requests.
|
||||
|
||||
```sh
|
||||
docker build -t gerbeur .
|
||||
@@ -77,12 +87,17 @@ docker run -d \
|
||||
```
|
||||
|
||||
The two volumes are required for persistence:
|
||||
- `gerbeur-db` — SQLite database (`api/sql/gerbeur.db`), initialized automatically on first run
|
||||
|
||||
- `gerbeur-db` — SQLite database (`api/sql/gerbeur.db`), initialized
|
||||
automatically on first run
|
||||
- `gerbeur-uploads` — user-uploaded files (`api/uploads/`)
|
||||
|
||||
#### Separate API and frontend (optional)
|
||||
|
||||
If you need to run the API on a different host than the frontend, pass the API location as build args so it gets baked into the frontend bundle. In that setup, add the frontend origin to `GERBEUR_ALLOWED_ORIGINS` so cross-origin HTTP/WebSocket requests are accepted:
|
||||
If you need to run the API on a different host than the frontend, pass the API
|
||||
location as build args so it gets baked into the frontend bundle. In that setup,
|
||||
add the frontend origin to `GERBEUR_ALLOWED_ORIGINS` so cross-origin
|
||||
HTTP/WebSocket requests are accepted:
|
||||
|
||||
```sh
|
||||
docker build \
|
||||
@@ -94,7 +109,8 @@ docker build \
|
||||
|
||||
### Reverse proxy
|
||||
|
||||
Put a reverse proxy (nginx, Caddy, …) in front of the container to handle TLS. Forward everything to port 8000. Example Caddyfile:
|
||||
Put a reverse proxy (nginx, Caddy, …) in front of the container to handle TLS.
|
||||
Forward everything to port 8000. Example Caddyfile:
|
||||
|
||||
```
|
||||
example.com {
|
||||
@@ -158,27 +174,65 @@ place that fetches from other sites. Tests mirror the source one file per
|
||||
module, so the tests of `services/chat-service.ts` are in
|
||||
`test/services/chat-service.test.ts`.
|
||||
|
||||
All three parts — `api/`, `shared/` and `src/` — are documented for `deno doc`,
|
||||
the same way in every file. Each module opens with a `/** … @module */` block
|
||||
saying what it is for. Every export has a JSDoc comment, and so does every field
|
||||
of an exported interface; a comment that describes a declaration is always `/**
|
||||
*/`, and `//` is only for notes on the code inside. Functions open with a verb
|
||||
("Returns…", "Checks…", "Renders…" for a component), everything else with a noun
|
||||
phrase. Parameters are named in the prose, in backticks. `{@link}` points at
|
||||
symbols the file already has in scope. A function that can throw to its caller
|
||||
says so with `@throws`: for an `APIException`, the statuses and when. A router's
|
||||
module doc lists its endpoints, one line each: method and path, who may call it,
|
||||
what it does; a page's JSDoc names its route and who may see it. The first
|
||||
paragraph of a doc is its summary, which the reference shows alone in lists, so
|
||||
it is a sentence that stands on its own: it never ends in a colon introducing
|
||||
what follows.
|
||||
### Comments
|
||||
|
||||
A comment says what the code can't. If deleting it would lose nothing, it is the
|
||||
wrong comment: on an export, rewrite it; anywhere else, delete it.
|
||||
|
||||
**Every module and every export has a JSDoc** (`/** */`). Each gives at least
|
||||
one of:
|
||||
|
||||
1. **Context**: who uses it and where (a page and its route, a caller, a
|
||||
WebSocket message).
|
||||
2. **Usage**: what a caller must know to use it correctly, such as a
|
||||
precondition, an invariant, what it leaves to the caller, or a side effect.
|
||||
Add an example only when the call isn't obvious.
|
||||
3. **Place in the chain**: where its input comes from and where its output goes.
|
||||
|
||||
A type's doc is phrased from the value's side, naming the producer and the
|
||||
consumer: "Returned to Y by X when Z", "Passed to Y by X". It is never "What X
|
||||
returns to Y" or "What X passes to Y".
|
||||
|
||||
Don't pack a doc into a label followed by a colon and a list ("X: one thing,
|
||||
plus some other thing"). Write sentences with a subject and a verb.
|
||||
|
||||
When the obvious comment is useless, write the useful one: the context is always
|
||||
there. A **field** (of an interface, or a parameter) gets a doc only when it
|
||||
adds something its parent's doc doesn't. Context that concerns the whole type
|
||||
goes on the type, not on one of its fields. The same goes for private functions
|
||||
and locals. None of these count as a doc:
|
||||
|
||||
- a restatement of the name or type (`/** The dump. */`, "Whether it is
|
||||
loading.", "The props of X.", "Renders the X.");
|
||||
- a description of what the body, the JSX or the SQL plainly does;
|
||||
- a list that goes stale (every caller of a widely used component): name the
|
||||
kind of caller instead.
|
||||
|
||||
When a comment is there, it is as short as it can be, often one sentence, and it
|
||||
is written in full sentences. Labels and fragments ("Upvotes on dumps.") don't
|
||||
count. A module doc is a few lines at most. It opens with what the module does,
|
||||
then says who calls it and what it hands back, and it doesn't repeat what its
|
||||
exports' docs say. `@throws` stays on whatever throws to its caller, with the
|
||||
statuses of an `APIException` and when each happens. A router's module doc lists
|
||||
its endpoints (method, path, who may call it), since the code states them
|
||||
nowhere in one place. A summary never ends in a colon: the reference shows it
|
||||
alone.
|
||||
|
||||
**`//` comments** go wherever the code isn't obvious. They explain why, not
|
||||
what: a workaround, an ordering constraint, a surprising limitation, a security
|
||||
reason, an event handler that must not bubble.
|
||||
|
||||
**Every claim is checked against the code** (grep the callers). A wrong comment
|
||||
is worse than none.
|
||||
|
||||
### Reference
|
||||
|
||||
In the frontend, every exported function states its return type, so the
|
||||
reference can show it: a component returns `React.JSX.Element` (or `| null`), a
|
||||
hook returning several things returns a documented `Use<Name>Result` interface.
|
||||
A component that takes props takes an exported `<Name>Props` interface, each
|
||||
prop documented. React's types are used through the global `React` namespace
|
||||
rather than a default import of `react`, which `deno doc` can't follow.
|
||||
A component that takes props takes an exported `<Name>Props` interface. React's
|
||||
types are used through the global `React` namespace rather than a default import
|
||||
of `react`, which `deno doc` can't follow.
|
||||
|
||||
`deno task doc` builds the HTML reference into `docs/` from every module of
|
||||
`api/`, `shared/` and `src/` but the tests (and the generated `src/locales/`).
|
||||
@@ -187,9 +241,10 @@ repository's three parts and their layers in prose; `deno doc` builds the list
|
||||
of every module itself, grouped by path. The `docs` workflow publishes it on
|
||||
every push to `main` that touches them. `deno task doc:lint` checks the rules
|
||||
the linter can see, and that no summary ends in a colon, through `doc-lint.ts`.
|
||||
`deno doc` doesn't document npm packages, so it takes a type imported from one
|
||||
(react-hook-form's `FieldValues`, say) for a private one, and those references
|
||||
alone are let through.
|
||||
Two kinds of diagnostic are let through. A field without a doc passes, since a
|
||||
field gets one only when it adds to its type's. `deno doc` doesn't document npm
|
||||
packages, so it takes a type imported from one (react-hook-form's `FieldValues`,
|
||||
say) for a private one, and those references pass too.
|
||||
|
||||
Each of the frontend's `components/`, `contexts/`, `hooks/` and `utils/` is
|
||||
split into the same domain folders — `auth`, `category`, `chat`, `comment`,
|
||||
|
||||
Reference in New Issue
Block a user