feat(serve+view): file serving, downloads, supported systems; rename
- rename the two libraries: writeonce-framework -> writeonce-serve
(`use serve`), wo-html -> writeonce-view (`use view`). Names say the
ROLE now; every sample, script, gate and live doc follows
- stories/specs/plans keep the old names: they are dated records, and
both library READMEs carry a "renamed 2026-08-25" note
- serve/http/files.wo: StaticFiles { dir, max_bytes } — traversal
refused not normalised, extension content types, attachment
disposition for archives. Lifted out of the shop, which had said in
a comment that it belonged in the framework
- shop drops its private copy and mounts the framework's
- site: /dl/*path over $WO_DIST (default ./dist), 16 MiB ceiling
- /install gains supported systems — Linux x86-64, glibc >= 2.38,
not musl — read off `file` and the binaries' GLIBC_ symbol
versions, not off a wish list; plus GitHub release as primary,
/dl as mirror, and the sha256 verify step
- site-accept: 17 -> 21 checks (supported systems, gzip download with
a binary-safe probe, checksum, /dl traversal 404)
Verified on 192.168.0.165: the real 960,820-byte tarball downloads
as application/gzip and its sha256 matches the published digest.
Gates: oop-accept MET, site 21/0, web-app 46/0, fibers 10/0,
db-actor 8/0; shop rebuilt and its /assets served by the framework.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
24245438cf
commit
47a920f19a
62 changed files with 310 additions and 158 deletions
|
|
@ -269,7 +269,7 @@ dependencies, declared in the manifest:
|
|||
|
||||
```toml
|
||||
[deps]
|
||||
niceframework = { git = "https://github.com/shoneyj/niceframework", rev = "v0.1.0" }
|
||||
niceserve = { git = "https://github.com/shoneyj/niceframework", rev = "v0.1.0" }
|
||||
```
|
||||
|
||||
`woc` fetches each dep (via the `git` binary) into `.wo-deps/<name>/`, pins
|
||||
|
|
@ -303,7 +303,7 @@ acceptance tests:
|
|||
just employee # compile + run every mode against a durable database
|
||||
```
|
||||
|
||||
- **`docs/examples/writeonce-framework/` + `docs/examples/web-app/`** — a web
|
||||
- **`docs/examples/writeonce-serve/` + `docs/examples/web-app/`** — a web
|
||||
framework written in writeonce (HTTP/1.1 behind a TLS-terminating proxy,
|
||||
router with `:param` captures, interface-based handlers) and a storefront
|
||||
consuming it **as a `[deps]` dependency**, with `@table` persistence. Run:
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ Things worth knowing before editing them:
|
|||
- **`{{ e }}` desugars to `esc(${e})`, resolved by ordinary name
|
||||
lookup.** `desugar_interp` in `parser.ml` builds a `Call` on an
|
||||
`Ident "esc"` — precisely what a developer wrote by hand before. The
|
||||
compiler learns nothing about HTML, `esc` stays wo-html's ordinary
|
||||
compiler learns nothing about HTML, `esc` stays writeonce-view's ordinary
|
||||
`pub fn`, a typo'd field inside the hole is a normal name/type error,
|
||||
and a locally defined `esc` shadows deliberately (a custom escaper is
|
||||
a feature). `${ }` inside the same literal stays raw — that is the
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ the number or produce the benchmark.
|
|||
| 22's battery never run | 22 is ⬜ "needs a spec first"; no `bench/baseline.json`, no `just db-bench`; `runtime/bench/` is the retired C prototype's harness |
|
||||
| TSan covers one demo | only `scripts/fibers-accept.sh` builds and runs `wovm_tsan` |
|
||||
| no fuzzing, no CI | no `.github/`, no fuzz target |
|
||||
| one framework, five samples, one consumer | exact: `writeonce-framework`; employee, employee-list, fibers, gc-cycle, log-watcher; `web-app` |
|
||||
| one framework, five samples, one consumer | exact: `writeonce-serve`; employee, employee-list, fibers, gc-cycle, log-watcher; `web-app` |
|
||||
|
||||
### Consequence
|
||||
|
||||
|
|
|
|||
|
|
@ -38,19 +38,19 @@ fn render() -> Text {
|
|||
}
|
||||
```
|
||||
|
||||
Every `render()` makes its class a **component** — wo-html's structural
|
||||
Every `render()` makes its class a **component** — writeonce-view's structural
|
||||
`Component` interface, satisfied by having the method, never declared.
|
||||
Parent components hold children directly (`cards: multi Component`) and
|
||||
render them with `render_all`, so `ProductListPage` knows nothing about
|
||||
`ProductCard` beyond `render()`. The document itself is a component too:
|
||||
`AppShell { title, content }` in `layout/app.wo`, which links a real
|
||||
stylesheet rather than inlining one — that is why it is its own shell and
|
||||
not wo-html's `Layout`.
|
||||
not writeonce-view's `Layout`.
|
||||
|
||||
Two holes, and the difference is the whole escaping story:
|
||||
|
||||
- `{{ expr }}` **HTML-escapes** — it compiles to a call to the `esc` in
|
||||
scope (wo-html's, unless the app declares its own). Display data goes
|
||||
scope (writeonce-view's, unless the app declares its own). Display data goes
|
||||
here; a typo'd field is a compile error, not a broken page.
|
||||
- `${ expr }` is **raw** — for markup you built yourself, like the
|
||||
`${stock}` fragment above or `${content}` in the app shell.
|
||||
|
|
@ -70,7 +70,7 @@ up, or sandbox.
|
|||
| `product_list/view.wo` | VIEW — classes with `fn render() -> Text`, fields = exactly what is displayed | `product-list/view.html` |
|
||||
| `product_list/controller.wo` | CONTROLLER — query the model, fill the view, answer a `Resp`; beside its view in the same module | component `.ts` + service |
|
||||
| `product_page/`, `orders/` | one module per feature: `view.wo` + `controller.wo` | feature folders |
|
||||
| `static_files/controller.wo` | `/assets/*` from disk, traversal-safe, typed | `angular.json` assets |
|
||||
| `/assets/*` | served by the framework's `StaticFiles` — the template no longer carries its own copy | `angular.json` assets |
|
||||
| `assets/style.css` | ONE real stylesheet, sectioned per feature | the `.scss` files |
|
||||
| the `render()` bodies | ONE raw text literal each: real newlines, real double-quoted attributes, source indentation removed at compile time, `${}` raw holes and `{{ }}` auto-escaping ones | Vue `<template>` (in-SFC) |
|
||||
| `layout/app.wo` → `AppShell` | the document, as a two-slot component | `app.component.html` |
|
||||
|
|
@ -94,12 +94,12 @@ it looks like.
|
|||
this directory was its consumer. Styles stay a real CSS file, served
|
||||
statically (there is no scss preprocessor).
|
||||
- **No closures, no DI.** A view is a class with fields + `render()`
|
||||
(wo-html's `Component`); a controller is a class satisfying `Handler`.
|
||||
(writeonce-view's `Component`); a controller is a class satisfying `Handler`.
|
||||
Capture = a field.
|
||||
- **The MVC seam is enforced by where the query sits.** Controllers hold
|
||||
every `from … select`; a view receives VALUES — for the list page, a
|
||||
`multi Component` of already-filled cards. No view in this template
|
||||
touches the database, and wo-html contains no query at all.
|
||||
touches the database, and writeonce-view contains no query at all.
|
||||
- **No sessions/cart yet.** Buying is per-product (qty → order). A cart
|
||||
needs a session story that does not exist yet.
|
||||
- **No client-side JS.** Every interaction is a form round trip.
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
-- wrapping every page with the shared header and footer. Styles are NOT
|
||||
-- inlined — pages link /assets/style.css (served by static_files), so
|
||||
-- markup and styling stay separate files.
|
||||
use framework/http
|
||||
use html
|
||||
use serve/http
|
||||
use view
|
||||
|
||||
-- The app shell as a COMPONENT (wo-html's `Component`: fields in, Text
|
||||
-- out), the same shape as wo-html's own `Layout` — two slots instead of
|
||||
-- The app shell as a COMPONENT (writeonce-view's `Component`: fields in, Text
|
||||
-- out), the same shape as writeonce-view's own `Layout` — two slots instead of
|
||||
-- four, and its own document rather than `page()`'s, because this
|
||||
-- template deliberately LINKS a stylesheet instead of inlining one.
|
||||
-- That is the whole reason it is not just `Layout`: markup and styling
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
--
|
||||
-- WO_DATA=./data ./target/shop 8080 (run from the shop directory:
|
||||
-- /assets/* serves from ./assets)
|
||||
use framework
|
||||
use framework/router
|
||||
use serve
|
||||
use serve/http
|
||||
use serve/router
|
||||
use product_list
|
||||
use product_page
|
||||
use orders
|
||||
use static_files
|
||||
|
||||
fn seed_if_empty() {
|
||||
let n = 0;
|
||||
|
|
@ -44,6 +44,8 @@ fn main(args: multi Text) -> Int {
|
|||
app.get("/p/:sku", ShowProduct {});
|
||||
app.post("/orders/:sku", CreateOrder {});
|
||||
app.get("/orders", ListOrders {});
|
||||
app.get("/assets/*path", StaticFiles { dir: "assets" });
|
||||
-- the framework's file server (serve/http): 2 MiB ceiling is well
|
||||
-- above this template's stylesheet.
|
||||
app.get("/assets/*path", StaticFiles { dir: "assets", max_bytes: 2097152 });
|
||||
return app.serve("127.0.0.1", port);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
-- orders/controller.wo — the buying flow: stock-checked order creation
|
||||
-- (decrement + insert are each WAL-committed before they acknowledge)
|
||||
-- and the orders list (ref navigation: o.product.name).
|
||||
use framework/http
|
||||
use serve/http
|
||||
use layout
|
||||
use time
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
-- orders/view.wo — the confirmation page and the orders table.
|
||||
-- Raw text literals for the bodies; see product_list/view.wo for the
|
||||
-- convention.
|
||||
use html
|
||||
use view
|
||||
|
||||
pub class OrderOk {
|
||||
name: Text
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
-- directory = one module, view and controller together. The @tables it
|
||||
-- queries live in the root module, which is fine: a CLASS is reachable
|
||||
-- across module lines (only free `fn`s are module-scoped).
|
||||
use framework/http
|
||||
use serve/http
|
||||
use layout
|
||||
|
||||
pub class ListProducts {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
-- source indentation removed at compile time. `${}` interpolates raw,
|
||||
-- `{{ }}` HTML-escapes — so display data goes through `{{ }}` and never
|
||||
-- through a hand-written esc() call.
|
||||
use html
|
||||
use view
|
||||
|
||||
pub class ProductCard {
|
||||
sku: Text
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
-- product_page/controller.wo — the CONTROLLER for /p/:sku.
|
||||
use framework/http
|
||||
use serve/http
|
||||
use layout
|
||||
|
||||
pub class ShowProduct {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
-- product_page/view.wo — the product detail view with the order form.
|
||||
-- Raw text literals for the bodies; see product_list/view.wo for the
|
||||
-- convention.
|
||||
use html
|
||||
use view
|
||||
|
||||
pub class ProductPage {
|
||||
sku: Text
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
-- static_files/controller.wo — serves /assets/* from disk. Traversal-
|
||||
-- safe (any ".." answers 404, never touches the filesystem), extension-
|
||||
-- mapped content types, 2 MiB cap per file. Text is binary-safe, so
|
||||
-- images travel as-is. (Story 38 lifts this into the framework; until
|
||||
-- then the template carries its own copy — it is ~40 lines.)
|
||||
use framework/http
|
||||
use fs
|
||||
|
||||
pub class StaticFiles {
|
||||
dir: Text
|
||||
fn handle(req: Req) -> Resp {
|
||||
let rel = req.params["path"];
|
||||
if rel == nil {
|
||||
return not_found();
|
||||
}
|
||||
if index_of("${rel}", "..") != -1 {
|
||||
return not_found();
|
||||
}
|
||||
let body = try fs.read_all("${self.dir}/${rel}", 2097152) catch (e) nil;
|
||||
if body == nil {
|
||||
return not_found();
|
||||
}
|
||||
let ct = "application/octet-stream";
|
||||
if ends_with("${rel}", ".css") { ct = "text/css; charset=utf-8"; }
|
||||
if ends_with("${rel}", ".js") { ct = "text/javascript"; }
|
||||
if ends_with("${rel}", ".svg") { ct = "image/svg+xml"; }
|
||||
if ends_with("${rel}", ".png") { ct = "image/png"; }
|
||||
if ends_with("${rel}", ".webp") { ct = "image/webp"; }
|
||||
let h: map<Text, Text> = {};
|
||||
h["content-type"] = ct;
|
||||
return Resp { status: 200, headers: h, body: "${body}" };
|
||||
}
|
||||
}
|
||||
|
|
@ -8,5 +8,5 @@ wo = ">= 0.1"
|
|||
# Two library dependencies, the site sample's proven shape. The [deps]
|
||||
# KEY is the module name `use` imports.
|
||||
[deps]
|
||||
framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
|
||||
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }
|
||||
serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
|
||||
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ samples now read the same way.
|
|||
| --- | --- | --- |
|
||||
| `types.wo` | MODEL | the `Chapter` `@table`, the `ChapterLink` projection, `Chapters.links()`, and `seed_if_empty()` |
|
||||
| `content.wo` | MODEL (content) | the nine chapter bodies as fragment-returning functions, plus `seed_chapters()` |
|
||||
| `layout/app.wo` | VIEW (chrome) | `AppShell` — the component that fills wo-html's `Layout` — the two named widths, and `html_error` |
|
||||
| `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 `<head>` links |
|
||||
| `install/view.wo`, `install/controller.wo` | VIEW + CONTROLLER | `/install` — the toolchain guide. Static copy, so `InstallPage` has no fields |
|
||||
|
|
@ -43,7 +43,7 @@ class — recorded gap #1 — but nothing needs it to.)
|
|||
- **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 wo-html's builders at boot; after
|
||||
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` —
|
||||
|
|
@ -87,10 +87,19 @@ class — recorded gap #1 — but nothing needs it to.)
|
|||
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.
|
||||
- **wo-html's sheet is static.** Tailwind's class NAMES, one hand-written
|
||||
- **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()`.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
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 `wo-html` dependency, and
|
||||
rows in a `@table`, the markup is built by the `writeonce-view` dependency, and
|
||||
the whole thing is chapter 9's own example.
|
||||
|
||||
```
|
||||
[deps]
|
||||
framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
|
||||
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }
|
||||
serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
|
||||
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }
|
||||
```
|
||||
|
||||
## Run it
|
||||
|
|
@ -29,6 +29,9 @@ SITE_HOST=0.0.0.0 SITE_TOKEN=change-me WO_DATA=./data ./target/site 8080
|
|||
`GET /packages/<name>` — the package catalogue with copy-paste
|
||||
`[deps]` lines and usage.
|
||||
- `GET /favicon.svg` — the mark, inline SVG, no asset pipeline.
|
||||
- `GET /dl/<file>` — release tarballs, served by the framework's
|
||||
`StaticFiles` from `$WO_DIST` (default `./dist`, where `just dist`
|
||||
writes them).
|
||||
- `POST /admin/ch/<slug>` — 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
|
||||
|
|
@ -54,7 +57,7 @@ MVC, laid out exactly like the program template
|
|||
| `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 (wo-html's structural
|
||||
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
|
||||
|
|
@ -64,7 +67,7 @@ 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 wo-html contains no query at all.
|
||||
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
|
||||
|
|
@ -96,5 +99,5 @@ 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 `wo-html`'s escaping/builders/Tailwind-style utility
|
||||
assignment, and `writeonce-view`'s escaping/builders/Tailwind-style utility
|
||||
sheet — self-contained pages, no CDN, no JS, no build step.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
-- 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 framework/http
|
||||
use serve/http
|
||||
|
||||
pub class AdminEdit {
|
||||
token: Text
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
-- 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 framework/http
|
||||
use serve/http
|
||||
use layout
|
||||
|
||||
pub class ShowChapter {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
-- 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 html
|
||||
use view
|
||||
|
||||
-- `ChapterLink` is the MODEL's projection type (types.wo); a class is
|
||||
-- reachable across module lines, so the view just names it.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
-- 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 wo-html dep — prose in el(), code samples through
|
||||
-- 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 html
|
||||
use view
|
||||
|
||||
fn ch_hello() -> Text {
|
||||
let b = el("p", "leading-relaxed mb-4",
|
||||
|
|
@ -61,15 +61,15 @@ fn ch_actors() -> Text {
|
|||
|
||||
fn ch_deps() -> Text {
|
||||
let b = el("p", "leading-relaxed mb-4",
|
||||
"Dependencies are git repositories pinned in <code>wo.toml</code>; <code>wo.lock</code> " .. "records the exact revision, and locked builds work offline. The [deps] KEY names the " .. "module you <code>use</code>. This site has two: the web framework, and the wo-html " .. "library that rendered the page you are reading.");
|
||||
"Dependencies are git repositories pinned in <code>wo.toml</code>; <code>wo.lock</code> " .. "records the exact revision, and locked builds work offline. The [deps] KEY names the " .. "module you <code>use</code>. This site has two: the web framework, and the writeonce-view " .. "library that rendered the page you are reading.");
|
||||
b = b .. code_block(`
|
||||
[deps]
|
||||
framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
|
||||
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }`);
|
||||
serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
|
||||
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }`);
|
||||
b = b .. code_block(`
|
||||
use framework
|
||||
use framework/http
|
||||
use html
|
||||
use serve
|
||||
use serve/http
|
||||
use view
|
||||
|
||||
-- html's builders + tailwind-style utilities, zero JS, no build step:
|
||||
let body = el("h1", "text-3xl font-bold", "Hello");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
-- favicon/controller.wo — GET /favicon.svg. The one route that answers
|
||||
-- something other than HTML or text, so it builds its own Resp.
|
||||
use framework/http
|
||||
use serve/http
|
||||
use layout
|
||||
|
||||
pub class Favicon {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
-- health.controller.wo — GET /health: the liveness probe the accept
|
||||
-- script and any proxy poll. Text, not HTML, on purpose.
|
||||
use framework/http
|
||||
use serve/http
|
||||
|
||||
pub class Health {
|
||||
fn handle(req: Req) -> Resp {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
-- 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 framework/http
|
||||
use serve/http
|
||||
use layout
|
||||
use chapter
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
-- 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 wo-html's `Layout` uses. HomePage
|
||||
-- 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 html
|
||||
use view
|
||||
|
||||
pub class HomePage {
|
||||
chapter_nav: Component
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
-- install/controller.wo — GET /install. Nothing to query: the page is
|
||||
-- static copy, so the controller only wraps it in the shell.
|
||||
use framework/http
|
||||
use serve/http
|
||||
use layout
|
||||
|
||||
pub class ShowInstall {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
-- 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 html
|
||||
use view
|
||||
|
||||
pub class InstallPage {
|
||||
fn render() -> Text {
|
||||
|
|
@ -13,43 +13,58 @@ pub class InstallPage {
|
|||
language runtime to install on the machines you deploy to, and no
|
||||
build toolchain beyond these two files.</p>`;
|
||||
|
||||
let s1 = section("1. Get the toolchain",
|
||||
`<p class="leading-relaxed mb-4">Download the release tarball and extract
|
||||
it into <code>/usr/local</code>. Run this as root, or through
|
||||
let sys = supported();
|
||||
|
||||
let s1 = section("1. Download",
|
||||
`<p class="leading-relaxed mb-4">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:</p>
|
||||
<p class="mb-4">
|
||||
<a class="btn no-underline" href="https://github.com/shoneyj/writeonce/releases/download/v0.1.0/writeonce-0.1.0-linux-amd64.tar.gz">Download 0.1.0 (linux-amd64)</a>
|
||||
<a class="btn-outline no-underline" href="/dl/writeonce-0.1.0-linux-amd64.tar.gz">Mirror</a>
|
||||
</p>
|
||||
<p class="leading-relaxed mb-4">Verify it before you extract it. The
|
||||
expected digest is served beside the archive at
|
||||
<a href="/dl/writeonce-0.1.0-linux-amd64.tar.gz.sha256">.sha256</a>:</p>`,
|
||||
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",
|
||||
`<p class="leading-relaxed mb-4">Extract into <code>/usr/local</code>,
|
||||
replacing any previous install. Run this as root, or through
|
||||
<code>sudo</code>:</p>`,
|
||||
code_block("rm -rf /usr/local/writeonce\ntar -C /usr/local -xzf writeonce-0.1.0-linux-amd64.tar.gz"));
|
||||
|
||||
let s2 = section("2. Put it on your PATH",
|
||||
let s3 = section("3. Put it on your PATH",
|
||||
`<p class="leading-relaxed mb-4">Add one line to your
|
||||
<code>$HOME/.profile</code> (or <code>/etc/profile</code> for every user
|
||||
on the box), then restart your shell:</p>`,
|
||||
code_block("export PATH=$PATH:/usr/local/writeonce/bin"));
|
||||
|
||||
let s3 = section("3. Check it",
|
||||
let s4 = section("4. Check it",
|
||||
`<p class="leading-relaxed mb-4">Both should print the same version.
|
||||
<code>woc</code> finds <code>wovm</code> beside itself, so a tarball
|
||||
install needs no further configuration.</p>`,
|
||||
code_block("woc version # writeonce 0.1.0 linux/amd64\nwovm --version # wovm 0.1.0"));
|
||||
|
||||
let s4 = section("4. Your first project",
|
||||
let s5 = section("5. Your first project",
|
||||
`<p class="leading-relaxed mb-4">A project is a directory with a
|
||||
<code>wo.toml</code> manifest and one or more <code>.wo</code> files.
|
||||
Nothing else — no lockfile to create by hand, no scaffolding step.</p>`,
|
||||
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 s5 = section("5. Build and run",
|
||||
let s6 = section("6. Build and run",
|
||||
`<p class="leading-relaxed mb-4"><code>woc <dir></code> emits ONE
|
||||
standalone binary at <code>target/<name></code>. Copy that file to a
|
||||
server and run it — the VM is inside it, and so is the database.</p>`,
|
||||
code_block("woc .\n./target/hello # hello, writeonce"));
|
||||
|
||||
let s6 = section("6. Add a dependency",
|
||||
let s7 = section("7. Add a dependency",
|
||||
`<p class="leading-relaxed mb-4">Dependencies are git repositories pinned
|
||||
by revision. The <code>[deps]</code> KEY is the module name your code
|
||||
<code>use</code>s. <code>woc</code> writes a <code>wo.lock</code> with the
|
||||
exact revision, and a locked build works offline. See
|
||||
<a href="/packages">Packages</a> for what is available.</p>`,
|
||||
code_block("[deps]\nhtml = { git = \"https://github.com/shoneyj/wo-html\", rev = \"v0.1.0\" }"));
|
||||
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") ..
|
||||
|
|
@ -62,10 +77,30 @@ pub class InstallPage {
|
|||
<code>[runtime] wo = ">= 0.1"</code>; <code>woc</code> refuses to build
|
||||
a project that needs a newer toolchain than itself.</p>`);
|
||||
|
||||
return head .. s1 .. s2 .. s3 .. s4 .. s5 .. s6 .. note;
|
||||
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 `
|
||||
<div class="bg-white rounded-lg border shadow-sm p-6 mb-8">
|
||||
<h2 class="text-2xl font-bold mb-2">Supported systems</h2>
|
||||
<ul class="list-disc pl-6 leading-relaxed">
|
||||
<li class="mb-2"><b>Linux on x86-64</b> — the only target built today. There is no ARM, macOS or Windows build.</li>
|
||||
<li class="mb-2"><b>glibc 2.38 or newer.</b> The binaries link the system C library dynamically and import symbols up to <code>GLIBC_2.38</code>. That covers Ubuntu 24.04+, Debian 13+, and Fedora 39+ — and rules out Ubuntu 22.04 (2.35), Debian 12 (2.36) and RHEL 9 (2.34).</li>
|
||||
<li class="mb-2"><b>Not musl.</b> Alpine needs a build against musl, which does not exist yet.</li>
|
||||
<li class="mb-2"><b>Nothing else.</b> No JVM, no Node, no Python, no package manager. The two binaries and libc are the whole dependency list.</li>
|
||||
</ul>
|
||||
<p class="leading-relaxed mt-4 text-gray-700">Check yours with
|
||||
<code>ldd --version</code>. If it is older than 2.38, build from source
|
||||
until a wider-compatibility release exists.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
-- One numbered step: heading, prose, and the commands to run.
|
||||
fn section(title: Text, prose: Text, code: Text) -> Text {
|
||||
return el("div", "mb-8",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
-- 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 (wo-html's `page()` does that), so the shell
|
||||
-- fills wo-html's own `Layout` rather than writing its own document.
|
||||
-- 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 framework/http
|
||||
use html
|
||||
use serve/http
|
||||
use view
|
||||
|
||||
pub class AppShell {
|
||||
title: Text
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
-- layout/footer.wo — the site footer, shared by every page.
|
||||
use html
|
||||
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.");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
-- layout/header.wo — the site navigation bar, shared by every page.
|
||||
use html
|
||||
use view
|
||||
|
||||
pub fn header() -> Text {
|
||||
let links = link("/install", "text-gray-700", "Install");
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
-- site — writeonce.de: the language tutorial, served BY the language.
|
||||
-- Full stack in one binary: writeonce-framework ([deps]) for HTTP/routing/
|
||||
-- auth, wo-html ([deps]) for server-rendered pages with Tailwind-style
|
||||
-- Full stack in one binary: writeonce-serve ([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).
|
||||
|
|
@ -14,8 +15,9 @@
|
|||
-- model is types.wo; every feature is a directory holding its view and
|
||||
-- its controller.
|
||||
use env
|
||||
use framework
|
||||
use framework/router
|
||||
use serve
|
||||
use serve/http
|
||||
use serve/router
|
||||
use home
|
||||
use chapter
|
||||
use install
|
||||
|
|
@ -49,6 +51,14 @@ fn main(args: multi Text) -> Int {
|
|||
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: [] };
|
||||
|
|
@ -59,6 +69,9 @@ fn main(args: multi Text) -> Int {
|
|||
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}");
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
-- 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 framework/http
|
||||
use html
|
||||
use serve/http
|
||||
use view
|
||||
use layout
|
||||
|
||||
typedef PackageInfo = {
|
||||
|
|
@ -54,25 +54,26 @@ fn catalogue() -> multi PackageInfo {
|
|||
let out: multi PackageInfo = [];
|
||||
|
||||
push(out, PackageInfo {
|
||||
name: "framework",
|
||||
summary: "A web framework written in writeonce: an HTTP/1.1 keep-alive server core, a router with :param captures, and Handler/Middleware structural interfaces.",
|
||||
git: "https://github.com/shoneyj/writeonce-framework",
|
||||
name: "serve",
|
||||
summary: "The web framework: an HTTP/1.1 keep-alive server core, a router with :param captures, and Handler/Middleware structural interfaces.",
|
||||
git: "https://github.com/shoneyj/writeonce-serve",
|
||||
rev: "v0.1.0",
|
||||
what: `
|
||||
<ul class="list-disc pl-6 leading-relaxed">
|
||||
<li class="mb-2"><code>App</code> — the route table and the middleware chains; <code>app.get</code>/<code>post</code>/<code>delete_</code>, <code>use_mw</code>, <code>use_after</code>, <code>mount</code>, <code>serve</code>.</li>
|
||||
<li class="mb-2"><code>StaticFiles</code> — serve a directory over a wildcard route. Traversal is refused, not normalised; <code>max_bytes</code> is a hard ceiling. This site's <code>/dl</code> downloads run through it.</li>
|
||||
<li class="mb-2"><code>Handler</code>, <code>Middleware</code>, <code>After</code> — structural interfaces. A handler is a CLASS; its fields are the closure this language does not have.</li>
|
||||
<li class="mb-2"><code>Req</code>/<code>Resp</code> plus builders: <code>ok_text</code>, <code>ok_html</code>, <code>ok_json</code>, <code>created_json</code>, <code>not_found</code>, <code>bad_request</code>, <code>unauthorized</code>, <code>conflict</code>, <code>redirect</code>.</li>
|
||||
<li class="mb-2">Auth MECHANISM only — <code>bearer_token</code>, <code>basic_credentials</code>, constant-time <code>ct_eq</code>. Which routes are gated stays your policy.</li>
|
||||
<li class="mb-2">Bodies: <code>form_values</code>, <code>multipart_parts</code>, content negotiation, ETags, security headers, CORS, WebSocket frames.</li>
|
||||
</ul>`,
|
||||
usage: code_block("use framework\nuse framework/http\nuse framework/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}")
|
||||
usage: code_block("use serve\nuse serve/http\nuse serve/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: "html",
|
||||
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/wo-html",
|
||||
git: "https://github.com/shoneyj/writeonce-view",
|
||||
rev: "v0.1.0",
|
||||
what: `
|
||||
<ul class="list-disc pl-6 leading-relaxed">
|
||||
|
|
@ -82,7 +83,7 @@ fn catalogue() -> multi PackageInfo {
|
|||
<li class="mb-2">Builders: <code>el</code>, <code>link</code>, <code>card</code>, <code>nav_bar</code>, <code>code_block</code>, <code>form_post</code>, <code>text_input</code>, <code>text_area</code>, <code>submit_btn</code>, <code>btn_link</code>.</li>
|
||||
<li class="mb-2"><code>tw_css()</code> + <code>page()</code> — one hand-written utility sheet, inlined, so a page is one self-contained response. No CDN, no build step, no JS.</li>
|
||||
</ul>`,
|
||||
usage: code_block("use html\n\nclass Card {\n name: Text\n fn render() -> Text {\n return `\n <div class=\"card\">\n <h3>{{ self.name }}</h3>\n </div>`;\n }\n}\n\n-- in a handler:\nlet c = Card { name: user_supplied };\nreturn ok_html(page(\"Hello\", c.render()));")
|
||||
usage: code_block("use view\n\nclass Card {\n name: Text\n fn render() -> Text {\n return `\n <div class=\"card\">\n <h3>{{ self.name }}</h3>\n </div>`;\n }\n}\n\n-- in a handler:\nlet c = Card { name: user_supplied };\nreturn ok_html(page(\"Hello\", c.render()));")
|
||||
});
|
||||
|
||||
return out;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
-- 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 html
|
||||
use view
|
||||
|
||||
pub class PackageCard {
|
||||
name: Text
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
name = "site"
|
||||
version = "0.1.0"
|
||||
description = "writeonce.de — the language tutorial served by the language: framework + wo-html [deps], @table chapters, server-rendered pages"
|
||||
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/writeonce-framework and docs/examples/wo-html, so CI never
|
||||
# docs/examples/writeonce-serve and docs/examples/writeonce-view, so CI never
|
||||
# touches the network). The [deps] KEY is the module name `use` imports.
|
||||
[deps]
|
||||
framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
|
||||
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }
|
||||
serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
|
||||
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# web-app — the storefront sample
|
||||
|
||||
A small store: `Product`/`Order` as `@table` classes, JSON routes, one auth
|
||||
middleware — built on [`writeonce-framework`](../writeonce-framework/), which
|
||||
middleware — built on [`writeonce-serve`](../writeonce-serve/), which
|
||||
it imports **through `[deps]`** (iteration 15). This app is iteration 16's
|
||||
acceptance workload: `just web-app` runs the whole chain — fetch → lock →
|
||||
build → serve → curl matrix → restart persistence → SIGTERM.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- web-app — the storefront: writeonce-framework (via [deps]) + @table
|
||||
-- web-app — the storefront: writeonce-serve (via [deps]) + @table
|
||||
-- persistence. Every handler is a class satisfying Handler; the auth gate is
|
||||
-- a Middleware; the data layer is the language's own database — no ORM, no
|
||||
-- separate process, one binary.
|
||||
|
|
@ -6,9 +6,9 @@ use env
|
|||
use json
|
||||
use net
|
||||
use time
|
||||
use framework
|
||||
use framework/http
|
||||
use framework/router
|
||||
use serve
|
||||
use serve/http
|
||||
use serve/router
|
||||
|
||||
-- decode target for POST /products, encode shape for every product answer
|
||||
typedef ProductView = { name: Text, price: Float, stock: Int }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name = "web-app"
|
||||
version = "0.1.0"
|
||||
description = "Storefront sample: consumes writeonce-framework through [deps]; @table persistence; iteration 16's acceptance workload"
|
||||
description = "Storefront sample: consumes writeonce-serve through [deps]; @table persistence; iteration 16's acceptance workload"
|
||||
|
||||
[runtime]
|
||||
wo = ">= 0.1"
|
||||
|
|
@ -11,9 +11,9 @@ wo = ">= 0.1"
|
|||
# The framework is a real dependency, never a relative path — extraction of
|
||||
# the framework to its own repository changes only this URL. The acceptance
|
||||
# gate (scripts/web-app-accept.sh) substitutes a run-time file:// remote
|
||||
# built from docs/examples/writeonce-framework, so CI never needs the
|
||||
# built from docs/examples/writeonce-serve, so CI never needs the
|
||||
# network and this repo never carries .wo-deps/wo.lock artifacts.
|
||||
# The [deps] KEY is the module name `use` imports (hyphens are not identifier
|
||||
# characters, so the key is `framework` while the repository keeps its name).
|
||||
[deps]
|
||||
framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
|
||||
serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
# writeonce-framework
|
||||
# writeonce-serve
|
||||
> Renamed 2026-08-25: this library was `writeonce-framework`, imported as
|
||||
> `use framework`. Stories, specs and plans dated before that still say the
|
||||
> old name — they are dated records and were left as written.
|
||||
|
||||
A web framework **written in writeonce**, consumed as a `[deps]` dependency
|
||||
(iteration 15). Spec: `docs/superpowers/specs/2026-08-18-web-framework-design.md` §B.
|
||||
|
||||
```toml
|
||||
[deps]
|
||||
writeonce-framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
|
||||
writeonce-serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
|
||||
```
|
||||
|
||||
## What it is
|
||||
|
|
@ -167,7 +170,7 @@ naming the kind, unless a demo `main` is added (lib+bin is allowed).
|
|||
- **`internal/` — not importable by a consumer.** The connection-level request
|
||||
parser and carry-state record (`parse.wo`) and the serve loop, status text,
|
||||
and response serializer (`serve.wo`) live here. A consuming app that writes
|
||||
`use writeonce-framework/internal` gets **WO-E108** at that `use`. The rule
|
||||
`use writeonce-serve/internal` gets **WO-E108** at that `use`. The rule
|
||||
is Go's: a path segment named `internal` is refused across the `[deps]`
|
||||
boundary only — the framework's own modules import it freely.
|
||||
|
||||
|
|
@ -180,3 +183,20 @@ elimination); a consumer simply cannot name them.
|
|||
`docs/examples/web-app` — a small storefront importing this framework
|
||||
through `[deps]`. Its acceptance (`just web-app`) exercises the whole chain:
|
||||
fetch → lock → build → serve → durable restart.
|
||||
|
||||
## Serving files
|
||||
|
||||
`StaticFiles { dir, max_bytes }` mounts a directory on a wildcard route:
|
||||
|
||||
```
|
||||
app.get("/assets/*path", StaticFiles { dir: "assets", max_bytes: 2097152 })
|
||||
app.get("/dl/*path", StaticFiles { dir: "dist", max_bytes: 16777216 })
|
||||
```
|
||||
|
||||
Two rules, both refusals rather than repairs: a path containing `..` is a
|
||||
404 and never reaches the filesystem, and `max_bytes` is a hard ceiling —
|
||||
`fs.read_all` truncates above it, so set it above the largest file you
|
||||
mean to serve. Content types come from the extension; archives
|
||||
(`.tar.gz`, `.tgz`, `.zip`) also get `content-disposition: attachment`.
|
||||
Text is binary-safe in this language, so archives and images travel
|
||||
unchanged. Lifted out of the shop template 2026-08-25.
|
||||
72
docs/examples/writeonce-serve/http/files.wo
Normal file
72
docs/examples/writeonce-serve/http/files.wo
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
-- http/files.wo — serving a file from disk, the framework's answer to
|
||||
-- "let people download something". Lifted out of the shop template
|
||||
-- 2026-08-25, which had carried its own copy and said in a comment that
|
||||
-- this belonged here.
|
||||
--
|
||||
-- Mount it on a wildcard route and it answers that subtree:
|
||||
--
|
||||
-- app.get("/assets/*path", StaticFiles { dir: "assets", max_bytes: 2097152 })
|
||||
-- app.get("/dl/*path", StaticFiles { dir: "dist", max_bytes: 8388608 })
|
||||
--
|
||||
-- Safety is two rules, both refusals rather than repairs: a path holding
|
||||
-- `..` is a 404 and never reaches the filesystem, and a file bigger than
|
||||
-- `max_bytes` is truncated by `fs.read_all` — so `max_bytes` is a real
|
||||
-- ceiling you must set above the largest file you intend to serve, not a
|
||||
-- hint. Text is binary-safe in this language, so archives and images
|
||||
-- travel unchanged.
|
||||
use fs
|
||||
|
||||
pub class StaticFiles {
|
||||
dir: Text
|
||||
max_bytes: Int
|
||||
|
||||
fn handle(req: Req) -> Resp {
|
||||
let rel = req.params["path"];
|
||||
if rel == nil {
|
||||
return not_found();
|
||||
}
|
||||
-- Traversal: refuse, never normalise. A rewritten path is a second
|
||||
-- chance to get it wrong.
|
||||
if index_of("${rel}", "..") != -1 {
|
||||
return not_found();
|
||||
}
|
||||
let body = try fs.read_all("${self.dir}/${rel}", self.max_bytes) catch (e) nil;
|
||||
if body == nil {
|
||||
return not_found();
|
||||
}
|
||||
let h: map<Text, Text> = {};
|
||||
h["content-type"] = content_type("${rel}");
|
||||
if is_download("${rel}") {
|
||||
h["content-disposition"] = "attachment";
|
||||
}
|
||||
return Resp { status: 200, headers: h, body: "${body}" };
|
||||
}
|
||||
}
|
||||
|
||||
-- Extension to content type. Unknown extensions are octet-stream: a
|
||||
-- wrong guess is worse than no guess.
|
||||
pub fn content_type(name: Text) -> Text {
|
||||
if ends_with(name, ".html") { return "text/html; charset=utf-8"; }
|
||||
if ends_with(name, ".css") { return "text/css; charset=utf-8"; }
|
||||
if ends_with(name, ".js") { return "text/javascript"; }
|
||||
if ends_with(name, ".json") { return "application/json"; }
|
||||
if ends_with(name, ".svg") { return "image/svg+xml"; }
|
||||
if ends_with(name, ".png") { return "image/png"; }
|
||||
if ends_with(name, ".webp") { return "image/webp"; }
|
||||
if ends_with(name, ".ico") { return "image/x-icon"; }
|
||||
if ends_with(name, ".woff2") { return "font/woff2"; }
|
||||
if ends_with(name, ".txt") { return "text/plain; charset=utf-8"; }
|
||||
if ends_with(name, ".sha256") { return "text/plain; charset=utf-8"; }
|
||||
if ends_with(name, ".tar.gz") { return "application/gzip"; }
|
||||
if ends_with(name, ".tgz") { return "application/gzip"; }
|
||||
if ends_with(name, ".zip") { return "application/zip"; }
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
-- Archives are offered as a save, not rendered into a tab.
|
||||
fn is_download(name: Text) -> Bool {
|
||||
if ends_with(name, ".tar.gz") { return true; }
|
||||
if ends_with(name, ".tgz") { return true; }
|
||||
if ends_with(name, ".zip") { return true; }
|
||||
return false;
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ pub fn ok_text(body: Text) -> Resp {
|
|||
-- iteration 37: HTML is transport here, exactly like text and JSON —
|
||||
-- the status line and the content-type, nothing about rendering. It
|
||||
-- lives beside its two siblings because both HTML apps had hand-rolled
|
||||
-- the identical four lines; wo-html stays a pure Text library and never
|
||||
-- the identical four lines; writeonce-view stays a pure Text library and never
|
||||
-- learns what a Resp is.
|
||||
pub fn ok_html(body: Text) -> Resp {
|
||||
let h: map<Text, Text> = {};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
name = "writeonce-framework"
|
||||
name = "writeonce-serve"
|
||||
kind = "library"
|
||||
version = "0.1.0"
|
||||
description = "A web framework written in writeonce: HTTP/1.1 keep-alive server core, router with :param captures, Handler/Middleware structural interfaces (iteration 16)"
|
||||
|
|
@ -9,4 +9,4 @@ wo = ">= 0.1"
|
|||
# A LIBRARY project (declared above since iteration 17): no `fn main` here —
|
||||
# the consuming app owns the entry. `woc <dir>` typechecks the whole project.
|
||||
# Apps import this repo through `wo.toml [deps]` (iteration 15) and
|
||||
# `use writeonce-framework` / `use writeonce-framework/http` / `.../router`.
|
||||
# `use serve` / `use serve/http` / `use serve/router`.
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
# wo-html — server-rendered HTML as plain Text
|
||||
# writeonce-view — server-rendered HTML as plain Text
|
||||
> Renamed 2026-08-25: this library was `wo-html`, imported as `use html`.
|
||||
> Stories, specs and plans dated before that still say the old name — they
|
||||
> are dated records and were left as written.
|
||||
|
||||
A view library, not a framework and not a template engine. Everything in
|
||||
it is a pure function or a class with a `render()`; nothing here opens a
|
||||
|
|
@ -6,7 +9,7 @@ socket, reads a file, or touches the database.
|
|||
|
||||
```
|
||||
[deps]
|
||||
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }
|
||||
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }
|
||||
```
|
||||
|
||||
## The four layers
|
||||
|
|
@ -78,7 +81,7 @@ component like any other.
|
|||
|
||||
`ok_html` is the **framework's** (`framework/http`, beside `ok_text` and
|
||||
`ok_json`): a status line plus a content-type is transport, not
|
||||
rendering, so wo-html never learns what a `Resp` is.
|
||||
rendering, so writeonce-view never learns what a `Resp` is.
|
||||
|
||||
The seam is what makes a view testable without a server and a query
|
||||
testable without markup. Breaking it looks like one convenience — a
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
-- wo-html — server-rendered HTML as plain Text. Four layers, all pure:
|
||||
-- writeonce-view — server-rendered HTML as plain Text. Four layers, all pure:
|
||||
-- esc() HTML-escape untrusted text. `{{ }}` in a raw text
|
||||
-- literal compiles to a call to this, so display data is
|
||||
-- escaped by construction; esc() by hand is the fallback
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
name = "wo-html"
|
||||
name = "writeonce-view"
|
||||
kind = "library"
|
||||
version = "0.1.0"
|
||||
description = "HTML building for writeonce apps: escaping, element builders, and a Tailwind-style utility stylesheet — server-rendered pages as plain Text"
|
||||
|
|
@ -7,4 +7,4 @@ description = "HTML building for writeonce apps: escaping, element builders, and
|
|||
wo = ">= 0.1"
|
||||
|
||||
# A LIBRARY project: no `fn main`. Apps import it through wo.toml [deps]
|
||||
# (the key names the module — `html = { git = ... }` gives `use html`).
|
||||
# (the key names the module — `view = { git = ... }` gives `use view`).
|
||||
|
|
@ -52,7 +52,7 @@ Status board: [`00-status.md`](../stories/00-status.md) · Doctrine: [`../00-pri
|
|||
| **Per-example `principle.md` files** | 2026-08-08: one canonical repo-level [`docs/00-principles.md`](../00-principles.md) instead; examples link to it. |
|
||||
| **Minimal 3-file log-watcher sample** | Breaks the file-for-file `.hx` → `.wo` mapping and leaves the "could not express" column unproven — which is the sample's entire acceptance criterion. |
|
||||
| **Raw code in plan documents** | Plans carry concept, reason, and required behavior in words; the executor writes the code. |
|
||||
| **`##ui` / `.htmlx` LiveView frontend track** | 2026-08-17: removed the 9-doc `exploration/ui/` design set, the `14-mvc-ui-implementation` plan, and the `ui-htmlx-live` plan. All were built on the non-advancing Rust runtime (`.dev/reference/crates/wo-htmlx`, `cargo run`, WebSocket live-patches) and contradict the current woc/wovm direction. The 13d pricing-UI row went with them. Revisit only if a UI story is re-opened on the woc/wovm stack. |
|
||||
| **`##ui` / `.htmlx` LiveView frontend track** | 2026-08-17: removed the 9-doc `exploration/ui/` design set, the `14-mvc-ui-implementation` plan, and the `ui-htmlx-live` plan. All were built on the non-advancing Rust runtime (`.dev/reference/crates/writeonce-viewx`, `cargo run`, WebSocket live-patches) and contradict the current woc/wovm direction. The 13d pricing-UI row went with them. Revisit only if a UI story is re-opened on the woc/wovm stack. |
|
||||
| **Old-runtime "front door" + v1 design docs** | 2026-08-17: removed `writeonce-pl.md`, `runtime/wo-language.md`, `future-scope/ai-agents-content-management.md`, the numbered v1 set `02-recovery`/`03-data`/`04-ui`/`05-datalayer`/`06-markdown-render`/`07-ssl`, and `runtime/database/05-go-sdk.md`. They pitched the old Rust `wo` runtime (REST + LiveView + SQL/Cypher) as the current language and contradicted the shipped woc/wovm toolchain. |
|
||||
| **The 2026-08-01 shard-actor plan (epoll-based)** | 2026-08-21: [`superpowers/plans/2026-08-01-shard-actor-vm-runtime.md`](../superpowers/plans/2026-08-01-shard-actor-vm-runtime.md) marked discarded, file kept as reference. Superseded by the arc plan of record ([`2026-08-20-shard-fiber-arc.md`](../superpowers/plans/2026-08-20-shard-fiber-arc.md), stages 1+2 landed): io_uring is a MUST and the epoll-based approach is discarded — the old plan's "epoll now / io_uring later" premise is inverted, and its substrate (`runtime/wo-rt.c`) left with the Rust track 2026-08-18. |
|
||||
| **The entire Rust `wo` runtime track** | 2026-08-18: removed `crates/` (the Stage-2 Rust runtime), `Cargo.toml`/`Cargo.lock`, `prototypes/` (wo-rt-c stale duplicate + wo-db C++ ref), the `rt-c-*` justfile recipes, the Rust engineering plans (`docs/plan/05..16`, `docs/plan/done/`), and `docs/runtime/` (the old runtime overview + 7-phase DB design series + async/fibers/gc/surreal essays). It was the prior, abandoned architecture — fully independent of the woc/wovm stack. Master now reflects only the current single-language project; the removed track lives in git history if ever needed as reference. Kept: the syscall/postgres/assembly/c-runtime **exploration studies** (they fed the current C runtime) and the discarded/learnings registers. |
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ Lowered by the emitter, not added to the format:
|
|||
| `a == b` on `Text` | `EQS` (content equality); `EQ` otherwise |
|
||||
| `a .. b` | `CONCAT` — `+` is arithmetic only, never string addition |
|
||||
| `` `...${e}...` `` | the same `StrLit`/`Interp`/`CONCAT` chain a `"..."` string produces — the raw literal is a LEXER form (verbatim content, common margin removed at lex time), not a new node or opcode |
|
||||
| `` `...{{ e }}...` `` | `esc(${e})` — the parser wraps the interpolation in a call to whatever `esc` is in scope (wo-html's `pub fn esc`, or a local one that deliberately shadows it). No builtin, no opcode, no HTML knowledge in the compiler or the VM |
|
||||
| `` `...{{ e }}...` `` | `esc(${e})` — the parser wraps the interpolation in a call to whatever `esc` is in scope (writeonce-view's `pub fn esc`, or a local one that deliberately shadows it). No builtin, no opcode, no HTML knowledge in the compiler or the VM |
|
||||
| `a and b` | evaluate `a`; `JZ` past evaluating `b` (result stays `a`'s value); else evaluate `b` into the same register (haxe-parity Task 2) |
|
||||
| `a or b` | evaluate `a`; `JZ` + `JMP` past evaluating `b` when `a` is already true; else evaluate `b` (haxe-parity Task 2) |
|
||||
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ sees through Interp; same corpus pin). Body-parsing hooks: all three ✅.
|
|||
|
||||
Scope split (2026-08-20): the surface above plus the remaining transport/
|
||||
routing/security gaps is **framework v1**, tracked item-by-item in the
|
||||
[framework README's status ledger](../examples/writeonce-framework/README.md)
|
||||
[framework README's status ledger](../examples/writeonce-serve/README.md)
|
||||
(✅/🔶/⬜/⏸/🔧 per feature — timeouts and Unix sockets need `net` runtime
|
||||
seams, crypto hashes need C builtins since the language has no bitwise
|
||||
operators, streaming/cancellation park behind 8/11). The memory-rich
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ status: hold
|
|||
|
||||
> **Scope label (2026-08-20): this iteration is FRAMEWORK V2.** Framework
|
||||
> v1 is the transport/routing/body/security surface tracked in the
|
||||
> [framework README's status ledger](../../../examples/writeonce-framework/README.md);
|
||||
> [framework README's status ledger](../../../examples/writeonce-serve/README.md);
|
||||
> v2 is what the embedded store adds on top. v1 gaps land before or
|
||||
> alongside v2 as slices, per the ledger.
|
||||
|
||||
|
|
|
|||
4
justfile
4
justfile
|
|
@ -41,7 +41,7 @@ deps-accept:
|
|||
./scripts/deps-accept.sh
|
||||
|
||||
# web-app: iteration 16's gate — the whole chain at run time, network-free:
|
||||
# a temp git remote from docs/examples/writeonce-framework, file:// URL
|
||||
# a temp git remote from docs/examples/writeonce-serve, file:// URL
|
||||
# substituted into a temp copy of docs/examples/web-app, then fetch -> lock
|
||||
# -> build -> serve -> the storefront matrix (auth/CRUD/@unique/FK/404/400/
|
||||
# pipelining) -> SIGTERM -> WAL restart persistence.
|
||||
|
|
@ -49,7 +49,7 @@ web-app:
|
|||
./scripts/web-app-accept.sh
|
||||
|
||||
# site: the writeonce.de tutorial (docs/examples/site) — TWO deps
|
||||
# (framework + wo-html), server-rendered pages, page matrix -> 401/edit
|
||||
# (serve + view), server-rendered pages, page matrix -> 401/edit
|
||||
# -> SIGTERM -> WAL restart persistence. `just site` runs it.
|
||||
site:
|
||||
./scripts/site-accept.sh
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/site-accept.sh — the writeonce.de tutorial site's gate: TWO deps
|
||||
# (framework + wo-html) resolved from run-time file:// remotes, build,
|
||||
# (serve + view) resolved from run-time file:// remotes, build,
|
||||
# serve, the page matrix (render/escape/404/401/authed edit), SIGTERM,
|
||||
# and WAL restart persistence of an admin edit.
|
||||
set -uo pipefail
|
||||
|
|
@ -27,8 +27,8 @@ cleanup() {
|
|||
trap cleanup EXIT
|
||||
|
||||
# ---- both deps as git remotes; the app pointed at them ----
|
||||
cp -r "$ROOT/docs/examples/writeonce-framework" "$W/fw"
|
||||
cp -r "$ROOT/docs/examples/wo-html" "$W/lib"
|
||||
cp -r "$ROOT/docs/examples/writeonce-serve" "$W/fw"
|
||||
cp -r "$ROOT/docs/examples/writeonce-view" "$W/lib"
|
||||
for d in "$W/fw" "$W/lib"; do
|
||||
git -C "$d" init -q
|
||||
git -C "$d" add -A
|
||||
|
|
@ -36,9 +36,15 @@ for d in "$W/fw" "$W/lib"; do
|
|||
git -C "$d" tag v0.1.0
|
||||
done
|
||||
cp -r "$ROOT/docs/examples/site" "$W/app"
|
||||
sed -i "s|https://github.com/shoneyj/writeonce-framework|file://$W/fw|; s|https://github.com/shoneyj/wo-html|file://$W/lib|" "$W/app/wo.toml"
|
||||
sed -i "s|https://github.com/shoneyj/writeonce-serve|file://$W/fw|; s|https://github.com/shoneyj/writeonce-view|file://$W/lib|" "$W/app/wo.toml"
|
||||
printf '[build]\nruntime = "%s"\n' "$WOVM" >> "$W/app/wo.toml"
|
||||
|
||||
# a stand-in release tarball so /dl can be exercised without running
|
||||
# `just dist` first — the bytes do not matter, the serving path does
|
||||
mkdir -p "$W/app/dist"
|
||||
printf 'not-a-real-tarball' | gzip > "$W/app/dist/writeonce-0.1.0-linux-amd64.tar.gz"
|
||||
( cd "$W/app/dist" && sha256sum writeonce-0.1.0-linux-amd64.tar.gz > writeonce-0.1.0-linux-amd64.tar.gz.sha256 )
|
||||
|
||||
# ---- 1. two-dep fetch + lock + build ----
|
||||
if "$WOC" "$W/app" >"$W/build.out" 2>&1 && [[ -x "$W/app/target/site" && -f "$W/app/wo.lock" ]]; then
|
||||
ok "deps chain: two remotes fetched + wo.lock + build"
|
||||
|
|
@ -69,6 +75,20 @@ except urllib.error.HTTPError as e:
|
|||
PYEOF
|
||||
}
|
||||
|
||||
# binary-safe: status + content-type + byte count, no decoding. The
|
||||
# release tarball is gzip, which `hit` cannot represent.
|
||||
hit_bin() {
|
||||
python3 - "$PORT" "$1" <<'PYEOF'
|
||||
import sys, urllib.request, urllib.error
|
||||
port, path = sys.argv[1], sys.argv[2]
|
||||
try:
|
||||
r = urllib.request.urlopen(f"http://127.0.0.1:{port}{path}", timeout=5)
|
||||
print(f"{r.status}|{r.headers.get('content-type','')}|{len(r.read())}")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"{e.code}|{e.headers.get('content-type','')}|{len(e.read())}")
|
||||
PYEOF
|
||||
}
|
||||
|
||||
expect() { # name got want_status want_substr
|
||||
local name="$1" got="$2" want="$3" sub="$4"
|
||||
local st="${got%%|*}" body="${got#*|}"
|
||||
|
|
@ -94,10 +114,17 @@ expect "chapter renders a code sample" "$(hit /ch/hello)" 200 "fn main"
|
|||
expect "escaped interpolation visible" "$(hit /ch/values)" 200 '${port}'
|
||||
expect "unknown chapter is a 404 page" "$(hit /ch/nope)" 404 "No such chapter"
|
||||
expect "install guide renders" "$(hit /install)" 200 "tar -C /usr/local"
|
||||
expect "packages index lists both" "$(hit /packages)" 200 "/packages/framework"
|
||||
expect "package detail shows its dep" "$(hit /packages/html)" 200 "wo-html"
|
||||
expect "packages index lists both" "$(hit /packages)" 200 "/packages/serve"
|
||||
expect "package detail shows its dep" "$(hit /packages/view)" 200 "writeonce-view"
|
||||
expect "unknown package is a 404 page" "$(hit /packages/nope)" 404 "No such package"
|
||||
expect "favicon is served as svg" "$(hit /favicon.svg)" 200 "<svg"
|
||||
expect "install lists supported systems" "$(hit /install)" 200 "glibc 2.38 or newer"
|
||||
got="$(hit_bin /dl/writeonce-0.1.0-linux-amd64.tar.gz)"
|
||||
if [[ "$got" == 200\|application/gzip\|* && "${got##*|}" -gt 0 ]]; then
|
||||
ok "the release tarball downloads as gzip"
|
||||
else bad "tarball download" "$got"; fi
|
||||
expect "its checksum downloads" "$(hit /dl/writeonce-0.1.0-linux-amd64.tar.gz.sha256)" 200 "writeonce-0.1.0-linux-amd64.tar.gz"
|
||||
expect "traversal out of /dl is a 404" "$(hit /dl/../wo.toml)" 404 ""
|
||||
expect "the logo is inline in the nav" "$(hit /)" 200 "aria-label=\"writeonce\""
|
||||
expect "admin without token is 401" "$(hit /admin/ch/hello POST "title=X")" 401 "unauthorized"
|
||||
expect "admin edit answers a redirect" "$(hit /admin/ch/hello POST "title=Hello v2" s3cr3t)" 302 ""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/web-app-accept.sh — iteration 16's acceptance gate. Proves the whole
|
||||
# chain at run time, network-free: a temp git remote is built from
|
||||
# docs/examples/writeonce-framework, its file:// URL is substituted into a
|
||||
# docs/examples/writeonce-serve, its file:// URL is substituted into a
|
||||
# temp copy of docs/examples/web-app, then: fetch -> lock -> build -> serve ->
|
||||
# the storefront matrix -> SIGTERM -> restart persistence, plus iteration 17's
|
||||
# library-kind and internal/-boundary checks. The repo itself
|
||||
|
|
@ -33,13 +33,13 @@ cleanup() {
|
|||
trap cleanup EXIT
|
||||
|
||||
# ---- the framework as a git remote; the app pointed at it ----
|
||||
cp -r "$ROOT/docs/examples/writeonce-framework" "$W/fw"
|
||||
cp -r "$ROOT/docs/examples/writeonce-serve" "$W/fw"
|
||||
git -C "$W/fw" init -q
|
||||
git -C "$W/fw" add -A
|
||||
git -C "$W/fw" -c user.email=t@t -c user.name=t commit -qm v01
|
||||
git -C "$W/fw" tag v0.1.0
|
||||
cp -r "$ROOT/docs/examples/web-app" "$W/app"
|
||||
sed -i "s|https://github.com/shoneyj/writeonce-framework|file://$W/fw|" "$W/app/wo.toml"
|
||||
sed -i "s|https://github.com/shoneyj/writeonce-serve|file://$W/fw|" "$W/app/wo.toml"
|
||||
printf '[build]\nruntime = "%s"\n' "$WOVM" >> "$W/app/wo.toml"
|
||||
|
||||
# ---- 1. fetch + lock + build ----
|
||||
|
|
@ -63,10 +63,10 @@ fi
|
|||
# A consumer reaching past the privacy line is WO-E108 at its own `use`.
|
||||
cp -r "$W/app" "$W/app-internal"
|
||||
rm -rf "$W/app-internal/target" "$W/app-internal/wo.lock" "$W/app-internal/.wo-deps"
|
||||
sed -i '1i use framework/internal' "$W/app-internal/main.wo"
|
||||
sed -i '1i use serve/internal' "$W/app-internal/main.wo"
|
||||
out="$("$WOC" "$W/app-internal" 2>&1)"; rc=$?
|
||||
if [[ "$rc" == "1" ]] && printf '%s' "$out" | grep -q 'WO-E108'; then
|
||||
ok "dep boundary: importing framework/internal is WO-E108"
|
||||
ok "dep boundary: importing serve/internal is WO-E108"
|
||||
else
|
||||
bad "internal-boundary" "exit=$rc out=$(printf '%s' "$out" | head -1)"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
-- carry the markup's own nesting and not the method's.
|
||||
-- `${...}` interpolates raw; `{{ ... }}` HTML-escapes through the `esc`
|
||||
-- in scope (here a local one, which is exactly how a program overrides
|
||||
-- wo-html's).
|
||||
-- writeonce-view's).
|
||||
fn esc(t: Text) -> Text {
|
||||
let out = "";
|
||||
let i = 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue