diff --git a/.gitmodules b/.gitmodules index 3d38b21..abe6910 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "reference/writeonce-api"] path = reference/writeonce-api url = https://github.com/shoneyJ/writeonce-api +[submodule "docs/examples/site"] + path = docs/examples/site + url = git@github.com:shoneyJ/writeonce-site.git diff --git a/docs/examples/site b/docs/examples/site new file mode 160000 index 0000000..81ef9b9 --- /dev/null +++ b/docs/examples/site @@ -0,0 +1 @@ +Subproject commit 81ef9b9ca00c7941dd37d3d3e3e4de6af849a5b4 diff --git a/docs/examples/site/CODE-LOGIC.md b/docs/examples/site/CODE-LOGIC.md deleted file mode 100644 index 22f5dbe..0000000 --- a/docs/examples/site/CODE-LOGIC.md +++ /dev/null @@ -1,108 +0,0 @@ -# site — how it is put together - -Written 2026-08-23 with the sample's landing; restructured 2026-08-25 -onto the program template's MVC layout (`docs/examples/shop`), so the two -samples now read the same way. - -## The layout - -| file | layer | what it owns | -| --- | --- | --- | -| `types.wo` | MODEL | the `Chapter` `@table`, the `ChapterLink` projection, `Chapters.links()`, and `seed_if_empty()` | -| `content.wo` | MODEL (content) | the ten chapter bodies as fragment-returning functions, plus `seed_chapters()` | -| `layout/app.wo` | VIEW (chrome) | `AppShell` — the component that fills writeonce-view's `Layout` — the two named widths, and `html_error` | -| `layout/header.wo`, `layout/footer.wo` | VIEW (chrome) | the shared nav bar (brand = mark + wordmark) and footer | -| `layout/logo.wo` | VIEW (chrome) | the mark as inline SVG, plus the `` links | -| `install/view.wo`, `install/controller.wo` | VIEW + CONTROLLER | `/install` — the toolchain guide. Static copy, so `InstallPage` has no fields | -| `packages/view.wo`, `packages/controller.wo` | VIEW + CONTROLLER | `/packages` and `/packages/:name` — the catalogue, its cards, and per-package usage | -| `favicon/controller.wo` | CONTROLLER | `/favicon.svg` — builds its own `Resp` (image/svg+xml) | -| `home/view.wo` | VIEW | `HomePage` and the homepage's code showcase | -| `home/controller.wo` | CONTROLLER | `Home` — the `/` handler | -| `chapter/view.wo` | VIEW | `ChapterNav`, `ChapterPage` | -| `chapter/controller.wo` | CONTROLLER | `ShowChapter` — the `/ch/:slug` handler | -| `admin/controller.wo` | CONTROLLER | `AdminEdit` — bearer-gated edit, answers a redirect (no view: it redirects) | -| `health/controller.wo` | CONTROLLER | `Health` — the liveness probe (no view: it answers text) | -| `main.wo` | BOOTSTRAP | seed, routes, serve. Nothing else | -| `wo.toml` | — | the two `[deps]`: `framework` (serving) and `html` (markup) | - -One feature = one directory = one module, holding that feature's view -and its controller together. A module sees its own declarations plus -what it `use`s, so `home/` reaching the chapter nav has to say `use -chapter`. - -The model stays at the root and is reachable from everywhere: a CLASS -crosses module lines without being exported, and only a free `fn` is -module-scoped (`WO-E210`). That single rule explains the whole layout — -`Chapter` and `ChapterLink` are classes, so the feature modules just -name them; the shared query would have been a free fn, so it is a -`static fn` on `Chapters` instead. (`pub` cannot prefix an `@table` -class — recorded gap #1 — but nothing needs it to.) - -## Decisions that are not obvious from the code - -- **Chapters are rows, not constants.** `seed_if_empty()` inserts them - only when the table answers empty, so a WAL restart keeps admin edits - instead of reseeding over them — the sample's own proof of chapter 6's - claim. The seed bodies are BUILT with writeonce-view's builders at boot; after - that the table is the truth and the builders are never consulted again. -- **The seam is enforced by where the query sits.** `Chapters.links()` - lives with the MODEL and hands the view a `multi ChapterLink` — - a projection, not a cursor. No component in this sample touches the - database, which is what lets `ChapterNav` be the same component on the - homepage and on every chapter page, differing only by `current`. -- **`HomePage` and `ChapterPage` hold a `Component`, not chapter data.** - The nav arrives as an already-built child component in a slot, so - neither page knows what a chapter is. That is content projection — - Angular's ``, with the slot as an ordinary field. -- **Two widths, named once.** `AppShell` carries a `container` field and - `layout/app.wo` exports `reading_shell` / `wide_shell`. The Tailwind - class strings appear in exactly one place instead of being repeated at - every call site. -- **Auth is handler-side by doctrine.** The framework ships mechanism - (`bearer_token`, constant-time `ct_eq`); which routes are gated and by - which token is policy, so `AdminEdit` checks its own field. No global - middleware — the public pages stay public. -- **`ok_html` is the framework's**, beside `ok_text`/`ok_json`: a status - line plus a content-type is transport, not rendering. -- **`\$` in chapter code samples.** Chapter sources show interpolation - (`${port}`) inside string literals of a language that interpolates — - the lexer's `\$` escape keeps them literal; `code_block()` then - HTML-escapes the result. This is also why those two samples stay - escaped `"..."` strings rather than becoming raw literals: a raw - literal has no escape character, so it cannot spell a literal `${`. -- **Concat spans lines two ways now.** A line ENDING in `..` continues on - the next (the one newline suppression in the language) — it never works - at the START of a line. For markup, prefer the backtick raw literal: - real newlines, real double-quoted attributes, source indentation - removed at compile time, `${ }` raw and `{{ }}` auto-escaping. The old - "`..` does not straddle newlines, so build accumulator-style" note is - obsolete and was removed. -- **The logo is inline SVG, authored once.** `logo_svg(px)` goes in the - nav brand and `favicon_svg()` is served at `/favicon.svg` — a dark tile - with a two-stroke "W", white then accent blue. No asset pipeline, no - binary in the repo, and it stays legible at 16px. The `` link - reaches the document through `Layout`'s `head` slot. -- **A raw literal cannot contain a literal `{{`.** The packages page has - prose ABOUT `{{ }}` holes, and writing it directly would have made it a - hole; it is written with `{` entities instead. This is the same - limitation the chapter code samples hit with `${`, and the reason both - doors exist. -- **Downloads are the framework's, not the site's.** `/dl/*path` is - `StaticFiles` mounted in `main.wo` with a 16 MiB ceiling — no - controller, because there is no decision to make. `WO_DIST` says where - the tarballs are (default `./dist`). The install page offers the GitHub - release as primary and this as a mirror, with the `.sha256` beside it. -- **The supported-systems list is read off the binaries**, not off a - wish list: `file` gives the triple, and the highest `GLIBC_` symbol - version they import gives the libc floor (2.38 today). Overstating - support costs a reader an afternoon. -- **`SITE_HOST` picks the interface.** Loopback by default — right behind - a proxy — with the env var for reaching a dev instance across the LAN. - The bound address is printed at startup. -- **writeonce-view's sheet is static.** Tailwind's class NAMES, one hand-written - CSS string inlined per page by `page()` — self-contained responses, no - toolchain; growing the sheet is appending a line in `tw_css()`. - -Gate: `just site` — see `scripts/site-accept.sh` (11 checks; the restart -leg polls `/health` instead of sleeping, so it does not share -web-app-accept's 0.5s boot race). diff --git a/docs/examples/site/README.md b/docs/examples/site/README.md deleted file mode 100644 index c8c5833..0000000 --- a/docs/examples/site/README.md +++ /dev/null @@ -1,140 +0,0 @@ -# site — writeonce.de - -The language tutorial, served BY the language. One binary carries the HTTP -server, the router, the pages and the database; the chapters you read are -rows in a `@table`, the markup is built by the `writeonce-view` dependency, and -the whole thing is chapter 9's own example. - -``` -[deps] -porch = { git = "https://github.com/shoneyj/porch", rev = "v0.1.0" } -view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" } -``` - -## Run it - -``` -woc . && SITE_TOKEN=change-me WO_DATA=./data ./target/site 8080 -``` - -It binds loopback by default. To reach it from another machine while -developing, name the interface: - -``` -SITE_HOST=0.0.0.0 SITE_TOKEN=change-me WO_DATA=./data ./target/site 8080 -``` - -- `GET /` — the homepage; `GET /ch/` — one chapter. -- `GET /install` — the installation guide; `GET /packages` and - `GET /packages/` — the package catalogue with copy-paste - `[deps]` lines and usage. -- `GET /favicon.svg` — the mark, inline SVG, no asset pipeline. -- `GET /dl/` — release tarballs, served by the framework's - `StaticFiles` from `$WO_DIST` (default `./dist`, where `just dist` - writes them). -- `POST /admin/ch/` — edit a chapter (`title`/`body`, form-encoded, - `authorization: Bearer $SITE_TOKEN`). Edits are WAL-durable under - `WO_DATA` and replay on restart — that is chapter 6, demonstrated by - the site that teaches it. -- Without `WO_DATA` the chapters live in RAM and reseed on every boot. - -The acceptance gate is `just site` (scripts/site-accept.sh): two file:// -dep remotes, build, the page matrix, 401, an authed edit, SIGTERM, and -the edit surviving a restart. - -## The file map - -MVC, laid out exactly like the program template -([`docs/examples/shop`](../shop/README.md)) so the two read the same way: - -| this app | layer | -| --- | --- | -| `types.wo` | MODEL — the `Chapter` `@table`, and seed-if-empty | -| `content.wo` | the ten chapter bodies + `seed_chapters()` | -| `layout/` | the chrome: `AppShell` (+ the two named widths), header, footer, `html_error` | -| `home/`, `chapter/`, `install/`, `packages/` | one module per feature: its `view.wo` (components: fields in, Text out) and its `controller.wo` (query the model, fill the components, answer a `Resp`) | -| `admin/`, `health/`, `favicon/` | controller-only features — a redirect, a text probe, an SVG | -| `layout/logo.wo` | the mark as inline SVG: one source for the nav brand and `/favicon.svg` | -| `main.wo` | bootstrap: seed, routes, serve — nothing else | - -Every `render()` makes its class a component (writeonce-view's structural -`Component`). `HomePage` and `ChapterPage` each take the chapter nav as -an already-built child component in a slot, so neither knows what a -chapter is; `ChapterNav` is therefore literally the same component on the -homepage and on every chapter page, differing only by which `ord` is -current. - -The seam that keeps it honest: **every query lives in a controller.** -`chapter_links()` sits in `chapter.controller.wo` and hands the views a -`multi ChapterLink` projection — no component in this sample touches the -database, and writeonce-view contains no query at all. - -One feature = one directory = one module, holding that feature's view -and its controller. The `@table` lives in `types.wo` at the root and is -reachable from every feature module without being exported — a CLASS -crosses module lines, only a free `fn` is module-scoped (`WO-E210`). -That is why the query both pages need is `Chapters.links()`, a `static -fn` on a root class, rather than a free function one of them would have -to import from the other. - -## writeonce.de deployment - -The framework speaks HTTP/1.1 keep-alive and no TLS by design — terminate -TLS at the proxy and forward: - -``` -server { - server_name writeonce.de; - listen 443 ssl http2; # certs via certbot/acme - location / { proxy_pass http://127.0.0.1:8080; } -} -``` - -Run the binary under systemd (`Restart=on-failure`, `Environment=SITE_TOKEN=...`, -`Environment=WO_DATA=/var/lib/writeonce-site`); SIGTERM drains cleanly. - -### What to copy to the host - -The binary is self-contained — VM, bytecode and database engine are -inside it — but two directories are read at RUNTIME and must travel -with it: - -``` -/srv/writeonce-site/ - site the binary (docs/examples/site/target/site) - dist/ what /dl serves — the RELEASE assets: - writeonce-0.1.0-linux-amd64.tar.gz - writeonce-0.1.0-linux-amd64.tar.gz.sha256 - data/ WO_DATA — the WAL; create it, keep it -``` - -``` -SITE_TOKEN= required, the process refuses to start without it -WO_DATA=/srv/writeonce-site/data durable chapters; omit for RAM-only -WO_DIST=/srv/writeonce-site/dist where /dl reads from (default ./dist) -SITE_HOST leave UNSET behind a proxy — loopback is the - right default; set it only to expose directly -``` - -Behind a proxy `SITE_HOST` stays unset, so the process binds -`127.0.0.1` and is unreachable except through nginx. It prints the -bound address at startup, which is the quickest way to confirm that. - -**Keep `dist/` the published release, not a local build.** `just dist` -produces a different digest on every run, so a locally built tarball -would not match the `.sha256` the release publishes and would make the -mirror disagree with the GitHub download. Fetch the assets from the -release instead: - -``` -gh release download v0.1.0 -D dist -R shoneyJ/writeonce -``` - -## What it demonstrates - -Chapters 1–9 teach the language (values, containers, classes, optionals, -tables, actors, deps, serving); the app itself exercises the framework's -routing/:params, the Logging middleware, bearer auth (mechanism from -`http/auth.wo`, policy here), `form_values`, `@table` + query + update by -assignment, and `writeonce-view`'s escaping/builders/Tailwind-style utility -sheet — self-contained pages, no CDN, no JS, no build step. diff --git a/docs/examples/site/admin/controller.wo b/docs/examples/site/admin/controller.wo deleted file mode 100644 index 244a7bb..0000000 --- a/docs/examples/site/admin/controller.wo +++ /dev/null @@ -1,33 +0,0 @@ --- admin.controller.wo — POST /admin/ch/:slug: title/body update, --- form-encoded, bearer-gated. Mechanism (bearer_token, constant-time --- ct_eq) is the framework's; POLICY — which routes, which token — is --- this app's, right here. No rendering: the answer is a redirect. -use porch/http - -pub class AdminEdit { - token: Text - - fn handle(req: Req) -> Resp { - let got = bearer_token(req); - if got == nil { return unauthorized(); } - if ct_eq("${got}", self.token) == false { return unauthorized(); } - let slug = req.params["slug"]; - if slug == nil { return not_found(); } - let hits = from c in Chapter where c.slug == slug take 1 select c; - if len(hits) == 0 { return not_found(); } - let f = form_values(req); - if f == nil { return bad_request("body must be form-encoded (title, body)"); } - let title = f["title"]; - let body = f["body"]; - if title == nil and body == nil { return bad_request("nothing to update"); } - if title != nil { - let t = trim("${title}"); - if t == "" { return bad_request("title must not be empty"); } - hits[0].title = t; - } - if body != nil { - hits[0].body = "${body}"; - } - return redirect("/ch/${slug}"); - } -} diff --git a/docs/examples/site/chapter/controller.wo b/docs/examples/site/chapter/controller.wo deleted file mode 100644 index 36493f0..0000000 --- a/docs/examples/site/chapter/controller.wo +++ /dev/null @@ -1,23 +0,0 @@ --- chapter/controller.wo — the CONTROLLER for `/ch/:slug`. It queries the --- model and fills the view components that sit beside it in this module; --- a view receives VALUES, never a cursor. -use porch/http -use layout - -pub class ShowChapter { - fn handle(req: Req) -> Resp { - let slug = req.params["slug"]; - if slug == nil { - return html_error(404, "No such chapter", "The address names no chapter."); - } - let hits = from c in Chapter where c.slug == slug take 1 select c; - if len(hits) == 0 { - return html_error(404, "No such chapter", "Nothing is filed under that slug."); - } - let c = hits[0]; - let nav = ChapterNav { items: Chapters.links(), current: c.ord }; - let page = ChapterPage { ord: c.ord, title: c.title, body: c.body, chapter_nav: nav }; - let shell = reading_shell("writeonce — ${c.title}", page.render()); - return ok_html(shell.render()); - } -} diff --git a/docs/examples/site/chapter/view.wo b/docs/examples/site/chapter/view.wo deleted file mode 100644 index bb9519c..0000000 --- a/docs/examples/site/chapter/view.wo +++ /dev/null @@ -1,46 +0,0 @@ --- chapter/view.wo — the VIEW for `/ch/:slug`, plus the chapter nav that --- the homepage reuses. --- --- The MVC seam in one place: these components RENDER, the controller --- QUERIES, and the two never meet. ChapterNav holds VALUES (a list of --- links), so it renders identically on the homepage and on a chapter --- page — the only difference is which ord is `current`. Reuse is the --- same component with different fields, never copied markup. -use view - --- `ChapterLink` is the MODEL's projection type (types.wo); a class is --- reachable across module lines, so the view just names it. - -pub class ChapterNav { - items: multi ChapterLink - current: Int - - fn render() -> Text { - let out = ""; - for c in self.items { - let label = `${c.ord}. {{ c.title }}`; - if c.ord == self.current { - out = out .. el("li", "mb-2 font-bold text-gray-900", label); - } else { - out = out .. el("li", "mb-2", link("/ch/${c.slug}", "", label)); - } - } - return el("ul", "list-disc pl-6", out); - } -} - --- One chapter. `body` is site-authored HTML held in the row, so it goes --- through the RAW hole; the title is data and goes through `{{ }}`. -pub class ChapterPage { - ord: Int - title: Text - body: Text - chapter_nav: Component - - fn render() -> Text { - let head = el("h1", "text-3xl font-bold mb-4", `${self.ord}. {{ self.title }}`); - let art = el("div", "bg-white rounded-lg border shadow-sm p-6", head .. self.body); - let nav = el("div", "mt-8", el("h2", "text-lg font-bold mb-2", "Chapters") .. self.chapter_nav.render()); - return art .. nav; - } -} diff --git a/docs/examples/site/content.wo b/docs/examples/site/content.wo deleted file mode 100644 index 42c94eb..0000000 --- a/docs/examples/site/content.wo +++ /dev/null @@ -1,115 +0,0 @@ --- content.wo — the tutorial chapters, seeded into the Chapter table on --- first boot (types.wo's seed_if_empty). Model CONTENT, so it sits in --- the root module beside types.wo: it inserts rows. Bodies are HTML fragments --- BUILT with the writeonce-view dep — prose in el(), code samples through --- code_block() which escapes them. Editing a chapter later (the admin --- route) overwrites body/title in place; the WAL keeps the edit across --- restarts, which is exactly chapter 6's lesson demonstrated by the --- site that teaches it. -use view - -fn ch_hello() -> Text { - let b = el("p", "leading-relaxed mb-4", - "A writeonce program is one directory of .wo files and one entry: a free " .. "function named main. It returns the process exit code. There is no " .. "runtime to install separately and no build pipeline — woc build produces " .. "ONE self-contained binary with the VM and your bytecode inside."); - b = b .. code_block("fn main() -> Int {\n print(\"hello, writeonce\");\n return 0;\n}"); - b = b .. el("p", "leading-relaxed mt-4", - "Run it: woc build . -o hello && ./hello. " .. "Statements end with ;, blocks use braces, comments start with --."); - return b; -} - -fn ch_values() -> Text { - let b = el("p", "leading-relaxed mb-4", - "let binds a value; the type is inferred. Scalars: Int (64-bit), " .. "Float, Bool, Text (bytes, binary-safe). Text " .. "interpolates with \${...} and concatenates with ... " .. "Integer literals speak hex and binary, and the full bitwise set is here: " .. "& | ^ << >> — grouped Go-style, so a mask compare needs no parentheses."); - b = b .. code_block("let port = 8080;\nlet pi = 3.14159;\nlet name = \"writeonce\";\nlet msg = \"listening on \${port}\";\n\nlet flags = 0b1010_0001;\nlet high = flags & 0xF0; -- bitwise AND, then == compares\nlet shifted = 1 << 12; -- 4096\nif flags & 0x80 != 0 {\n print(\"top bit set\"); -- groups (flags & 0x80) != 0\n}"); - return b; -} - -fn ch_containers() -> Text { - let b = el("p", "leading-relaxed mb-4", - "Two containers: multi T (a growable list) and map<K, V>. " .. "A map read m[k] answers nil when the key is absent — the everyday idiom " .. "for optional lookups like HTTP headers. for .. in walks both."); - b = b .. code_block("let langs: multi Text = [\"c\", \"ocaml\", \"writeonce\"];\npush(langs, \"more\");\nprint(\"count \${len(langs)}\");\n\nlet ages: map = {};\nages[\"ada\"] = 36;\nlet a = ages[\"grace\"]; -- ?Int: nil, no trap\nif a == nil { print(\"unknown\"); }\n\nfor l in langs {\n print(l);\n}\nfor k, v in ages {\n print(\"\${k} is \${v}\");\n}"); - return b; -} - -fn ch_classes() -> Text { - let b = el("p", "leading-relaxed mb-4", - "Classes hold fields and methods. There are NO function values and NO closures — a " .. "deliberate doctrine: behavior travels as a class satisfying an interface, and " .. "satisfaction is structural (same method name and shape, Go-style, no " .. "implements). This is how porch, the web framework, takes handlers."); - b = b .. code_block("interface Handler {\n fn handle(req: Req) -> Resp\n}\n\nclass Hello {\n greeting: Text\n fn handle(req: Req) -> Resp {\n return ok_text(\"\${self.greeting}, \${req.path}\");\n }\n}\n\n-- any class with a matching handle() satisfies Handler\napp.get(\"/hello\", Hello { greeting: \"hi\" });"); - return b; -} - -fn ch_optionals() -> Text { - let b = el("p", "leading-relaxed mb-4", - "?T is a value or nil, and the compiler forces the check before use. " .. "Failures are TRAPS: named, catchable, never silent. try ... catch (e) is " .. "an expression; e carries code, line, method and message. Anything " .. "uncaught ends the program with the same structured report."); - b = b .. code_block("let n = parse_int(\"42x\"); -- ?Int\nif n == nil {\n print(\"not a number\");\n}\n\nlet r = try fs.read_all(\"/etc/missing\", 4096) catch (e) e.msg;\nprint(r); -- the file's bytes, or \"No such file or directory\"\n\nlet d = 0;\nlet q = try 10 / d catch (e) 0 - 1; -- DIV0 is a trap, caught here"); - return b; -} - -fn ch_tables() -> Text { - let b = el("p", "leading-relaxed mb-4", - "The database is IN the language. @table makes a class a table; " .. "insert writes a row; queries are first-class expressions; an UPDATE is a " .. "plain field assignment on a query result. With WO_DATA=<dir> every " .. "commit is WAL-durable before it is acknowledged and replays on restart — this very " .. "site stores these chapters that way, and the admin form's edits survive a kill."); - b = b .. code_block("@table(name: \"notes\", index: [tag])\nclass Note {\n tag: Text @unique\n val: Int\n}\n\ninsert Note { tag: \"first\", val: 1 };\n\nfor n in from x in Note where x.val > 0 order by x.tag select x {\n print(\"\${n.tag} = \${n.val}\");\n}\n\nlet hits = from x in Note where x.tag == \"first\" take 1 select x;\nif len(hits) == 1 {\n hits[0].val = 2; -- an update: assign through the row\n}"); - return b; -} - -fn ch_storage() -> Text { - let b = el("p", "leading-relaxed mb-4", - "Two optional @table keys decide where a table's rows LIVE. Both default " .. "to today's behaviour, so every table above keeps working untouched. " .. "durable: false keeps a table entirely in RAM — a full table in-process, " .. "same indexes, same @unique, same queries — that is simply empty after a " .. "restart. That is what a session store or a rate-limit counter wants: losing it on " .. "reboot is correct, and paying to make it durable is waste."); - b = b .. code_block("@table(name: \"sessions\", durable: false)\nclass Session {\n sid: Text @unique\n hits: Int\n}\n\n@table(name: \"catalog\", resident: keys, index: [sku])\nclass Product {\n sku: Text @unique\n price: Int\n blurb: Text\n}"); - b = b .. el("p", "leading-relaxed mt-4 mb-4", - "resident: keys is the other direction: only the KEY stays in memory. The " .. "row body lives in the write-ahead log and is read back on demand, so a table can be " .. "far larger than RAM without a page-fault cliff. Measured on a product catalogue, the " .. "resident set is 2.55× smaller than the same table fully resident. " .. "Such a table has nowhere to keep its rows without a log, so declaring it and starting " .. "without WO_DATA is REFUSED at startup rather than silently ignored."); - b = b .. el("p", "leading-relaxed mb-4", - "Reads and writes are unchanged — the storage mode is not a different API:"); - b = b .. code_block("let hits = from x in Product where x.sku == \"a-1\" take 1 select x;\nif len(hits) == 1 {\n hits[0].price = 1299; -- an ordinary assignment\n}"); - b = b .. el("p", "leading-relaxed mt-4", - "Underneath, that assignment appends a delta — which field changed, plus a " .. "back-pointer to the row's previous record — instead of rewriting the whole row. A read " .. "walks that chain backward, newest wins. Left alone the chain would grow forever on a " .. "hot row, so it is BOUNDED: past 16 links an update writes a full row image and the " .. "chain restarts at zero. A row updated a million times still costs a bounded number of " .. "record reads, on read and on replay alike."); - return b; -} - -fn ch_actors() -> Text { - let b = el("p", "leading-relaxed mb-4", - "Concurrency is actors on fibers: spawn makes an actor from a class with a " .. "receive method, send delivers one message at a time, and " .. "ownership MOVES with the message — no locks, no shared mutable state, no data races " .. "by construction. Blocking calls park the fiber; the shard serves others meanwhile. " .. "One VM per core by default; mailboxes are bounded (a full one is a catchable trap)."); - b = b .. code_block("class Counter {\n total: Int\n fn receive(msg: Tick) {\n self.total = self.total + msg.n;\n print(\"total \${self.total}\");\n }\n}\n\nclass Tick {\n n: Int\n}\n\nfn main() -> Int {\n let c: actor Tick = spawn Counter { total: 0 };\n send(c, Tick { n: 1 });\n send(c, Tick { n: 2 });\n time.sleep(50); -- parks this fiber; the actor runs\n return 0;\n}"); - return b; -} - -fn ch_deps() -> Text { - let b = el("p", "leading-relaxed mb-4", - "Dependencies are git repositories pinned in wo.toml; wo.lock " .. "records the exact revision, and locked builds work offline. The [deps] KEY names the " .. "module you use. This site has two: porch, the writeonce web framework, and the writeonce-view " .. "library that rendered the page you are reading."); - b = b .. code_block(` - [deps] - porch = { git = "https://github.com/shoneyj/porch", rev = "v0.1.0" } - view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }`); - b = b .. code_block(` - use porch - use porch/http - use view - - -- html's builders + tailwind-style utilities, zero JS, no build step: - let body = el("h1", "text-3xl font-bold", "Hello"); - return ok_html(page("Hello", body));`); - return b; -} - -fn ch_serving() -> Text { - let b = el("p", "leading-relaxed mb-4", - "The whole stack of this site: routes with :param captures, a middleware " .. "chain, handler classes, @table persistence, and server-rendered HTML — " .. "one binary behind a proxy. This is the site's own main, abbreviated:"); - b = b .. code_block("fn main(args: multi Text) -> Int {\n seed_if_empty();\n let app = App { middleware: [], routes: [] };\n app.use_mw(Mw { m: Logging {} });\n app.get(\"/\", Home {});\n app.get(\"/ch/:slug\", ShowChapter {});\n app.post(\"/admin/ch/:slug\", AdminEdit { token: token });\n return app.serve(\"127.0.0.1\", port);\n}"); - b = b .. el("p", "leading-relaxed mt-4", - "The admin route checks its bearer token in the handler — mechanism lives in the " .. "framework (bearer_token, constant-time ct_eq), POLICY stays " .. "in the app. Try editing this chapter: " .. "curl -X POST -H \"authorization: Bearer ...\" -d \"title=...&body=...\" /admin/ch/serving."); - return b; -} - --- One seed row per chapter: (ord, slug, title, body-builder above). -pub fn seed_chapters() { - insert Chapter { slug: "hello", ord: 1, title: "Hello, writeonce", body: ch_hello() }; - insert Chapter { slug: "values", ord: 2, title: "Values, Text and bitwise", body: ch_values() }; - insert Chapter { slug: "containers", ord: 3, title: "multi and map", body: ch_containers() }; - insert Chapter { slug: "classes", ord: 4, title: "Classes and interfaces", body: ch_classes() }; - insert Chapter { slug: "optionals", ord: 5, title: "Optionals and traps", body: ch_optionals() }; - insert Chapter { slug: "tables", ord: 6, title: "@table: the built-in database", body: ch_tables() }; - insert Chapter { slug: "storage", ord: 7, title: "Storage modes: durable and resident", body: ch_storage() }; - insert Chapter { slug: "actors", ord: 8, title: "Actors and fibers", body: ch_actors() }; - insert Chapter { slug: "deps", ord: 9, title: "Dependencies", body: ch_deps() }; - insert Chapter { slug: "serving", ord: 10, title: "Serving the web (this site)", body: ch_serving() }; -} diff --git a/docs/examples/site/favicon/controller.wo b/docs/examples/site/favicon/controller.wo deleted file mode 100644 index 18b8dc6..0000000 --- a/docs/examples/site/favicon/controller.wo +++ /dev/null @@ -1,13 +0,0 @@ --- favicon/controller.wo — GET /favicon.svg. The one route that answers --- something other than HTML or text, so it builds its own Resp. -use porch/http -use layout - -pub class Favicon { - fn handle(req: Req) -> Resp { - let h: map = {}; - h["content-type"] = "image/svg+xml"; - h["cache-control"] = "public, max-age=86400"; - return Resp { status: 200, headers: h, body: favicon_svg() }; - } -} diff --git a/docs/examples/site/health/controller.wo b/docs/examples/site/health/controller.wo deleted file mode 100644 index dac52b2..0000000 --- a/docs/examples/site/health/controller.wo +++ /dev/null @@ -1,9 +0,0 @@ --- health.controller.wo — GET /health: the liveness probe the accept --- script and any proxy poll. Text, not HTML, on purpose. -use porch/http - -pub class Health { - fn handle(req: Req) -> Resp { - return ok_text("ok"); - } -} diff --git a/docs/examples/site/home/controller.wo b/docs/examples/site/home/controller.wo deleted file mode 100644 index d5e16a4..0000000 --- a/docs/examples/site/home/controller.wo +++ /dev/null @@ -1,19 +0,0 @@ --- home/controller.wo — the CONTROLLER for `/`: query the model, fill the --- view components that sit beside it, answer a Resp. One feature = one --- directory = one module, view and controller together. --- --- It reaches the chapter nav through `use chapter` and the query through --- the model's `Chapters.links()` static — a class crosses module lines, --- a free fn does not. -use porch/http -use layout -use chapter - -pub class Home { - fn handle(req: Req) -> Resp { - let nav = ChapterNav { items: Chapters.links(), current: 0 }; - let page = HomePage { chapter_nav: nav }; - let shell = wide_shell("writeonce — learn the language", page.render()); - return ok_html(shell.render()); - } -} diff --git a/docs/examples/site/home/view.wo b/docs/examples/site/home/view.wo deleted file mode 100644 index ae853d7..0000000 --- a/docs/examples/site/home/view.wo +++ /dev/null @@ -1,53 +0,0 @@ --- home/view.wo — the VIEW for `/`. A component: fields in, Text out. --- Everything on this page is static copy EXCEPT the chapter list, so --- the one field is that list's already-built component — content --- projection, the same slot pattern writeonce-view's `Layout` uses. HomePage --- therefore knows nothing about chapters, the Chapter table, or how the --- nav decides which entry is current. -use view - -pub class HomePage { - chapter_nav: Component - - fn render() -> Text { - -- hero: tagline + the two CTAs (the go.dev shape, no JS anywhere) - let h1 = el("h1", "text-4xl font-bold mb-4", "One language. One runtime.
One database. One binary."); - let sub = el("p", "text-lg text-gray-700 leading-relaxed mb-6", "writeonce is a language whose compiler, runtime, web server and " .. "database ship as a single never-stopping Linux binary. Ownership-" .. "checked memory, inferred GC where ownership cannot reach, actors " .. "on every core — and the page you are reading is served by it."); - let ctas = el("div", "flex items-center justify-center gap-4", btn_link("/ch/hello", "Get started", true) .. btn_link("https://github.com/shoneyJ/writeonce", "View source", false)); - let hero = el("div", "text-center py-16", h1 .. sub .. ctas); - - -- code showcase: a real flavor of the language - let show_head = el("h2", "text-2xl font-bold mb-2 text-center", "An actor per chat room, rows in the built-in database"); - let show_cap = el("p", "text-sm text-gray-500 text-center mb-4", "No broker, no ORM, no async keyword — ownership moves the message, the WAL makes the row durable."); - let showcase = el("div", "mx-auto max-w-3xl mb-8", show_head .. show_cap .. home_snippet()); - - -- why-cards (2x2 grid, collapses on small screens) - let cards = card("One binary", "woc build emits a self-contained executable: VM, your bytecode, the database engine. Deploys are a file copy; the runtime swaps code in place."); - cards = cards .. card("Memory safety, no tax", "Rust-shaped ownership checked at compile time; where ownership cannot express the shape, the compiler infers GC — per shard, no global pause."); - cards = cards .. card("The database is built in", "Every class is a table. Inserts are WAL-logged before they acknowledge; restart replays. No server to operate, no connection string — and a table can declare itself RAM-only, or keep only its keys in memory and outgrow RAM."); - cards = cards .. card("Actors on every core", "spawn returns an address, send moves ownership. Fibers park on io_uring instead of blocking threads — no async/await, ever."); - let grid = el("div", "grid grid-cols-2 gap-6 mb-8", cards); - - -- chapters (the gate's anchor string lives here) - let learn = el("h2", "text-2xl font-bold mb-4", "Learn writeonce"); - let learn_p = el("p", "leading-relaxed mb-4", "The tutorial is written in the language and stored in its tables — work through the chapters in order:"); - let chapters = el("div", "bg-white rounded-lg border shadow-sm p-6 mb-8", learn .. learn_p .. self.chapter_nav.render()); - - return hero .. showcase .. grid .. chapters; - } -} - --- The homepage's code showcase: a real flavor of the language — an --- actor per chat room, rows in the built-in database, one binary. --- Page copy, so it lives with the page, not with the seed data. -fn home_snippet() -> Text { - let s = "@table\nclass Message {\n room: Text\n body: Text\n}\n\n"; - s = s .. "class Room {\n name: Text\n fn receive(msg: Post) {\n"; - s = s .. " insert Message { room: self.name, body: msg.body };\n"; - s = s .. " print(\"[\${self.name}] \${msg.body}\");\n }\n}\n\n"; - s = s .. "fn main() -> Int {\n"; - s = s .. " let general: actor Post = spawn Room { name: \"general\" };\n"; - s = s .. " send(general, Post { body: \"hello, writeonce\" });\n"; - s = s .. " time.sleep(50);\n return 0;\n}"; - return code_block(s); -} diff --git a/docs/examples/site/install/controller.wo b/docs/examples/site/install/controller.wo deleted file mode 100644 index cd86c80..0000000 --- a/docs/examples/site/install/controller.wo +++ /dev/null @@ -1,12 +0,0 @@ --- install/controller.wo — GET /install. Nothing to query: the page is --- static copy, so the controller only wraps it in the shell. -use porch/http -use layout - -pub class ShowInstall { - fn handle(req: Req) -> Resp { - let page = InstallPage {}; - let shell = reading_shell("writeonce — install", page.render()); - return ok_html(shell.render()); - } -} diff --git a/docs/examples/site/install/view.wo b/docs/examples/site/install/view.wo deleted file mode 100644 index ebad827..0000000 --- a/docs/examples/site/install/view.wo +++ /dev/null @@ -1,110 +0,0 @@ --- install/view.wo — the VIEW for /install. Static copy: no fields, so --- the component has none. It is still a component, and still renders --- through the same interface as every other page. -use view - -pub class InstallPage { - fn render() -> Text { - let head = ` -

