@loadbare/app 0.4.0 → 0.5.1

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 (165) hide show
  1. package/README.md +53 -82
  2. package/dist/build/assemble.d.ts +7 -5
  3. package/dist/build/assemble.d.ts.map +1 -1
  4. package/dist/build/assemble.js +29 -9
  5. package/dist/build/cli.d.ts +20 -12
  6. package/dist/build/cli.d.ts.map +1 -1
  7. package/dist/build/cli.js +34 -16
  8. package/dist/build/elements.d.ts +15 -29
  9. package/dist/build/elements.d.ts.map +1 -1
  10. package/dist/build/elements.js +25 -111
  11. package/dist/build/expand.d.ts +1 -1
  12. package/dist/build/expand.js +1 -1
  13. package/dist/build/format.d.ts +6 -3
  14. package/dist/build/format.d.ts.map +1 -1
  15. package/dist/build/format.js +6 -3
  16. package/dist/build/locations.d.ts +14 -37
  17. package/dist/build/locations.d.ts.map +1 -1
  18. package/dist/build/locations.js +31 -69
  19. package/dist/build/origins.d.ts +109 -0
  20. package/dist/build/origins.d.ts.map +1 -0
  21. package/dist/build/origins.js +270 -0
  22. package/dist/core/lb-constants.d.ts +1 -0
  23. package/dist/core/lb-constants.d.ts.map +1 -1
  24. package/dist/core/lb-constants.js +15 -8
  25. package/dist/core/lb-types.d.ts +2 -2
  26. package/dist/core/lb-types.d.ts.map +1 -1
  27. package/dist/hub/lb-apply.js +1 -1
  28. package/dist/hub/lb-hub.d.ts.map +1 -1
  29. package/dist/hub/lb-hub.js +44 -17
  30. package/dist/hub/lb-rows.d.ts.map +1 -1
  31. package/dist/hub/lb-rows.js +3 -3
  32. package/dist/server/lb-express.d.ts.map +1 -1
  33. package/dist/server/lb-server.d.ts +5 -4
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/tests/assemble.test.js +11 -4
  36. package/dist/tests/elements.test.js +47 -51
  37. package/dist/tests/expand.test.d.ts +1 -1
  38. package/dist/tests/expand.test.js +2 -2
  39. package/dist/tests/fixtures/elements/collision/imports.d.ts +3 -0
  40. package/dist/tests/fixtures/elements/collision/imports.d.ts.map +1 -0
  41. package/dist/tests/fixtures/elements/collision/imports.js +1 -0
  42. package/dist/tests/fixtures/elements/manifest/imports.d.ts +3 -0
  43. package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +1 -0
  44. package/dist/tests/fixtures/elements/manifest/imports.js +1 -0
  45. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +3 -0
  46. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +1 -0
  47. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +1 -0
  48. package/dist/tests/fixtures/elements/{collision/elements.d.ts → manifest-not-array/imports.d.ts} +1 -1
  49. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +1 -0
  50. package/dist/tests/fixtures/elements/manifest-not-array/imports.js +1 -0
  51. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts +2 -0
  52. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts.map +1 -0
  53. package/dist/tests/fixtures/elements/pkg/acme-widget.js +1 -0
  54. package/dist/tests/lb-express.test.js +1 -1
  55. package/dist/tests/origins.test.d.ts +10 -0
  56. package/dist/tests/origins.test.d.ts.map +1 -0
  57. package/dist/tests/origins.test.js +326 -0
  58. package/dist/tests/pages.test.js +3 -3
  59. package/dist/tests/styles.test.js +7 -4
  60. package/docs/reference/builder.md +128 -0
  61. package/docs/reference/chrome.md +75 -0
  62. package/docs/reference/css.md +44 -0
  63. package/docs/reference/custom-elements.md +327 -0
  64. package/docs/reference/data-binding.md +240 -0
  65. package/docs/reference/overview.md +38 -0
  66. package/docs/reference/page-files.md +175 -0
  67. package/docs/reference/server.md +123 -0
  68. package/docs/reference/widgets.md +163 -0
  69. package/docs/roadmap.md +130 -0
  70. package/docs/testing.md +228 -0
  71. package/docs/theory.md +344 -223
  72. package/docs/tutorials/000-getting-started.md +86 -0
  73. package/docs/tutorials/010-pages-and-navigation.md +129 -0
  74. package/docs/tutorials/020-css.md +103 -0
  75. package/docs/tutorials/030-html-decomposition.md +79 -0
  76. package/docs/tutorials/040-displaying-data.md +169 -0
  77. package/docs/tutorials/050-actions.md +77 -0
  78. package/docs/tutorials/060-custom-element-code.md +73 -0
  79. package/docs/tutorials/065-conditional-rendering.md +161 -0
  80. package/docs/tutorials/070-displaying-a-list.md +137 -0
  81. package/docs/tutorials/072-inserting-into-a-list.md +88 -0
  82. package/docs/tutorials/074-deleting-from-a-list.md +77 -0
  83. package/docs/tutorials/076-updating-a-list-item.md +86 -0
  84. package/docs/tutorials/080-widget-requests.md +124 -0
  85. package/docs/tutorials/090-using-widget-libraries.md +75 -0
  86. package/package.json +10 -18
  87. package/dist/client.js +0 -522
  88. package/dist/demo-static/src/widgets/app-box.d.ts +0 -15
  89. package/dist/demo-static/src/widgets/app-box.d.ts.map +0 -1
  90. package/dist/demo-static/src/widgets/app-box.js +0 -19
  91. package/dist/tests/fixtures/elements/collision/elements.d.ts.map +0 -1
  92. package/dist/tests/fixtures/elements/collision/elements.js +0 -3
  93. package/dist/tests/fixtures/elements/manifest/elements.d.ts +0 -5
  94. package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +0 -1
  95. package/dist/tests/fixtures/elements/manifest/elements.js +0 -3
  96. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +0 -5
  97. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +0 -1
  98. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +0 -3
  99. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +0 -5
  100. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +0 -1
  101. package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +0 -3
  102. package/dist/tests/golden.test.d.ts +0 -19
  103. package/dist/tests/golden.test.d.ts.map +0 -1
  104. package/dist/tests/golden.test.js +0 -60
  105. package/dist/tests/helpers/window.d.ts +0 -43
  106. package/dist/tests/helpers/window.d.ts.map +0 -1
  107. package/dist/tests/helpers/window.js +0 -78
  108. package/dist/tests/lb-input.test.d.ts +0 -9
  109. package/dist/tests/lb-input.test.d.ts.map +0 -1
  110. package/dist/tests/lb-input.test.js +0 -78
  111. package/dist/tests/lb-list.test.d.ts +0 -12
  112. package/dist/tests/lb-list.test.d.ts.map +0 -1
  113. package/dist/tests/lb-list.test.js +0 -44
  114. package/dist/tests/lb-options.test.d.ts +0 -10
  115. package/dist/tests/lb-options.test.d.ts.map +0 -1
  116. package/dist/tests/lb-options.test.js +0 -121
  117. package/dist/tests/lb-picker.test.d.ts +0 -14
  118. package/dist/tests/lb-picker.test.d.ts.map +0 -1
  119. package/dist/tests/lb-picker.test.js +0 -59
  120. package/dist/tests/lb-select.test.d.ts +0 -9
  121. package/dist/tests/lb-select.test.d.ts.map +0 -1
  122. package/dist/tests/lb-select.test.js +0 -71
  123. package/dist/tests/lb-table.test.d.ts +0 -15
  124. package/dist/tests/lb-table.test.d.ts.map +0 -1
  125. package/dist/tests/lb-table.test.js +0 -205
  126. package/dist/widgets/index.d.ts +0 -7
  127. package/dist/widgets/index.d.ts.map +0 -1
  128. package/dist/widgets/index.js +0 -6
  129. package/dist/widgets/lb-input.d.ts +0 -2
  130. package/dist/widgets/lb-input.d.ts.map +0 -1
  131. package/dist/widgets/lb-input.js +0 -48
  132. package/dist/widgets/lb-list.d.ts +0 -2
  133. package/dist/widgets/lb-list.d.ts.map +0 -1
  134. package/dist/widgets/lb-list.js +0 -17
  135. package/dist/widgets/lb-options.d.ts +0 -26
  136. package/dist/widgets/lb-options.d.ts.map +0 -1
  137. package/dist/widgets/lb-options.js +0 -72
  138. package/dist/widgets/lb-picker.d.ts +0 -2
  139. package/dist/widgets/lb-picker.d.ts.map +0 -1
  140. package/dist/widgets/lb-picker.js +0 -25
  141. package/dist/widgets/lb-select.d.ts +0 -2
  142. package/dist/widgets/lb-select.d.ts.map +0 -1
  143. package/dist/widgets/lb-select.js +0 -43
  144. package/dist/widgets/lb-table.d.ts +0 -2
  145. package/dist/widgets/lb-table.d.ts.map +0 -1
  146. package/dist/widgets/lb-table.js +0 -113
  147. package/docs/application-chrome.md +0 -36
  148. package/docs/building-html-pages.md +0 -130
  149. package/docs/getting-started.md +0 -120
  150. package/docs/guide.md +0 -1164
  151. package/docs/hosting.md +0 -218
  152. package/docs/latent-risks.md +0 -20
  153. package/widgets/index.ts +0 -6
  154. package/widgets/lb-input.html +0 -1
  155. package/widgets/lb-input.ts +0 -64
  156. package/widgets/lb-list.html +0 -1
  157. package/widgets/lb-list.ts +0 -21
  158. package/widgets/lb-options.html +0 -4
  159. package/widgets/lb-options.ts +0 -88
  160. package/widgets/lb-picker.html +0 -7
  161. package/widgets/lb-picker.ts +0 -27
  162. package/widgets/lb-select.html +0 -4
  163. package/widgets/lb-select.ts +0 -55
  164. package/widgets/lb-table.html +0 -8
  165. package/widgets/lb-table.ts +0 -126
@@ -0,0 +1,240 @@
1
+ # Data Binding
2
+
3
+ A page binds its elements to server data with four attributes, and asks the
4
+ server to change that data with four more. The page writes all eight into
5
+ its HTML; the server declares which queries it answers and which operations
6
+ it permits, and refuses anything it has not declared.
7
+
8
+ ## A page that binds data
9
+
10
+ Here is a page that binds a scalar, a list, and three requests:
11
+
12
+ ```html
13
+ <!-- src/pages/members.page.html -->
14
+ <h1>Members</h1>
15
+
16
+ <p lb-query="dues">Dues collected this year: <span lb-cell="total"></span></p>
17
+
18
+ <form lb-insert lb-query="roster">
19
+ <input lb-cell="name" placeholder="Name" />
20
+ <button type="submit">Add member</button>
21
+ </form>
22
+
23
+ <lb-list lb-query="roster">
24
+ <ul>
25
+ <template lb-key="id">
26
+ <li>
27
+ <lb-input lb-cell="name"></lb-input>
28
+ <button lb-delete>Remove</button>
29
+ </li>
30
+ </template>
31
+ </ul>
32
+ </lb-list>
33
+ ```
34
+
35
+ The queries named here — `dues` and `roster` — and the operations the page
36
+ asks for are declared on the server; see [page files](./page-files.md).
37
+
38
+ ## Binding
39
+
40
+ | Attribute | Names |
41
+ |------------|----------------------------------------------------------|
42
+ | `lb-query` | The query a subtree displays |
43
+ | `lb-key` | The cell that identifies a row, and a live row's own key |
44
+ | `lb-cell` | The cell an element displays |
45
+ | `lb-value` | Where a widget receives its value |
46
+
47
+ A binding is scoped by ancestry. An element binds to the query on its
48
+ nearest ancestor carrying `lb-query`, and to the row on its nearest ancestor
49
+ carrying `lb-key`. Nothing else establishes scope: an element outside every
50
+ `lb-query` is bound to nothing and displays nothing.
51
+
52
+ Bind an element to a cell by putting `lb-cell` on the element that shows the
53
+ value. The scope's own root counts as a cell if it carries one, which is how
54
+ an `<option>` — whose content model is text — displays the value it is.
55
+
56
+ Name the same query on more than one subtree to display it in more than one
57
+ place. Every subtree gets the result.
58
+
59
+ `lb-key` holds one name in two positions. On a row template it names the
60
+ cell that identifies a row; on a row that is showing, it carries that row's
61
+ key value. A template is never a row, so the two never collide.
62
+
63
+ ### Where a bound value lands
64
+
65
+ A value lands on a bound element one of two ways.
66
+
67
+ | Element | Receives the value as |
68
+ |------------------|--------------------------|
69
+ | A native element | Its `textContent` |
70
+ | A custom element | Its `lb-value` attribute |
71
+
72
+ A native element has no behavior of its own, so its value is its text. A
73
+ widget owns whatever control it wraps, so it is handed the value and renders
74
+ it itself — see [Custom Elements](./custom-elements.md#code) for
75
+ observing `lb-value`.
76
+
77
+ Nothing an application writes ever sets `lb-value`. It is written by
78
+ Loadbare and read by the widget it is written on.
79
+
80
+ ### Rows
81
+
82
+ A query that answers with many rows is delivered to a list widget, which
83
+ clones its row template once per row and binds each clone to one row. A
84
+ native element has one destination for a value and cannot acquire children,
85
+ so a query that answers with rows must be bound to a widget that accepts
86
+ them.
87
+
88
+ Bind the widget to the query with `lb-query`, and name the key cell on the
89
+ `<template>` inside it with `lb-key`. See
90
+ [The Basic Widget Library](./widgets.md) for the widgets that accept
91
+ rows, and for `lb-group` and `lb-sort`, which a list widget reads to decide
92
+ where a row goes.
93
+
94
+ ## Requests
95
+
96
+ Four attributes turn an interaction into a request:
97
+
98
+ | Written | Asks for | Carries |
99
+ |---------------------------|------------------|------------------------------|
100
+ | `lb-action="name"` | The named action | `name`, and what is in scope |
101
+ | `lb-delete` | `tupleDelete` | `query`, `key` |
102
+ | `lb-insert` on a `<form>` | `tupleInsert` | `query`, `values` |
103
+ | `lb-update` on a `<form>` | `tupleUpdate` | `query`, `key`, `values` |
104
+
105
+ The shipped `<lb-input>` widget asks for a fifth, `cellChange`, carrying
106
+ `query`, `key`, `cell`, and `value`. It sends one when the page applies
107
+ `data-fire-on-change` to it, and stays quiet otherwise — an input inside an
108
+ `lb-insert` or `lb-update` form is read again by the form on submit, so a
109
+ widget that sent on its own would write the same edit twice.
110
+
111
+ Declare every one of these on the server. A name the page has not declared,
112
+ and an operation a query does not permit, are refused; see
113
+ [hooks, actions, CRUD](./page-files.md#hooks-actions-crud).
114
+
115
+ ### Actions
116
+
117
+ Write `lb-action` on a button to ask the server to do something that is not
118
+ one of the four CRUD operations:
119
+
120
+ ```html
121
+ <button lb-action="mailRoster">Mail the roster</button>
122
+ ```
123
+
124
+ An action carries whatever binding is in scope at the element — `query`,
125
+ `key`, `cell` — and nothing else. There is no argument list. A button
126
+ carries no value, so the server computes the whole of the new state and the
127
+ page displays only what came back.
128
+
129
+ Write `lb-action` on a widget to have the widget decide what performing the
130
+ action means. Loadbare turns a click into a request for a native element
131
+ only, and leaves a widget to send its own — a `<select>` performs its action
132
+ on change, not on click.
133
+
134
+ ### Deleting a row
135
+
136
+ ```html
137
+ <button lb-delete>Remove</button>
138
+ ```
139
+
140
+ `lb-delete` needs no name. The row's `lb-query` and `lb-key` are already in
141
+ scope, and they are all the server needs to know which row is meant and
142
+ whether the query permits deleting it.
143
+
144
+ Write `lb-delete` on a native element, the same as `lb-action`. A widget
145
+ sends its own request.
146
+
147
+ ### Forms
148
+
149
+ `lb-insert` and `lb-update` both gather every `lb-cell` inside the form into
150
+ one values map, read from the control each cell is or wraps. They differ in
151
+ one thing: `lb-update` also carries the key of the row it is inside, and
152
+ `lb-insert` carries none, because there is no row yet.
153
+
154
+ Put an `lb-update` form inside the row it edits, so it has that row's key
155
+ from the same ancestor a delete button reads:
156
+
157
+ ```html
158
+ <template lb-key="id">
159
+ <li>
160
+ <form lb-update>
161
+ <input lb-cell="name" />
162
+ <button type="submit">Save</button>
163
+ </form>
164
+ </li>
165
+ </template>
166
+ ```
167
+
168
+ Give every `lb-cell` in a form a control to read. A cell that is neither an
169
+ `<input>`, `<select>`, or `<textarea>` nor wraps one is left out of the
170
+ values map.
171
+
172
+ ## Conditional rendering
173
+
174
+ Loadbare ships static HTML and hydrates elements that are already in the
175
+ document. There is no `if`, and none is needed: write every possibility into
176
+ the page, and control which of them is showing.
177
+
178
+ Use the standard `hidden` attribute, or CSS, and set it from a value the
179
+ server sent. A widget that receives a value in `lb-value` is the natural
180
+ place to do it — it is handed the current state and decides what to show:
181
+
182
+ ```ts
183
+ for (const step of steps) step.hidden = step.dataset.step !== value;
184
+ ```
185
+
186
+ Ship the case that is showing on arrival unhidden, and hide the rest in the
187
+ HTML.
188
+
189
+ ### An empty list
190
+
191
+ A list widget stamps itself with `data-rows`, the number of rows it is
192
+ showing. It is the one conditional a page cannot be sent, because the server
193
+ answers with rows and says nothing about how many survived. It makes an
194
+ empty list a stylesheet rule rather than code in every list widget:
195
+
196
+ ```html
197
+ <lb-list lb-query="roster">
198
+ <ul>
199
+ <template lb-key="id">
200
+ <li lb-cell="name"></li>
201
+ </template>
202
+ </ul>
203
+ <p class="roster-empty">No members yet.</p>
204
+ </lb-list>
205
+ ```
206
+
207
+ ```css
208
+ .roster-empty {
209
+ display: none;
210
+ }
211
+ lb-list[data-rows="0"] .roster-empty {
212
+ display: revert;
213
+ }
214
+ ```
215
+
216
+ ## Request state
217
+
218
+ Loadbare stamps two attributes on the element a request came from — the
219
+ button, the form, or the widget itself:
220
+
221
+ | Attribute | Means |
222
+ |-------------------|-------------------------------------------|
223
+ | `data-lb-pending` | The request is in flight |
224
+ | `data-lb-error` | The last request from this element failed |
225
+
226
+ `data-lb-pending` is set when the request goes out and removed when it
227
+ settles. `data-lb-error` is set on a failed response, a network failure, or
228
+ a timeout alike, and cleared when that element sends its next request.
229
+
230
+ Neither one carries any meaning beyond the fact it states. Dim a pending
231
+ button in a stylesheet, or have a widget watch its own attributes and
232
+ disable itself. An application that styles neither behaves correctly and
233
+ shows nothing.
234
+
235
+ ## Sending a request from a widget
236
+
237
+ A widget can build and dispatch a request itself instead of carrying one of
238
+ the attributes above — which is what `<lb-input>` does, and what a widget
239
+ carrying `lb-action` must do. See
240
+ [Custom Elements](./custom-elements.md#code).
@@ -0,0 +1,38 @@
1
+ # The Elements of a Loadbare Application
2
+
3
+ This reference is organized into four groups that reflect how an application is
4
+ put together: the shell it lives in, the build that assembles it, the pages it
5
+ serves, and the widgets those pages are made of.
6
+
7
+ ## The application shell
8
+
9
+ | Topic | Description |
10
+ |--------------------------------------------------|-----------------------------------------------------|
11
+ | [`chrome.html`](./chrome.md) | The HTML document w/head, body, banner, main, etc. |
12
+ | [The Express server](./server.md) | Serves static assets and handles data channel calls |
13
+ | [The database layer](./server.md#database-layer) | Where the app's database access fits |
14
+
15
+ ## Building the app
16
+
17
+ | Topic | Description |
18
+ |----------------------------------------------------------|-------------------------------------------|
19
+ | [`imports.ts`](./builder.md#widgets-from-packages) | The packages this app takes widgets from |
20
+ | [`loadbare-app-build`](./builder.md#running-the-builder) | Configuring the build command |
21
+ | [CSS](./css.md) | How the builder packages css |
22
+
23
+ ## Application Pages
24
+
25
+ | Topic | Description |
26
+ |---------------------------------------------------------|--------------------------------|
27
+ | [`<name>.page.html`](./page-files.md#html) | The page's HTML |
28
+ | [`<name>.queries.ts`](./page-files.md#queries) | The data the page displays |
29
+ | [`<name>.hooks.ts`](./page-files.md#hooks-actions-crud) | Data Channel handlers |
30
+ | [Data Binding](./data-binding.md) | Connecting HTML to server data |
31
+
32
+ ## Widgets
33
+
34
+ | Topic | Description |
35
+ |------------------------------------------------|---------------------------------|
36
+ | [What a widget is](./custom-elements.md) | An HTML file, a script, or both |
37
+ | [`<tag-name>.html`](./custom-elements.md#html) | The markup the tag expands into |
38
+ | [`<tag-name>.ts`](./custom-elements.md#code) | The class the tag registers |
@@ -0,0 +1,175 @@
1
+ # Page Files
2
+
3
+ A page is a set of files sharing one base name. The application writes the
4
+ HTML, and adds queries and hooks when the page shows data.
5
+
6
+ | File | Holds |
7
+ |---------------------|-------------------------------|
8
+ | `<name>.page.html` | The page's HTML |
9
+ | `<name>.queries.ts` | The data the page displays |
10
+ | `<name>.hooks.ts` | What the page does when asked |
11
+
12
+ Name the landing page `index.page.html`. A bare `/` resolves to `index`, and
13
+ every other path names the page of the same name.
14
+
15
+ Put the files anywhere under `src/`. The builder pairs them by base name, not
16
+ by directory; `src/pages/` is the convention.
17
+
18
+ ## HTML
19
+
20
+ Write the page as a fragment. The fragment will land in `<main>`, which
21
+ is supplied by [chrome.html](./chrome.md).
22
+
23
+ ```html
24
+ <!-- src/pages/about.page.html -->
25
+ <h1>About</h1>
26
+ <p>This is the about page.</p>
27
+
28
+ <div lb-query="visits">
29
+ <p>This page has been visited <span lb-cell="count"></span> times.</p>
30
+ </div>
31
+ ```
32
+
33
+ Bind elements to data with `lb-query`, `lb-cell`, and the rest of the
34
+ attribute vocabulary in [Data Binding](./data-binding.md).
35
+
36
+ A page that displays no data needs no other file.
37
+
38
+ ## Queries
39
+
40
+ Export `queries` from `<name>.queries.ts`. Each key is a name the HTML binds
41
+ to with `lb-query`, and each value takes the request context and returns
42
+ that query's result:
43
+
44
+ ```ts
45
+ // src/pages/about.queries.ts
46
+ import { type Queries } from "@loadbare/app/server";
47
+
48
+ export const queries: Queries = {
49
+ visits: async (ctx) => ({ count: String(await ctx.db.visitCount()) }),
50
+ };
51
+ ```
52
+
53
+ Return every cell as a string. Format numbers, dates, and money in the query,
54
+ so the browser displays a value it never computes.
55
+
56
+ Return many rows through `rows()`:
57
+
58
+ ```ts
59
+ // src/pages/directory.queries.ts
60
+ import { rows, type Queries } from "@loadbare/app/server";
61
+
62
+ export const queries: Queries = {
63
+ directory: async (ctx) => rows(await ctx.db.directory()),
64
+ };
65
+ ```
66
+
67
+ Give every row a cell that identifies it, and name that cell with `lb-key` in
68
+ the HTML. Return the rows in the order the page shows them.
69
+
70
+ Return the full result every time. Sending only what changed is a hook's job —
71
+ see [refresh and patch](#refresh-and-patch).
72
+
73
+ ## Hooks, actions, CRUD
74
+
75
+ Export `hooks` from `<name>.hooks.ts`. It holds three keys, each optional:
76
+
77
+ | Key | Runs |
78
+ |-------------|------------------------------------------------------|
79
+ | `beforeGet` | Before the page's queries, on a request for the page |
80
+ | `actions` | What the page may be asked to do, by name |
81
+ | `crud` | The four operations a query permits on its rows |
82
+
83
+ ### beforeGet
84
+
85
+ ```ts
86
+ // src/pages/about.hooks.ts
87
+ import { type Hooks } from "@loadbare/app/server";
88
+
89
+ export const hooks: Hooks = {
90
+ beforeGet: (ctx) => ctx.db.recordVisit(),
91
+ };
92
+ ```
93
+
94
+ Declare no refresh set here. The page's queries run afterward.
95
+
96
+ ### actions
97
+
98
+ Declare an action under the name the HTML gives `lb-action`. Pair what it
99
+ does with the queries to re-run once it has:
100
+
101
+ ```ts
102
+ export const hooks: Hooks = {
103
+ actions: {
104
+ resetVisits: {
105
+ run: (ctx) => ctx.db.resetVisits(),
106
+ refresh: ["visits"],
107
+ },
108
+ },
109
+ };
110
+ ```
111
+
112
+ Declare every action the page allows. A name the page does not declare is
113
+ refused.
114
+
115
+ Read where the interaction happened from `run`'s second argument, which
116
+ carries `query`, `key`, `cell`, and `value` when the element that dispatched
117
+ the request had them.
118
+
119
+ ### crud
120
+
121
+ Declare CRUD operations under `crud`, keyed by the query they operate on. Each
122
+ one takes the binding its trigger supplies:
123
+
124
+ | Operation | The page writes | `run` receives |
125
+ |---------------|--------------------|------------------------|
126
+ | `cellChange` | `<lb-input data-fire-on-change>` | `key`, `cell`, `value` |
127
+ | `tupleDelete` | `lb-delete` | `key` |
128
+ | `tupleInsert` | `<form lb-insert>` | `values` |
129
+ | `tupleUpdate` | `<form lb-update>` | `key`, `values` |
130
+
131
+ ```ts
132
+ // src/pages/directory.hooks.ts
133
+ import { patch, type Hooks } from "@loadbare/app/server";
134
+
135
+ export const hooks: Hooks = {
136
+ crud: {
137
+ directory: {
138
+ tupleInsert: {
139
+ run: async (ctx, { values }) => {
140
+ const entry = await ctx.db.addDirectoryEntry(values);
141
+ return { directory: patch({ rows: [entry] }) };
142
+ },
143
+ refresh: [],
144
+ },
145
+ },
146
+ },
147
+ };
148
+ ```
149
+
150
+ Declare every operation the query permits. An operation a query does not
151
+ declare is refused, and a query with no `crud` entry permits none.
152
+
153
+ ### refresh and patch
154
+
155
+ List in `refresh` every query whose whole answer the operation changed.
156
+
157
+ Return a result from `run` to state a narrower change than re-running a query
158
+ would. What `run` returns is laid over the refreshed queries:
159
+
160
+ | Result | States |
161
+ |--------------------------|---------------------------------------------|
162
+ | `rows([...])` | The entire set, and its order |
163
+ | `patch({ rows: [...] })` | These rows arrive or change; the rest stand |
164
+ | `patch({ drop: [...] })` | These keys are gone; the rest stand |
165
+
166
+ Return a patch for a change the operation knows the extent of — one row added,
167
+ one row dropped, one cell edited — and leave `refresh` empty. Re-run the query
168
+ instead when membership or order changed in a way the operation cannot name:
169
+
170
+ ```ts
171
+ resetRoster: {
172
+ run: (ctx) => ctx.db.resetMembers(),
173
+ refresh: ["roster"],
174
+ },
175
+ ```
@@ -0,0 +1,123 @@
1
+ # The Express Server
2
+
3
+ Loadbare ships no server. The application writes an ordinary Express app and
4
+ serves four things from it: the client script, the stylesheet, the data
5
+ channel, and the one HTML document.
6
+
7
+ Express is a peer dependency. The application installs it.
8
+
9
+ ## A complete server
10
+
11
+ Here is a minimal but complete server for a typical app:
12
+
13
+ ```ts
14
+ // server.ts
15
+ import path from "node:path";
16
+ import { readFileSync } from "node:fs";
17
+ import express, { type Request } from "express";
18
+ import { hubRoutes } from "@loadbare/app/express";
19
+ import type { HubContext } from "@loadbare/app/server";
20
+ import { hub } from "./dist/pages";
21
+ import { openDb } from "./src/database";
22
+
23
+ const DIST = path.resolve("dist");
24
+ const app = express();
25
+
26
+ app.get("/client.js", (_req, res) => res.sendFile(path.join(DIST, "client.js")));
27
+ app.get("/app.css", (_req, res) => res.sendFile(path.join(DIST, "app.css")));
28
+
29
+ function contextFor(_req: Request): HubContext {
30
+ return { db: openDb() };
31
+ }
32
+ app.use(hubRoutes(hub, contextFor));
33
+
34
+ app.get(/.*/, (_req, res) =>
35
+ res.type("html").send(readFileSync(path.join(DIST, "app.html"), "utf-8")),
36
+ );
37
+
38
+ app.listen(8787);
39
+ ```
40
+
41
+ ## What the server serves
42
+
43
+ | Required | Serves |
44
+ |------------------------------|-------------------------------|
45
+ | `/client.js` | `dist/client.js` |
46
+ | `/app.css` | `dist/app.css` |
47
+ | `hubRoutes(hub, contextFor)` | `GET /lb/data` and `POST /lb` |
48
+ | Every other GET | `dist/app.html` |
49
+
50
+ Everything else is optional:
51
+
52
+ | Optional | Description |
53
+ |-------------------------------|----------------------------------------------|
54
+ | Middleware before `hubRoutes` | Sessions, authentication, logging |
55
+ | The application's own routes | Uploads, webhooks, anything outside Loadbare |
56
+ | An error handler | Express sends its own 500 without one |
57
+
58
+
59
+ ## Rules for writing the server
60
+
61
+ Register the static routes and `hubRoutes` before the catch-all.
62
+
63
+ Register authentication before `hubRoutes`.
64
+
65
+ Serve `dist/app.html` for every route the application does not claim,
66
+ including a path that names no page. See [`chrome.html`](./chrome.md) for the
67
+ `<dialog lb-unknown-page>` that announces that case to the user.
68
+
69
+ Give the server the origin root. A proxy in front of it passes `/lb/data` and
70
+ `/lb` through unchanged, and the application cannot be hosted under a subpath
71
+ such as `example.com/myapp/`.
72
+
73
+ Leave `express.json()` to `hubRoutes`, which mounts it on its own routes.
74
+
75
+ ## Running the server
76
+
77
+ Run the server under a TypeScript-capable runner. The builder writes
78
+ `dist/pages.ts`, which exports `hub`, as TypeScript:
79
+
80
+ ```json
81
+ {
82
+ "scripts": {
83
+ "dev": "loadbare-app-build --watch & tsx server.ts"
84
+ }
85
+ }
86
+ ```
87
+
88
+ Restart the server after adding or changing a `.hooks.ts` or `.queries.ts`
89
+ file.
90
+
91
+ ## Database layer
92
+
93
+ Loadbare ships no data layer. The application opens its own database and hands
94
+ it to Loadbare as the request context.
95
+
96
+ The application writes `contextFor` and passes it to `hubRoutes`. Loadbare
97
+ calls it on every data request:
98
+
99
+ ```ts
100
+ // server.ts
101
+ function contextFor(req: Request): HubContext {
102
+ return { db: openDb(req.session.userId) };
103
+ }
104
+ ```
105
+
106
+ Open the handle in `contextFor` rather than once at startup, so that each
107
+ request works through a database opened for the caller it authenticated.
108
+
109
+ Declare what the context holds, once, anywhere in the application's own
110
+ source. Next to the database module is the natural place:
111
+
112
+ ```ts
113
+ // src/database.ts
114
+ declare module "@loadbare/app/server" {
115
+ interface HubContext {
116
+ db: Db;
117
+ }
118
+ }
119
+ ```
120
+
121
+ Add a field for anything else a request needs — the authenticated user, a
122
+ request id, a feature flag set. Queries and hooks read them from `ctx`; see
123
+ [page files](./page-files.md).