@loadbare/app 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +18 -0
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +8 -3
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/build.d.ts +62 -0
  6. package/dist/build/build.d.ts.map +1 -0
  7. package/dist/build/build.js +148 -0
  8. package/dist/build/build.js.map +1 -0
  9. package/dist/build/cli.d.ts +6 -1
  10. package/dist/build/cli.d.ts.map +1 -1
  11. package/dist/build/cli.js +25 -78
  12. package/dist/build/cli.js.map +1 -1
  13. package/dist/build/declared.d.ts.map +1 -1
  14. package/dist/build/declared.js +3 -0
  15. package/dist/build/declared.js.map +1 -1
  16. package/dist/build/skills-cli.d.ts +2 -1
  17. package/dist/build/skills-cli.d.ts.map +1 -1
  18. package/dist/build/skills-cli.js +8 -1
  19. package/dist/build/skills-cli.js.map +1 -1
  20. package/dist/build/skills.d.ts +2 -0
  21. package/dist/build/skills.d.ts.map +1 -1
  22. package/dist/build/skills.js +2 -0
  23. package/dist/build/skills.js.map +1 -1
  24. package/dist/core/lb-constants.d.ts +14 -0
  25. package/dist/core/lb-constants.d.ts.map +1 -1
  26. package/dist/core/lb-constants.js +25 -0
  27. package/dist/core/lb-constants.js.map +1 -1
  28. package/dist/dev/lb-dev.d.ts +128 -0
  29. package/dist/dev/lb-dev.d.ts.map +1 -0
  30. package/dist/dev/lb-dev.js +289 -0
  31. package/dist/dev/lb-dev.js.map +1 -0
  32. package/dist/hub/lb-apply.d.ts +10 -2
  33. package/dist/hub/lb-apply.d.ts.map +1 -1
  34. package/dist/hub/lb-apply.js +8 -2
  35. package/dist/hub/lb-apply.js.map +1 -1
  36. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  37. package/dist/hub/lb-hub.browser.js +127 -19
  38. package/dist/hub/lb-hub.browser.js.map +1 -1
  39. package/docs/comparison.md +4 -3
  40. package/docs/reference/builder.md +12 -4
  41. package/docs/reference/chrome.md +52 -1
  42. package/docs/reference/data-binding.md +27 -12
  43. package/docs/reference/dev-server.md +171 -0
  44. package/docs/reference/names.md +14 -1
  45. package/docs/reference/overview.md +1 -0
  46. package/docs/reference/server.md +8 -3
  47. package/docs/reference/widgets.md +10 -277
  48. package/docs/testing.md +2 -1
  49. package/docs/tutorials/000-getting-started.md +1 -1
  50. package/docs/tutorials/040-displaying-data.md +1 -1
  51. package/docs/tutorials/090-using-widget-libraries.md +1 -1
  52. package/package.json +4 -2
  53. package/skills/loadbare-app/SKILL.md +30 -12
  54. package/skills/loadbare-app/references/builder.md +12 -4
  55. package/skills/loadbare-app/references/chrome.md +52 -1
  56. package/skills/loadbare-app/references/data-binding.md +27 -12
  57. package/skills/loadbare-app/references/dev-server.md +171 -0
  58. package/skills/loadbare-app/references/names.md +14 -1
  59. package/skills/loadbare-app/references/overview.md +1 -0
  60. package/skills/loadbare-app/references/server.md +8 -3
  61. package/skills/loadbare-app/references/widgets.md +10 -277
@@ -57,7 +57,7 @@ Add a command to package.json that builds the app.
57
57
  ```json
58
58
  {
59
59
  "scripts": {
60
- "dev": "loadbare-app-build --watch & node server.js"
60
+ "dev": "loadbare-app-build --watch & trap 'kill $! 2>/dev/null' EXIT INT TERM; node server.js"
61
61
  }
62
62
  }
63
63
  ```
@@ -159,7 +159,7 @@ runner:
159
159
  ```json
160
160
  {
161
161
  "scripts": {
162
- "dev": "loadbare-app-build --watch & tsx server.js"
162
+ "dev": "loadbare-app-build --watch & trap 'kill $! 2>/dev/null' EXIT INT TERM; tsx server.js"
163
163
  }
164
164
  }
165
165
  ```
@@ -90,7 +90,7 @@ publication — that's the same class shape
90
90
  [Committing a Control](./080-widget-requests.md) already wrote, packaged and
91
91
  given an import specifier instead of living in an application's `src/`.
92
92
  It doesn't cover `lb-table`'s footer destination or `lb-picker`'s
93
- expansion parameters, which are in
93
+ expansion parameters, which each widget's own page covers; see
94
94
  [The Basic Widget Library](../reference/widgets.md).
95
95
 
96
96
  ---
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loadbare/app",
3
3
  "description": "High performance web app framework for server-bound applications",
4
- "version": "0.20.0",
4
+ "version": "0.22.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -22,7 +22,9 @@
22
22
  "./constants": "./dist/core/lb-constants.js",
23
23
  "./types": "./dist/core/lb-types.js",
24
24
  "./server": "./dist/server/lb-server.js",
25
- "./express": "./dist/server/lb-express.js"
25
+ "./express": "./dist/server/lb-express.js",
26
+ "./build": "./dist/build/build.js",
27
+ "./dev": "./dist/dev/lb-dev.js"
26
28
  },
27
29
  "scripts": {
28
30
  "prepublishOnly": "npm run typecheck && npm run test && npm run build",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: loadbare-app
3
- description: Build server-bound web applications with Loadbare/app. Covers the file conventions the builder finds by name (`chrome.html`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `imports.ts`, `<tag>.html`, `<tag>.browser.ts`); the `lb-*` attribute vocabulary that lands query rows on HTML and issues requests (`lb-query`, `lb-column`, `lb-show`, `lb-request`); queries declared with `row(key, run)` and `rows(key, run)`; the URL as the hub's own query `lb-url`, with `lb-url-link`, `lb-url-parm`, `lb-url-push` and `lb-url-unknown`; the user's order and filters as the hub's query `lb-choices`, read by `lb-order` and `lb-filter`; one form pointed at a row by the `--lb-edit`, `--lb-new` and `--lb-release` commands; re-sorting a list with `--lb-sort`; build-time expansion with `exp-*` parameters, `lb-exp-slot` and `lb-exp-template`; custom elements as form-associated controls and row hosts; the Express wiring with `hubRoutes`; and the `@loadbare/widgets` library. Use whenever a task involves `@loadbare/app`, `@loadbare/widgets`, `loadbare-app-build`, an `lb-` attribute, or a page, query, request or custom element file in a Loadbare application. The model is not React, htmx or REST, and cannot be inferred from them.
3
+ description: Build server-bound web applications with Loadbare/app. Covers the file conventions the builder finds by name (`chrome.html`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `imports.ts`, `<tag>.html`, `<tag>.browser.ts`); the `lb-*` attribute vocabulary that lands query rows on HTML and issues requests (`lb-query`, `lb-column`, `lb-show`, `lb-request`); queries declared with `row(key, run)` and `rows(key, run)`; the URL as the hub's own query `lb-url`, with `lb-url-link`, `lb-url-parm`, `lb-url-push` and `lb-url-unknown`; the user's order and filters as the hub's query `lb-choices`, read by `lb-order` and `lb-filter`; whether the page loaded and the server answers as the hub's query `lb-hub`; one form pointed at a row by the `--lb-edit`, `--lb-new` and `--lb-release` commands; re-sorting a list with `--lb-sort`; build-time expansion with `exp-*` parameters, `lb-exp-slot` and `lb-exp-template`; custom elements as form-associated controls and row hosts; the Express wiring with `hubRoutes`; and the `@loadbare/widgets` library. Use whenever a task involves `@loadbare/app`, `@loadbare/widgets`, `loadbare-app-build`, an `lb-` attribute, or a page, query, request or custom element file in a Loadbare application. The model is not React, htmx or REST, and cannot be inferred from them.
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  package: "@loadbare/app"
@@ -48,7 +48,9 @@ carries, so their import-time code runs then; open a database in
48
48
  `contextFor`, not at import. Then start the server: `createHub` refuses a page that declares a reserved name at startup,
49
49
  and the server console reports a query that answers with the wrong kind.
50
50
  Restart the server after adding or changing a `.queries.ts` or
51
- `.requests.ts` file.
51
+ `.requests.ts` file, or run it under the dev server, `devServer` from
52
+ `@loadbare/app/dev`, which rebuilds and restarts it after each change; see
53
+ [`references/dev-server.md`](references/dev-server.md).
52
54
 
53
55
  ## The shape of an application
54
56
 
@@ -73,7 +75,8 @@ server.ts
73
75
  The chrome carries `<lb-hub>` inside `<body>`, an empty `<main>` inside the
74
76
  hub, and `<script src="/client.js" defer>`. Everything a user touches sits
75
77
  inside `<lb-hub>`. The chrome belongs to no page, so it names only the
76
- hub's queries, `lb-url` and `lb-choices`, and sends no declared request.
78
+ hub's queries, `lb-url`, `lb-choices` and `lb-hub`, and sends no declared
79
+ request.
77
80
 
78
81
  A page is an HTML fragment that lands in `<main>`:
79
82
 
@@ -191,8 +194,8 @@ row is filled from the nested query's last answer, whatever order they land
191
194
  in.
192
195
 
193
196
  **A `rows` query declares its order, and the hub places every row by it.**
194
- An edit sent from inside a list leaves that list's rows where they stand,
195
- stamped `lb-row-unsorted` where out of order, until the page loads, the
197
+ An edit sent from inside a list, or from a form `--lb-edit` pointed at one
198
+ of its rows, leaves that list's rows where they stand, stamped `lb-row-unsorted` where out of order, until the page loads, the
196
199
  user's order or filter changes, or a button with `commandfor` naming the
197
200
  list sends `command="--lb-sort"`, which places them with no round trip;
198
201
  show it by `[lb-query-unsorted-count]`. Never refresh to make rows hold
@@ -217,6 +220,15 @@ text for contains. A choice shows with no round trip, then the server's
217
220
  page changes. A filter that decides which rows the server sends is a query
218
221
  parm.
219
222
 
223
+ **A page that failed to load says so.** The hub serves a third query,
224
+ `lb-hub`, and stamps its columns on `<lb-hub>` too: `lb-load-state`
225
+ (`loading`, `loaded`, `failed`), `lb-server-state` (`reachable`, `failing`,
226
+ `unreachable`), `lb-error` (`network`, `timeout` or the HTTP status),
227
+ `lb-pending` and `lb-as-of`. Show a server that is down from the chrome's
228
+ stylesheet, `lb-hub[lb-load-state="failed"] .load-failed`, rather than
229
+ leaving an empty page; see
230
+ [`references/chrome.md`](references/chrome.md#the-hubs-state).
231
+
220
232
  **Groups are templates.** A `<template lb-group>` holds a heading and one
221
233
  nested `<template>`, the next level or the row template; the nth breaks on
222
234
  the order's nth term, to any depth. Contents land before the nested
@@ -475,12 +487,10 @@ and a condition need none. A custom element exists to be a control of its
475
487
  own, or to act on rows once they land through the hook the hub calls,
476
488
  `lbRowsLanded` (the `HubLifecycle` interface).
477
489
 
478
- Before writing one, check `@loadbare/widgets`: `lb-input`, `lb-select`,
479
- `lb-options`, `lb-picker`, `lb-table`, `lb-unknown-page`, `lb-confirm`,
480
- `lb-form`, `lb-insert-dialog`, `lb-master`, `lb-checkbox`, `lb-number`,
481
- `lb-date`. The first four are
482
- form-associated controls that carry `lb-column` and `lb-request`
483
- themselves. Install the package and list it in `src/imports.ts`:
490
+ Before writing one, check `@loadbare/widgets`, the basic widget set, which
491
+ the loadbare-widgets skill documents. Its controls are form-associated and
492
+ carry `lb-column` and `lb-request` themselves. Install the package and list
493
+ it in `src/imports.ts`:
484
494
 
485
495
  ```ts
486
496
  export default ["@loadbare/widgets"];
@@ -510,9 +520,17 @@ with the package, so no network is needed.
510
520
  the request context.
511
521
  - [`references/builder.md`](references/builder.md) — the builder's options,
512
522
  what it reads, and where it looks.
523
+ - [`references/dev-server.md`](references/dev-server.md) — `devServer` from
524
+ `@loadbare/app/dev`, and the parts to assemble another from.
513
525
  - [`references/css.md`](references/css.md) — how stylesheets are ordered.
514
526
  - [`references/widgets.md`](references/widgets.md) — the basic widget
515
- library.
527
+ library as a package; each widget is in the loadbare-widgets skill.
516
528
  - [`references/names.md`](references/names.md) — every name Loadbare
517
529
  owns, the constants, and the section defining each. Check a name here
518
530
  before designing around something the other references do not mention.
531
+
532
+ ## The other loadbare skills
533
+
534
+ `npx loadbare-app skills install` puts this skill where a coding agent finds
535
+ it. `npx loadbare-widgets skills install` and `npx loadbare-db skills
536
+ install` do the same for the loadbare-widgets and loadbare-db skills.
@@ -16,19 +16,27 @@ loadbare-app-build [--src src] [--out dist] [--watch] [--minify]
16
16
  | `--watch` | Build once, then again on every change under `--src` |
17
17
  | `--minify` | Minify `client.js` and `app.css` |
18
18
 
19
- Run it from the application's own `package.json`:
19
+ During development, [the dev server](./dev-server.md) runs the build in
20
+ watch mode, and restarts the server when it needs to. To run the builder and
21
+ the server side by side without it, from the application's own
22
+ `package.json`:
20
23
 
21
24
  ```json
22
25
  {
23
26
  "scripts": {
24
- "dev": "loadbare-app-build --watch & tsx server.ts"
27
+ "dev": "loadbare-app-build --watch & trap 'kill $! 2>/dev/null' EXIT INT TERM; tsx server.ts"
25
28
  }
26
29
  }
27
30
  ```
28
31
 
32
+ The trap stops the watcher when the server exits. Without it, a
33
+ backgrounded watcher ignores Ctrl-C under `sh` and outlives the server.
34
+
29
35
  `--watch` watches every `.html`, `.ts`, and `.css` file under `--src`, and
30
- reports a failed build to the console without stopping. It runs no dev server
31
- and reloads no browser.
36
+ reports a failed build to the console without stopping. It runs no server
37
+ and reloads no browser. A program that builds or watches as part of its own
38
+ work imports `buildApp` and `watchApp` from `@loadbare/app/build` instead;
39
+ see [Assembling your own](./dev-server.md#assembling-your-own).
32
40
 
33
41
  `--minify` leaves `app.html` alone.
34
42
 
@@ -277,7 +277,58 @@ browser after a successful 200, and there is no server-delivered 404.
277
277
 
278
278
  `@loadbare/widgets` ships this dialog as `<lb-unknown-page>`, for a chrome
279
279
  that would rather write one tag; see
280
- [The Basic Widget Library](./widgets.md#lb-unknown-page).
280
+ [The Basic Widget Library](./widgets.md).
281
+
282
+ ## The hub's state
283
+
284
+ The hub serves a third query of its own, `lb-hub`: one row, keyed by
285
+ `lb-path`, saying whether the page's data arrived and whether the server
286
+ answers. A page whose load failed shows its markup with nothing landed on
287
+ it, and without this row nothing on the page can tell a list that is empty
288
+ from a server that is down.
289
+
290
+ | Column | Holds |
291
+ |-------------------|------------------------------------------------------------------------|
292
+ | `lb-load-state` | `loading`, `loaded` or `failed`: the page load for the URL showing |
293
+ | `lb-server-state` | `reachable`, `failing` or `unreachable`: how the latest round trip went |
294
+ | `lb-error` | Empty, or why the latest round trip failed |
295
+ | `lb-pending` | How many round trips are in flight, page loads and requests together |
296
+ | `lb-as-of` | When rows last came from the server, as an ISO timestamp |
297
+
298
+ - `lb-load-state` is `loading` while the page load is in flight, `loaded`
299
+ once the server answered it, and `failed` when it did not. A request whose
300
+ answer moves the URL and carries the page's queries loads the page too. A
301
+ load overtaken by a newer one says nothing, and a request that fails
302
+ later leaves the page `loaded`, since what it shows did arrive.
303
+ - `lb-server-state` is `reachable` when the server answered, a refusal
304
+ included; `failing` when it answered with an HTTP error status; and
305
+ `unreachable` when no answer came, because the network failed or the
306
+ ten-second deadline passed. It is empty until the first round trip
307
+ returns.
308
+ - `lb-error` is `network`, `timeout`, or the status the server answered
309
+ with, such as `500`. The next answer empties it.
310
+ - `lb-as-of` is unchanged by a refusal, which brings no rows.
311
+
312
+ The hub also stamps each column on `<lb-hub>` itself, as the attribute of
313
+ the same name, and leaves off one that is empty. A stylesheet reaches the
314
+ whole document that way, and the markup names the query only to show its
315
+ text:
316
+
317
+ ```css
318
+ .load-failed { display: none; }
319
+ lb-hub[lb-load-state="failed"] .load-failed { display: block; }
320
+ lb-hub[lb-load-state="failed"] main { opacity: 0.4; }
321
+ lb-hub:not([lb-pending="0"]) .banner { cursor: progress; }
322
+ ```
323
+
324
+ ```html
325
+ <p class="load-failed" lb-query="lb-hub">
326
+ This page did not load: the server is <span lb-column="lb-server-state"></span>
327
+ (<span lb-column="lb-error"></span>).
328
+ </p>
329
+ ```
330
+
331
+ Nothing writes `lb-hub` but the hub. The builder refuses a request for it.
281
332
 
282
333
  ## Preventing the first-load blink
283
334
 
@@ -119,9 +119,9 @@ from its nearest ancestor row: the live row it is in, or the element a
119
119
  it reads from that query's rows.
120
120
 
121
121
  An element with `lb-query` and `lb-column` both shows the rows of its own
122
- query and holds the column of the row around it. This
123
- [`<lb-options>`](./widgets.md#lb-options) lists the accounts and holds its
124
- row's `account`:
122
+ query and holds the column of the row around it. This `<lb-options>`, from
123
+ [`@loadbare/widgets`](./widgets.md), lists the accounts and holds its row's
124
+ `account`:
125
125
 
126
126
  ```html
127
127
  <lb-options lb-query="accounts" lb-column="account">
@@ -167,8 +167,8 @@ option for the value shows no selection.
167
167
 
168
168
  The hub hands every value to the browser as the server sent it. What a
169
169
  number, a date or a null looks like is decided in the query, or by a custom
170
- element that formats it, such as
171
- [`lb-number` and `lb-date`](./widgets.md#lb-number-and-lb-date). Send a
170
+ element that formats it, such as `lb-number` and `lb-date` in
171
+ [`@loadbare/widgets`](./widgets.md). Send a
172
172
  column the user sorts or filters raw, and format it in the element.
173
173
 
174
174
  ### Checkboxes
@@ -312,9 +312,12 @@ puts it somewhere else, so all rows landing again unchanged move nothing.
312
312
  A landing that answers a request sent from inside a list moves none of that
313
313
  list's rows. The request is from inside the list when the list is the
314
314
  nearest element carrying `lb-query` around the element that sent it: a
315
- control in a live row, or a ghost row. A form elsewhere on the page is not
316
- inside the list. A user who changes a value sees the value change, and the
317
- row stays where they were working:
315
+ control in a live row, or a ghost row. A form that
316
+ [`--lb-edit`](#editing-a-row-in-one-form) pointed at a live row finishes an
317
+ edit begun in that row, so its request is from inside the row's list,
318
+ wherever the form sits. Any other form elsewhere on the page is not inside
319
+ the list. A user who changes a value sees the value change, and the row
320
+ stays where they were working:
318
321
 
319
322
  | The answer | The row |
320
323
  |------------------------------------------|---------------------------------------------------|
@@ -941,7 +944,8 @@ nothing is not sent.
941
944
 
942
945
  For a query the hub serves, `lb-url` or `lb-choices`, the hub answers
943
946
  `lb-row-update` itself, with no round trip; see
944
- [Query parms](./chrome.md#query-parms) and [Choices](#choices).
947
+ [Query parms](./chrome.md#query-parms) and [Choices](#choices). The third,
948
+ `lb-hub`, takes no request.
945
949
 
946
950
  A key comes from a live row, or from an element a `row` landed on, so all
947
951
  three work against either kind:
@@ -958,8 +962,11 @@ update from a form or a live row sends every control it gathers. The
958
962
  application writes one `rowUpdate` for both, setting the columns `values`
959
963
  names, as an SQL UPDATE does.
960
964
 
961
- After a successful insert gathered from a form, the hub resets the form. A
962
- failed insert leaves the entry for the user to correct.
965
+ After a successful insert gathered from a form, the hub resets the form,
966
+ unless the answer landed the new row on it. A form over a `row` query not
967
+ yet entered, whose insert answers `url()` to the new key, then shows the row
968
+ just saved, on record. A failed insert leaves the entry for the user to
969
+ correct.
963
970
 
964
971
  Leave `lb-request` off a control inside a form or a live row that sends its
965
972
  own update, or the same edit is written twice.
@@ -1033,6 +1040,12 @@ An `lb-show` in the form reads from the row it was pointed at. With no row,
1033
1040
  every column is null, so `lb-show="!id"` shows only while entering a new
1034
1041
  row.
1035
1042
 
1043
+ A request from a form Edit pointed is sent from inside the row's list, so
1044
+ the row stays where the user was working, even when the filter would now
1045
+ hide it or the order would move it; see
1046
+ [Rows the user is working in](#rows-the-user-is-working-in). A form New
1047
+ pointed is not inside any list, and its rows are placed by the order.
1048
+
1036
1049
  A successful request from the form lets it go, as Cancel does: the form
1037
1050
  empties, its controls return to their defaults, and the stamps go. A failed
1038
1051
  one leaves the form as the user had it, to correct.
@@ -1245,7 +1258,9 @@ A reset of any form, whether the hub's, a reset button's or a script's,
1245
1258
  returns its controls to their defaults, and the hub removes
1246
1259
  `lb-column-value` from each of them. A cancelled reset keeps both.
1247
1260
 
1248
- A page load that fails lands nothing and is reported to the console. A load
1261
+ A page load that fails lands nothing and is reported to the console, and
1262
+ the query `lb-hub` says so; see [The hub's state](./chrome.md#the-hubs-state).
1263
+ A load
1249
1264
  started by a request for `lb-url` stamps the element that sent it, as any
1250
1265
  request stamps its element.
1251
1266
 
@@ -0,0 +1,171 @@
1
+ # The Dev Server
2
+
3
+ `@loadbare/app/dev` runs an application while it is being written. After
4
+ any change, to markup, CSS, a query, a handler, a server module or the
5
+ server's entry file, the developer refreshes the browser. Nothing is ever
6
+ restarted by hand, a refresh never meets a refused connection, and it never
7
+ loads a half-written `client.js`.
8
+
9
+ ```ts
10
+ // dev.ts
11
+ import { devServer } from "@loadbare/app/dev";
12
+
13
+ await devServer({ server: "server.ts" });
14
+ ```
15
+
16
+ ```json
17
+ {
18
+ "scripts": {
19
+ "dev": "node dev.ts"
20
+ }
21
+ }
22
+ ```
23
+
24
+ ## Why a refresh is enough
25
+
26
+ - **The static output is read per request.** The server reads
27
+ `dist/app.html`, and sends `client.js` and `app.css` from disk, so markup,
28
+ CSS and browser code need only a rebuild.
29
+ - **Only server modules need a restart.** `dist/pages.ts` and everything it
30
+ imports are loaded once, and Node cannot unload a module. A new process is
31
+ the honest answer, and a Loadbare server starts in well under a second.
32
+ - **The URL holds the page's state.** A refresh returns the user to the
33
+ same page, row and query parms, so swapping code into a live page buys
34
+ almost nothing.
35
+
36
+ ## What it does
37
+
38
+ ```
39
+ browser --> devServer :port --> the application's server :appPort (a child)
40
+ |
41
+ +-- the build, watching src (in process)
42
+ ```
43
+
44
+ 1. Builds `dist/`, then rebuilds whenever an `.html`, `.ts` or `.css` file
45
+ under `src` changes. A burst of changes, such as an editor's save or a
46
+ branch switch, is one build.
47
+ 2. Runs the application's server as a child process, exactly as it runs in
48
+ production, on `appPort`.
49
+ 3. Restarts that server after a build when a server-side file changed: any
50
+ `.ts` under `src` other than a `.browser.ts`, or a change to the page
51
+ registry `dist/pages.ts`, which a page gained or lost changes. It also
52
+ restarts it when the server's entry file changes. CSS, HTML and browser
53
+ code never restart it.
54
+ 4. Holds every request while a build runs or the server restarts, and sends
55
+ it on when both are done.
56
+ 5. Keeps serving the last good build when a build fails, and says why in
57
+ the console.
58
+ 6. Answers 502, "The app server is down; see the dev console.", when the
59
+ server has exited, for example on a throw at import, and starts it again
60
+ on the next save.
61
+ 7. Says plainly when its port is in use, and exits.
62
+ 8. Passes everything else through, method, path, headers and body,
63
+ `POST /lb/*` included.
64
+ 9. Stops the server however the dev server ends: Ctrl-C, a kill, or a
65
+ crash.
66
+
67
+ The application's server reads its port from the environment, as the
68
+ [server recipe](./server.md) does:
69
+
70
+ ```ts
71
+ app.listen(Number(process.env.PORT ?? 8787));
72
+ ```
73
+
74
+ ## Options
75
+
76
+ Every option but `server` has a default.
77
+
78
+ | Option | Default | Meaning |
79
+ |-----------|----------------------------------|--------------------------------------------------------|
80
+ | `server` | (required) | The application's server entry file |
81
+ | `port` | `PORT` from the environment, or `8787` | The port the browser uses |
82
+ | `host` | `127.0.0.1` | The host that port listens on |
83
+ | `appPort` | `port + 10000` | Where the application's server listens |
84
+ | `portEnv` | `PORT` | The variable the application's server reads its port from |
85
+ | `node` | `["--env-file-if-exists=.env"]` | Node's arguments before the entry file |
86
+ | `src` | `src` | The builder's source tree |
87
+ | `out` | `dist` | The builder's output |
88
+ | `log` | the console | Where progress goes, one line at a time |
89
+
90
+ A server written in TypeScript runs under Node's own type stripping, Node
91
+ 22.18 and later. For a runner such as tsx, name it in `node`:
92
+ `node: ["--import", "tsx"]`.
93
+
94
+ `devServer` resolves once the first build is done and the server accepts
95
+ connections, with `{ port, close() }`.
96
+
97
+ ## Assembling your own
98
+
99
+ `devServer` is a short composition of parts that are exported on their own.
100
+ An application that wants something else, such as other files that restart
101
+ the server, a second server, or a reload pushed to the browser, builds its
102
+ own from them.
103
+
104
+ | Part | From | Does |
105
+ |--------------|------------------------|--------------------------------------------------------------|
106
+ | `watchApp` | `@loadbare/app/build` | Builds, rebuilds on a change, and reports each build |
107
+ | `buildApp` | `@loadbare/app/build` | Builds once |
108
+ | `appServer` | `@loadbare/app/dev` | The application's server as a child process, restarted on demand |
109
+ | `gate` | `@loadbare/app/dev` | Holds requests while any reason to hold them stands |
110
+ | `devProxy` | `@loadbare/app/dev` | The browser's port, passing requests through a gate |
111
+
112
+ `watchApp({ src, out, minify, onStart, onBuilt })` builds at once, then
113
+ again on each change. `onStart(changed)` is called as a build begins, with
114
+ the files under `src` that changed, relative to it, and an empty list for
115
+ the first. `onBuilt(outcome)` is called as it ends, with `{ ok: true,
116
+ changed, built }` or `{ ok: false, changed, error }`; `built.files` lists
117
+ what was written. Builds never overlap: changes during one are gathered into
118
+ the next. It returns `{ ready, close() }`, where `ready` resolves with the
119
+ first build's outcome. Paths resolve against the working directory.
120
+
121
+ `buildApp({ src, out, minify })` builds once, and throws when the build
122
+ fails. A build writes nothing until every output is ready, so a failed one
123
+ leaves the last good build in place. `loadbare-app-build` is a thin command
124
+ over these two.
125
+
126
+ `appServer({ entry, port, portEnv, node, log })` returns `{ restart(),
127
+ stop(), state }`. `restart()` stops the server if it runs, starts it, and
128
+ resolves once it accepts connections, or has exited instead. `state` is
129
+ `starting`, `running`, `down` for a server that exited on its own, or
130
+ `stopped`. Once stopped, it never starts again. The child is killed when
131
+ this process exits, however it exits.
132
+
133
+ `gate()` returns `{ close(reason), open(reason), wait(), isOpen }`. Each
134
+ `close` is matched by its own `open`, and `wait()` resolves once every
135
+ close has been. A build and a restart can each hold requests without one
136
+ ending the other's hold.
137
+
138
+ `devProxy({ port, host, target, gate, isDown })` listens on `port` and
139
+ passes every request to `target` once the gate is open, answering 502 while
140
+ `isDown()` is true. It resolves with the listening server, and rejects with
141
+ a plain message when the port is in use.
142
+
143
+ A dev server that also restarts on a change to `config/`:
144
+
145
+ ```ts
146
+ import { watch } from "node:fs";
147
+ import { watchApp } from "@loadbare/app/build";
148
+ import { appServer, devProxy, gate } from "@loadbare/app/dev";
149
+
150
+ const held = gate();
151
+ const app = appServer({ entry: "server.ts", port: 18787 });
152
+ const restart = async () => {
153
+ held.close("server");
154
+ await app.restart();
155
+ held.open("server");
156
+ };
157
+
158
+ const watcher = watchApp({
159
+ onStart: () => held.close("build"),
160
+ onBuilt: async (outcome) => {
161
+ console.log(outcome.ok ? outcome.built.summary : outcome.error.message);
162
+ if (outcome.changed.some((f) => f.endsWith(".ts"))) await restart();
163
+ held.open("build");
164
+ },
165
+ });
166
+ watch("config", { recursive: true }, () => void restart());
167
+
168
+ await watcher.ready;
169
+ await restart();
170
+ await devProxy({ port: 8787, target: 18787, gate: held, isDown: () => app.state === "down" });
171
+ ```
@@ -103,6 +103,7 @@ assigns one.
103
103
  | `lb-request-pending` | Hub | [Request state](./data-binding.md#request-state) |
104
104
  | `lb-request-error` | Hub | [Request state](./data-binding.md#request-state) |
105
105
  | `lb-request-refused` | Hub | [Refusals](./data-binding.md#refusals) |
106
+ | `lb-load-state`, `lb-server-state`, `lb-error`, `lb-pending`, `lb-as-of` on `<lb-hub>` | Hub | [The hub's state](./chrome.md#the-hubs-state) |
106
107
  | `lb-show` on a `<template>` | Hub and builder | [Where an absent element is](./data-binding.md#where-an-absent-element-is) |
107
108
  | `lb-page` | Builder | [What the builder writes](./builder.md#what-the-builder-writes) |
108
109
  | `lb-page-title` | Builder | [The page title](./page-files.md#the-page-title) |
@@ -110,12 +111,13 @@ assigns one.
110
111
  ## Reserved queries
111
112
 
112
113
  The hub serves these in the browser. Every page and the chrome may name
113
- them, and a page declares neither.
114
+ them, and a page declares none.
114
115
 
115
116
  | Query | Kind | Key | Columns | Defined in |
116
117
  | ------------ | ----- | --------- | --------------------------------------------------------------- | ------------------------------------- |
117
118
  | `lb-url` | `row` | `lb-path` | `lb-path`, `lb-page-label`, `lb-page-unknown`, every query parm | [The URL](./chrome.md#the-url) |
118
119
  | `lb-choices` | `row` | `lb-path` | `lb-path`, every choice | [Choices](./data-binding.md#choices) |
120
+ | `lb-hub` | `row` | `lb-path` | `lb-path`, `lb-load-state`, `lb-server-state`, `lb-error`, `lb-pending`, `lb-as-of` | [The hub's state](./chrome.md#the-hubs-state) |
119
121
 
120
122
  No query parm is reserved: a column of `lb-url` whose name begins with
121
123
  `lb-` is never a query parm.
@@ -175,6 +177,8 @@ accessor or field named `lb` and a capital.
175
177
  | -------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- |
176
178
  | `@loadbare/app/server` | `row`, `rows`, `patch`, `url`, `choices`, `refusal`, `createHub`, and the types `Queries`, `Requests`, `HubContext`, `Hub` | [Page Files](./page-files.md), [The data channel](./server.md#the-data-channel) |
177
179
  | `@loadbare/app/express` | `hubRoutes` | [The Express Server](./server.md) |
180
+ | `@loadbare/app/dev` | `devServer`, `appServer`, `gate`, `devProxy`, `DOWN_MESSAGE` | [The Dev Server](./dev-server.md) |
181
+ | `@loadbare/app/build` | `buildApp`, `watchApp` | [Assembling your own](./dev-server.md#assembling-your-own) |
178
182
  | `@loadbare/app/types` | `HubRequest`, `ResponseItem`, `HubRequestDone`, `HubLifecycle`, and the rules the hub applies: `readAs`, `parseFilter`, `passes`, `passesTerm` | [The data channel](./server.md#the-data-channel), [Order](./data-binding.md#order), [Filters](./data-binding.md#filters) |
179
183
  | `@loadbare/app` | `applyRow` | [Filling a subtree by hand](./custom-elements.md#filling-a-subtree-by-hand) |
180
184
  | `@loadbare/app/constants` | Every name on this page; see [Constants](#constants) | [Code](./custom-elements.md#code) |
@@ -244,6 +248,15 @@ imports them rather than writing a string.
244
248
  | `URL_COLUMN_PATH` | `lb-path` |
245
249
  | `URL_COLUMN_PAGE_LABEL` | `lb-page-label` |
246
250
  | `URL_COLUMN_PAGE_UNKNOWN` | `lb-page-unknown` |
251
+ | `HUB_QUERY` | `lb-hub` |
252
+ | `HUB_COLUMN_LOAD_STATE` | `lb-load-state` |
253
+ | `HUB_COLUMN_SERVER_STATE` | `lb-server-state` |
254
+ | `HUB_COLUMN_ERROR` | `lb-error` |
255
+ | `HUB_COLUMN_PENDING` | `lb-pending` |
256
+ | `HUB_COLUMN_AS_OF` | `lb-as-of` |
257
+ | `HUB_LOAD_LOADING`, `HUB_LOAD_LOADED`, `HUB_LOAD_FAILED` | `loading`, `loaded`, `failed` |
258
+ | `HUB_SERVER_REACHABLE`, `HUB_SERVER_FAILING`, `HUB_SERVER_UNREACHABLE` | `reachable`, `failing`, `unreachable` |
259
+ | `HUB_ERROR_NETWORK`, `HUB_ERROR_TIMEOUT` | `network`, `timeout` |
247
260
  | `ATTR_URL_LINK` | `lb-url-link` |
248
261
  | `ATTR_URL_PARM` | `lb-url-parm` |
249
262
  | `ATTR_URL_PUSH` | `lb-url-push` |
@@ -19,6 +19,7 @@ serves, and the custom elements those pages are made of.
19
19
  | [`imports.ts`](./builder.md#widgets-from-packages) | The packages this app takes widgets from |
20
20
  | [`loadbare-app-build`](./builder.md#running-the-builder) | Configuring the build command |
21
21
  | [CSS](./css.md) | How the builder packages css |
22
+ | [The dev server](./dev-server.md) | Refresh after any change, never restart |
22
23
 
23
24
  ## Application Pages
24
25
 
@@ -35,7 +35,7 @@ app.get(/.*/, (_req, res) =>
35
35
  res.type("html").send(readFileSync(path.join(DIST, "app.html"), "utf-8")),
36
36
  );
37
37
 
38
- app.listen(8787);
38
+ app.listen(Number(process.env.PORT ?? 8787));
39
39
  ```
40
40
 
41
41
  ## What the server serves
@@ -96,16 +96,21 @@ The limit covers every page's requests.
96
96
  ## Running the server
97
97
 
98
98
  Run the server under a TypeScript-capable runner. The builder writes
99
- `dist/pages.ts`, which exports `hub`, as TypeScript:
99
+ `dist/pages.ts`, which exports `hub`, as TypeScript. During development,
100
+ [the dev server](./dev-server.md) runs it, and restarts it when a server
101
+ module changes:
100
102
 
101
103
  ```json
102
104
  {
103
105
  "scripts": {
104
- "dev": "loadbare-app-build --watch & tsx server.ts"
106
+ "dev": "node dev.ts"
105
107
  }
106
108
  }
107
109
  ```
108
110
 
111
+ The server reads its port from `PORT`, which is how the dev server tells it
112
+ where to listen.
113
+
109
114
  Node 22.18 and later also runs it with no runner at all, since Node strips
110
115
  TypeScript types itself. `dist/pages.ts` imports each `.requests.ts` and
111
116
  `.queries.ts` file by its full name, extension included, because Node looks