Install writeonce

-

Two native binaries — woc, - the compiler, and wovm, the runtime VM. Both depend only on - the system C library. There is no package manager to install, no - language runtime to install on the machines you deploy to, and no - build toolchain beyond these two files.

`; - - let sys = supported(); - - let s1 = section("1. Download", - `

One tarball, two binaries. Take it from - the GitHub release, or from this site as a mirror — they are the same - bytes, and the checksum below proves it:

-

- Download 0.1.0 (linux-amd64) - Mirror -

-

Verify it before you extract it. The - expected digest is served beside the archive at - .sha256:

`, - code_block("curl -O https://writeonce.de/dl/writeonce-0.1.0-linux-amd64.tar.gz\ncurl -O https://writeonce.de/dl/writeonce-0.1.0-linux-amd64.tar.gz.sha256\nsha256sum -c writeonce-0.1.0-linux-amd64.tar.gz.sha256")); - - let s2 = section("2. Extract it", - `

Extract into /usr/local, - replacing any previous install. Run this as root, or through - sudo:

`, - code_block("rm -rf /usr/local/writeonce\ntar -C /usr/local -xzf writeonce-0.1.0-linux-amd64.tar.gz")); - - let s3 = section("3. Put it on your PATH", - `

Add one line to your - $HOME/.profile (or /etc/profile for every user - on the box), then restart your shell:

`, - code_block("export PATH=$PATH:/usr/local/writeonce/bin")); - - let s4 = section("4. Check it", - `

Both should print the same version. - woc finds wovm beside itself, so a tarball - install needs no further configuration.

`, - code_block("woc version # writeonce 0.1.0 linux/amd64\nwovm --version # wovm 0.1.0")); - - let s5 = section("5. Your first project", - `

A project is a directory with a - wo.toml manifest and one or more .wo files. - Nothing else — no lockfile to create by hand, no scaffolding step.

`, - code_block("mkdir hello && cd hello\n\ncat > wo.toml <<'EOF'\nname = \"hello\"\nversion = \"0.1.0\"\n\n[runtime]\nwo = \">= 0.1\"\nEOF\n\ncat > main.wo <<'EOF'\nfn main() -> Int {\n print(\"hello, writeonce\");\n return 0;\n}\nEOF")); - - let s6 = section("6. Build and run", - `

woc <dir> emits ONE - standalone binary at target/<name>. Copy that file to a - server and run it — the VM is inside it, and so is the database.

`, - code_block("woc .\n./target/hello # hello, writeonce")); - - let s7 = section("7. Add a dependency", - `

Dependencies are git repositories pinned - by revision. The [deps] KEY is the module name your code - uses. woc writes a wo.lock with the - exact revision, and a locked build works offline. See - Packages for what is available.

`, - code_block("[deps]\nview = { git = \"https://github.com/shoneyj/writeonce-view\", rev = \"v0.1.0\" }")); - - let note = el("div", "bg-white rounded-lg border shadow-sm p-6 mt-8", - el("h2", "text-lg font-bold mb-2", "Where things go") .. - `

The tarball installs to - /usr/local/writeonce: bin/woc, - bin/wovm, and a VERSION file. To point - woc at a different VM, set $WO_RUNTIME or add a - [build] runtime = "..." key to wo.toml. A - manifest may also require a minimum toolchain with - [runtime] wo = ">= 0.1"; woc refuses to build - a project that needs a newer toolchain than itself.

`); - - return head .. sys .. s1 .. s2 .. s3 .. s4 .. s5 .. s6 .. s7 .. note; - } -} - --- What the release actually runs on. Every claim here is read off the --- shipped binaries (`file`, and the highest GLIBC_ symbol version they --- import), not off a wish list — an install page that overstates its --- support costs someone an afternoon. -fn supported() -> Text { - return ` -
-

Supported systems

-
    -
  • Linux on x86-64 — the only target built today. There is no ARM, macOS or Windows build.
  • -
  • glibc 2.35 or newer. The binaries link the system C library dynamically: woc imports symbols up to GLIBC_2.35 and wovm up to GLIBC_2.34, read off the published v0.1.0 release. That covers Ubuntu 22.04+, Debian 12+ and Fedora 36+. On RHEL 9 (2.34) the runtime works but the compiler does not — build elsewhere and copy the binary over, which is the point of a self-contained executable.
  • -
  • Not musl. Alpine needs a build against musl, which does not exist yet.
  • -
  • Nothing else. No JVM, no Node, no Python, no package manager. The two binaries and libc are the whole dependency list.
  • -
-

Check yours with - ldd --version. If it is older than 2.35, build from source - until a wider-compatibility release exists. The floor is set by the - machine that BUILT the release, not by the language — the CI runner is - pinned to Ubuntu 22.04 to keep it this low.

-
`; -} - --- One numbered step: heading, prose, and the commands to run. -fn section(title: Text, prose: Text, code: Text) -> Text { - return el("div", "mb-8", - el("h2", "text-2xl font-bold mb-2", title) .. prose .. code); -} diff --git a/docs/examples/site/layout/app.wo b/docs/examples/site/layout/app.wo deleted file mode 100644 index cb17852..0000000 --- a/docs/examples/site/layout/app.wo +++ /dev/null @@ -1,49 +0,0 @@ --- layout/app.wo — the app shell: one component wrapping every page with --- the shared header and footer. Unlike the shop template, this site --- INLINES its stylesheet (writeonce-view's `page()` does that), so the shell --- fills writeonce-view's own `Layout` rather than writing its own document. --- --- The only thing that varies between pages is the container width, so --- that is the one extra slot — and the two widths are named once, here, --- instead of as class strings scattered through the controllers. -use porch/http -use view - -pub class AppShell { - title: Text - container: Text - content: Text - - fn render() -> Text { - let l = Layout { - title: self.title, - head: head_links(), - nav: header(), - content: el("div", self.container, self.content), - footer: footer() - }; - return l.render(); - } -} - --- Chapter pages keep a reading width. -pub fn reading_shell(title: Text, content: Text) -> AppShell { - return AppShell { title: title, container: "mx-auto max-w-3xl px-4 py-8", content: content }; -} - --- The homepage uses the wider container (the go.dev shape). -pub fn wide_shell(title: Text, content: Text) -> AppShell { - return AppShell { title: title, container: "mx-auto max-w-5xl px-4", content: content }; -} - --- Every non-200 HTML answer goes through here, so an error page is a --- real page: same chrome, same shell, just a different status. -pub fn html_error(status: Int, title: Text, msg: Text) -> Resp { - let content = el("h1", "text-2xl font-bold mb-4", title) .. - el("p", "", msg) .. - el("p", "", link("/", "", "Back to the chapters")); - let shell = reading_shell("writeonce — ${title}", content); - let h: map = {}; - h["content-type"] = "text/html; charset=utf-8"; - return Resp { status: status, headers: h, body: shell.render() }; -} diff --git a/docs/examples/site/layout/footer.wo b/docs/examples/site/layout/footer.wo deleted file mode 100644 index 552c765..0000000 --- a/docs/examples/site/layout/footer.wo +++ /dev/null @@ -1,6 +0,0 @@ --- layout/footer.wo — the site footer, shared by every page. -use view - -pub fn footer() -> Text { - return el("div", "footer", "writeonce.de — served by the language it teaches. " .. "One binary: compiler, runtime, database, this page."); -} diff --git a/docs/examples/site/layout/header.wo b/docs/examples/site/layout/header.wo deleted file mode 100644 index 8b28ccd..0000000 --- a/docs/examples/site/layout/header.wo +++ /dev/null @@ -1,13 +0,0 @@ --- layout/header.wo — the site navigation bar, shared by every page. -use view - -pub fn header() -> Text { - let links = link("/install", "text-gray-700", "Install"); - links = links .. link("/ch/hello", "text-gray-700", "Tutorial"); - links = links .. link("/packages", "text-gray-700", "Packages"); - links = links .. link("https://github.com/shoneyJ/writeonce", "text-gray-700", "GitHub"); - -- The brand is the mark plus the wordmark, one inline-flex row so the - -- tile and the text share a baseline at any font size. - let brand = el("span", "flex items-center gap-2", logo_svg(28) .. "writeonce.de"); - return nav_bar("/", brand, links); -} diff --git a/docs/examples/site/layout/logo.wo b/docs/examples/site/layout/logo.wo deleted file mode 100644 index 1c2f2aa..0000000 --- a/docs/examples/site/layout/logo.wo +++ /dev/null @@ -1,23 +0,0 @@ --- layout/logo.wo — the mark, authored as inline SVG so it needs no asset --- pipeline and no second request: a dark tile with a two-stroke "W", the --- first half white and the second the site's accent blue. One glyph, two --- colours, still legible at 16px. --- --- One source, two consumers: the nav brand embeds it, and --- `favicon/controller.wo` serves the same bytes at /favicon.svg. - -pub fn logo_svg(px: Int) -> Text { - return ``; -} - --- The favicon is the same mark without intrinsic width/height, so the --- browser scales it to whatever the tab needs. -pub fn favicon_svg() -> Text { - return ``; -} - --- What goes in . Inline SVG, no request, no cache question. -pub fn head_links() -> Text { - return ` - `; -} diff --git a/docs/examples/site/main.wo b/docs/examples/site/main.wo deleted file mode 100644 index fe3f66a..0000000 --- a/docs/examples/site/main.wo +++ /dev/null @@ -1,79 +0,0 @@ --- site — writeonce.de: the language tutorial, served BY the language. --- Full stack in one binary: porch ([deps]) for HTTP/routing/ --- auth, writeonce-view ([deps]) for server-rendered pages with Tailwind-style --- utilities, @table + WAL for the chapters themselves. The site is its own --- final chapter: /ch/serving shows this file's shape. --- --- SITE_TOKEN=... WO_DATA=./data ./site 8080 --- SITE_HOST=0.0.0.0 ... ./site 8080 (reachable from the network) --- WO_DIST=/srv/dist ... (where /dl serves tarballs from) --- --- Behind nginx/caddy for writeonce.de: the proxy terminates TLS and --- forwards to 127.0.0.1:8080 (the framework speaks HTTP/1.1 keep-alive). --- --- This file is the BOOTSTRAP and nothing else: seed, routes, serve. The --- model is types.wo; every feature is a directory holding its view and --- its controller. -use env -use porch -use porch/http -use porch/router -use home -use chapter -use install -use packages -use admin -use health -use favicon - -fn main(args: multi Text) -> Int { - if len(args) < 1 { - print_err("usage: site (SITE_TOKEN gates /admin; WO_DATA makes chapters durable)"); - return 2; - } - let port = parse_int(args[0]); - if port == nil { - print_err("site: must be a number"); - return 2; - } - let token = env.get("SITE_TOKEN"); - if token == nil { - print_err("site: SITE_TOKEN is required (the admin route's bearer token)"); - return 2; - } - - -- Bind to loopback unless SITE_HOST says otherwise. Behind a proxy - -- loopback is right; SITE_HOST=0.0.0.0 (or a LAN address) is how you - -- reach it from another machine while developing. - let host = "127.0.0.1"; - let h = env.get("SITE_HOST"); - if h != nil { - host = "${h}"; - } - - -- Where the release tarballs live. `just dist` writes them to ./dist; - -- a deployment points WO_DIST at wherever it keeps them. - let dist = "dist"; - let d = env.get("WO_DIST"); - if d != nil { - dist = "${d}"; - } - - seed_if_empty(); - - let app = App { middleware: [], routes: [] }; - app.use_mw(Mw { m: Logging {} }); - app.get("/", Home {}); - app.get("/install", ShowInstall {}); - app.get("/packages", ShowPackages {}); - app.get("/packages/:name", ShowPackage {}); - app.get("/health", Health {}); - app.get("/favicon.svg", Favicon {}); - -- 16 MiB ceiling: the toolchain tarball is under 1 MiB today, and - -- `max_bytes` is a hard truncation point, not a hint. - app.get("/dl/*path", StaticFiles { dir: dist, max_bytes: 16777216 }); - app.get("/ch/:slug", ShowChapter {}); - app.post("/admin/ch/:slug", AdminEdit { token: "${token}" }); - print_err("site: listening on ${host}:${port}"); - return app.serve(host, port); -} diff --git a/docs/examples/site/packages/controller.wo b/docs/examples/site/packages/controller.wo deleted file mode 100644 index 311c56c..0000000 --- a/docs/examples/site/packages/controller.wo +++ /dev/null @@ -1,90 +0,0 @@ --- packages/controller.wo — GET /packages and /packages/:name. --- --- The catalogue is static data, not rows, so it lives here on the DATA --- side of the feature rather than in the view: the components are handed --- values exactly as they would be if this were a table one day. -use porch/http -use view -use layout - -typedef PackageInfo = { - name: Text, - summary: Text, - git: Text, - rev: Text, - what: Text, - usage: Text -} - -pub class ShowPackages { - fn handle(req: Req) -> Resp { - let cards: multi Component = []; - for p in catalogue() { - push(cards, PackageCard { name: p.name, summary: p.summary }); - } - let page = PackagesPage { cards: cards }; - let shell = reading_shell("writeonce — packages", page.render()); - return ok_html(shell.render()); - } -} - -pub class ShowPackage { - fn handle(req: Req) -> Resp { - let name = req.params["name"]; - if name == nil { - return html_error(404, "No such package", "The address names no package."); - } - for p in catalogue() { - if p.name == name { - let page = PackagePage { - name: p.name, summary: p.summary, git: p.git, rev: p.rev, - what: p.what, usage: p.usage - }; - let shell = reading_shell("writeonce — ${p.name}", page.render()); - return ok_html(shell.render()); - } - } - return html_error(404, "No such package", "Nothing is published under that name."); - } -} - --- The two libraries this site runs on. Both are `kind = "library"` --- projects: no `fn main`, imported through `[deps]`. -fn catalogue() -> multi PackageInfo { - let out: multi PackageInfo = []; - - push(out, PackageInfo { - name: "porch", - summary: "The writeonce web framework: an HTTP/1.1 keep-alive server core, a router with :param captures, and Handler/Middleware structural interfaces. Imported as use porch.", - git: "https://github.com/shoneyj/porch", - rev: "v0.1.0", - what: ` -
    -
  • App — the route table and the middleware chains; app.get/post/delete_, use_mw, use_after, mount, serve.
  • -
  • StaticFiles — serve a directory over a wildcard route. Traversal is refused, not normalised; max_bytes is a hard ceiling. This site's /dl downloads run through it.
  • -
  • Handler, Middleware, After — structural interfaces. A handler is a CLASS; its fields are the closure this language does not have.
  • -
  • Req/Resp plus builders: ok_text, ok_html, ok_json, created_json, not_found, bad_request, unauthorized, conflict, redirect.
  • -
  • Auth MECHANISM only — bearer_token, basic_credentials, constant-time ct_eq. Which routes are gated stays your policy.
  • -
  • Bodies: form_values, multipart_parts, content negotiation, ETags, security headers, CORS, WebSocket frames.
  • -
`, - usage: code_block("use porch\nuse porch/http\nuse porch/router\n\nclass Hello {\n fn handle(req: Req) -> Resp {\n return ok_text(\"hello\");\n }\n}\n\nfn main(args: multi Text) -> Int {\n let app = App { middleware: [], routes: [] };\n app.use_mw(Mw { m: Logging {} });\n app.get(\"/\", Hello {});\n return app.serve(\"127.0.0.1\", 8080);\n}") - }); - - push(out, PackageInfo { - name: "view", - summary: "Server-rendered HTML as plain Text: escaping, element builders, a component layer, and a Tailwind-style utility stylesheet inlined into every page.", - git: "https://github.com/shoneyj/writeonce-view", - rev: "v0.1.0", - what: ` -
    -
  • esc() — escapes & < > ". A {{ }} hole in a raw text literal compiles to a call to it, so display data is escaped by construction. (Written with entities here: inside a raw literal, a real {{ would BE a hole.)
  • -
  • Component — a class with fields and fn render() -> Text, satisfied structurally. multi Component holds children directly; render_all renders them in order.
  • -
  • Layout — content projection: title, head, nav, content and footer as pre-rendered slots.
  • -
  • Builders: el, link, card, nav_bar, code_block, form_post, text_input, text_area, submit_btn, btn_link.
  • -
  • tw_css() + page() — one hand-written utility sheet, inlined, so a page is one self-contained response. No CDN, no build step, no JS.
  • -
`, - usage: code_block("use view\n\nclass Card {\n name: Text\n fn render() -> Text {\n return `\n
\n

{{ self.name }}

\n
`;\n }\n}\n\n-- in a handler:\nlet c = Card { name: user_supplied };\nreturn ok_html(page(\"Hello\", c.render()));") - }); - - return out; -} diff --git a/docs/examples/site/packages/view.wo b/docs/examples/site/packages/view.wo deleted file mode 100644 index 242495c..0000000 --- a/docs/examples/site/packages/view.wo +++ /dev/null @@ -1,61 +0,0 @@ --- packages/view.wo — the VIEW for /packages and /packages/:name. --- --- Three components: a card for the index, the index itself (holding its --- cards as CHILDREN through the structural interface), and the detail --- page. None of them knows where the catalogue came from. -use view - -pub class PackageCard { - name: Text - summary: Text - - fn render() -> Text { - return ` -
-

{{ self.name }}

-

{{ self.summary }}

-
`; - } -} - -pub class PackagesPage { - cards: multi Component - - fn render() -> Text { - return ` -

Packages

-

A package is a git repository with a - wo.toml that says kind = "library". There is no - registry and no publish step: you depend on a URL and a revision, and - wo.lock pins exactly what you built against. These are the - libraries this site itself is built on.

-
${render_all(self.cards)}
`; - } -} - -pub class PackagePage { - name: Text - summary: Text - git: Text - rev: Text - what: Text - usage: Text - - fn render() -> Text { - let dep = code_block("[deps]\n" .. self.name .. " = { git = \"" .. self.git .. "\", rev = \"" .. self.rev .. "\" }"); - let head = ` -

{{ self.name }}

-

{{ self.summary }}

-

Install

-

Add it to your wo.toml. The - KEY is the module name — that is what use imports.

`; - let what = ` -

What you get

- ${self.what}`; - let usage = ` -

Usage

- ${self.usage}`; - let back = el("p", "mt-8", link("/packages", "", "← all packages")); - return head .. dep .. what .. usage .. back; - } -} diff --git a/docs/examples/site/types.wo b/docs/examples/site/types.wo deleted file mode 100644 index b1cb9fb..0000000 --- a/docs/examples/site/types.wo +++ /dev/null @@ -1,52 +0,0 @@ --- types.wo — the MODEL. Every @table class IS a WAL-backed table: rows --- persist under WO_DATA and replay on restart; without WO_DATA the --- store is RAM-only. Nothing else lives here — no rendering, no request --- handling. --- --- Root module by NECESSITY, not choice: `pub` and `@table` cannot --- combine yet (recorded language gap), so tables cannot be exported to --- other modules — everything that queries them (the controllers) lives --- in the root module too. - --- Every chapter is a row: slug is the URL, ord orders the nav, body is a --- server-rendered HTML fragment. Edits (the admin route) persist through --- the WAL under WO_DATA and replay on restart. -@table(name: "chapters", index: [slug]) -class Chapter { - slug: Text @unique - ord: Int - title: Text - body: Text -} - --- One nav entry: a PROJECTION of a Chapter row, not the row itself. It --- lives with the model because that is what it is — the views merely --- consume it, and never hold a database handle. -typedef ChapterLink = { ord: Int, title: Text, slug: Text } - --- The chapter queries, on a class so every feature module can reach --- them: a free `fn` is scoped to the module that declares it (WO-E210), --- but a CLASS — and its statics — is reachable across module lines. --- That is what lets `home/` and `chapter/` share one query without one --- importing the other. -class Chapters { - -- Every chapter as a nav entry, ordered. Two pages call this. - static fn links() -> multi ChapterLink { - let items: multi ChapterLink = []; - for c in from x in Chapter order by x.ord select x { - push(items, ChapterLink { ord: c.ord, title: c.title, slug: c.slug }); - } - return items; - } -} - --- First boot only: an empty table gets the tutorial (content.wo). -fn seed_if_empty() { - let n = 0; - for c in from x in Chapter take 1 select x { - n = n + 1; - } - if n == 0 { - seed_chapters(); - } -} diff --git a/docs/examples/site/wo.toml b/docs/examples/site/wo.toml deleted file mode 100644 index 1ce7a92..0000000 --- a/docs/examples/site/wo.toml +++ /dev/null @@ -1,13 +0,0 @@ -name = "site" -version = "0.1.0" -description = "writeonce.de — the language tutorial served by the language: framework + writeonce-view [deps], @table chapters, server-rendered pages" - -[runtime] -wo = ">= 0.1" - -# Two real dependencies (the gate substitutes file:// remotes built from -# docs/examples/porch and docs/examples/writeonce-view, so CI never -# touches the network). The [deps] KEY is the module name `use` imports. -[deps] -porch = { git = "https://github.com/shoneyj/porch", rev = "v0.1.0" } -view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" } diff --git a/docs/guides/releasing.md b/docs/guides/releasing.md index 82a0e48..857057d 100644 --- a/docs/guides/releasing.md +++ b/docs/guides/releasing.md @@ -72,6 +72,12 @@ ignored by this workflow. Publishing binaries whose floor differs from the page is the one failure a user cannot debug. + docs/examples/site is a SUBMODULE (github.com/shoneyJ/writeonce-site), + so this edit is a commit in THAT repo, pushed there, and then a + second commit here moving the submodule pointer. Editing the files + and committing only in this repo records nothing — the change lives + in a directory this repo tracks by revision, not by content. + 10 Ship for real: git tag -a v0.1.0 -m "writeonce 0.1.0" git push origin v0.1.0 @@ -404,7 +410,9 @@ archive. updating every place the site names the current release: `docs/examples/site/install/view.wo` (the download links, the tar command, and the `.sha256` link). The version appears there as literal - text, so grep for the old number before you publish. + text, so grep for the old number before you publish. That file is in + the `writeonce-site` submodule — commit and push it there, then bump + the pointer here. - **One target today.** `mkdist.sh` builds `linux-amd64` only; a cross matrix is future work. Do not add architectures to the release notes that no build produces. diff --git a/scripts/site-accept.sh b/scripts/site-accept.sh index 640c111..4af4378 100755 --- a/scripts/site-accept.sh +++ b/scripts/site-accept.sh @@ -18,6 +18,16 @@ if [[ ! -x "$WOC" || ! -x "$WOVM" ]]; then exit 1 fi +# docs/examples/site is a SUBMODULE (github.com/shoneyJ/writeonce-site). A clone +# without --recurse-submodules leaves it an empty directory, and the copy below +# would then "succeed" into an empty app and fail much later as a build error +# that says nothing about the real cause. Say the real cause here. +if [[ ! -f "$ROOT/docs/examples/site/main.wo" ]]; then + echo "site-accept: docs/examples/site is empty — it is a submodule." >&2 + echo " run: git submodule update --init docs/examples/site" >&2 + exit 1 +fi + W="$(mktemp -d "${TMPDIR:-/tmp}/site-accept.XXXXXX")" SRV="" cleanup() {