@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,270 @@
1
+ /**
2
+ * Where a build's files come from — see docs/reference/builder.md.
3
+ *
4
+ * Three kinds of file reach the assembled application: a `.html` definition
5
+ * that expands a tag, a `.ts` script that registers its class, and a `.css`
6
+ * stylesheet. All three arrive from the same places, so all three are found
7
+ * the same way: one ordered list of origins, scanned once each, read by
8
+ * whoever needs them.
9
+ *
10
+ * The order is the cascade, and it is the same for every file kind:
11
+ *
12
+ * 1. this package's own `hub/`
13
+ * 2. each package declared in `imports.ts`, in first-mention order
14
+ * 3. the application's own `--src` tree
15
+ *
16
+ * A package contributes one directory of its own choosing — `loadbare.widgets`
17
+ * in its package.json, or the whole package when it says nothing. See
18
+ * `widgetDir` below for why a package gets to answer that.
19
+ *
20
+ * The one built-in origin holds one tag, `lb-hub`, and it is there because
21
+ * every application has a hub whether or not it says so. A widget is not in
22
+ * that position: `@loadbare/widgets` is a declared package like any other,
23
+ * and the builder cannot tell it from someone else's.
24
+ *
25
+ * Two rules fall out of that list, and there is no third:
26
+ *
27
+ * - Within one origin, two files claiming the same tag are an error. That
28
+ * is a typo, and the origin's own author is the one who can fix it.
29
+ * - Across origins, the later one wins. An application always has the
30
+ * final say over a package it imported, and a package over a built-in,
31
+ * without anyone having to out-name a dependency they do not control.
32
+ */
33
+ import { readdir, readFile, stat } from "node:fs/promises";
34
+ import path from "node:path";
35
+ import { pathToFileURL } from "node:url";
36
+ /** Lowercase, hyphenated — the one shape every custom element tag has, and the one native elements never do. */
37
+ export const CUSTOM_ELEMENT_TAG = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
38
+ const HTML_SUFFIX = ".html";
39
+ const SCRIPT_SUFFIXES = [".ts", ".js"];
40
+ const CSS_SUFFIX = ".css";
41
+ /**
42
+ * Every file under `dir`, its own nested `node_modules` excluded — those
43
+ * belong to the packages that declare them, not to this one.
44
+ */
45
+ async function filesUnder(dir) {
46
+ let entries;
47
+ try {
48
+ entries = await readdir(dir, { withFileTypes: true, recursive: true });
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ return entries
54
+ .filter((entry) => {
55
+ if (!entry.isFile())
56
+ return false;
57
+ const relative = path.relative(dir, entry.parentPath);
58
+ return !relative.split(path.sep).includes("node_modules");
59
+ })
60
+ .map((entry) => path.join(entry.parentPath, entry.name));
61
+ }
62
+ /**
63
+ * One origin's contribution, classified by filename alone.
64
+ *
65
+ * A `.html` or `.ts` file is a widget's only when its basename is a tag —
66
+ * which is what excludes `chrome.html`, `about.page.html`, `about.hooks.ts`
67
+ * and `imports.ts` here without naming any of them. Those are the
68
+ * application's own files, and build/locations.ts picks them out of `src`
69
+ * separately.
70
+ */
71
+ export async function scanOrigin(origin) {
72
+ const definitions = new Map();
73
+ const scripts = new Map();
74
+ const css = [];
75
+ const claim = (into, what, tag, file) => {
76
+ const already = into.get(tag);
77
+ if (already) {
78
+ throw new Error(`origins: '${origin.name}' has two ${what} files for <${tag}>: ` +
79
+ `'${already}' and '${file}'`);
80
+ }
81
+ into.set(tag, file);
82
+ };
83
+ for (const dir of origin.dirs) {
84
+ for (const file of await filesUnder(dir)) {
85
+ const name = path.basename(file);
86
+ if (name.endsWith(CSS_SUFFIX)) {
87
+ css.push(file);
88
+ }
89
+ else if (name.endsWith(HTML_SUFFIX)) {
90
+ const tag = name.slice(0, -HTML_SUFFIX.length).toLowerCase();
91
+ if (CUSTOM_ELEMENT_TAG.test(tag)) {
92
+ claim(definitions, "definition", tag, file);
93
+ }
94
+ }
95
+ else {
96
+ const suffix = SCRIPT_SUFFIXES.find((s) => name.endsWith(s));
97
+ if (!suffix)
98
+ continue;
99
+ const tag = name.slice(0, -suffix.length).toLowerCase();
100
+ // Without the extension: this becomes an import specifier, and
101
+ // esbuild resolves ".ts" from the bare path the same way as ".js".
102
+ // A `.d.ts` never reaches here — stripping ".ts" leaves a dot, which
103
+ // is not a tag.
104
+ if (CUSTOM_ELEMENT_TAG.test(tag)) {
105
+ claim(scripts, "script", tag, file.slice(0, -suffix.length));
106
+ }
107
+ }
108
+ }
109
+ }
110
+ // By filename first, the full path only to break a tie. A directory a file
111
+ // happens to sit in never affects the order.
112
+ css.sort((a, b) => path.basename(a).localeCompare(path.basename(b)) || a.localeCompare(b));
113
+ return {
114
+ name: origin.name,
115
+ ...(origin.specifier ? { specifier: origin.specifier } : {}),
116
+ definitions,
117
+ scripts,
118
+ css,
119
+ };
120
+ }
121
+ /**
122
+ * `imports.ts`, executed rather than parsed — it runs through the same
123
+ * loader as the rest of the build, so it is ordinary TypeScript, not a data
124
+ * format with its own rules. It lists the packages an application pulls
125
+ * widgets from, and each one becomes an origin, scanned exactly as `src` is.
126
+ */
127
+ export async function loadElementManifest(file) {
128
+ let mod;
129
+ try {
130
+ // pathToFileURL requires an absolute path; the caller's may not be one.
131
+ mod = (await import(pathToFileURL(path.resolve(file)).href));
132
+ }
133
+ catch (err) {
134
+ if (err.code === "ERR_MODULE_NOT_FOUND")
135
+ return [];
136
+ throw new Error(`elements: '${file}' failed to load: ${err.message}`);
137
+ }
138
+ const declared = mod.default ?? [];
139
+ if (!Array.isArray(declared)) {
140
+ throw new Error(`imports.ts: must default-export an array of package names, got ${typeof declared}`);
141
+ }
142
+ for (const name of declared) {
143
+ if (typeof name !== "string" || name === "") {
144
+ throw new Error(`imports.ts: every entry must be a package name, got ${JSON.stringify(name)}`);
145
+ }
146
+ }
147
+ return declared;
148
+ }
149
+ /**
150
+ * The ordinary node_modules walk, stopping at the first directory that has
151
+ * the package installed — not a full resolution of an entry file, on
152
+ * purpose: this only needs the package's root directory, and stopping short
153
+ * avoids caring whether the package exposes an "exports" map at all. Which
154
+ * part of that root holds the widgets is the package's own answer to give —
155
+ * `widgetDir` asks it.
156
+ */
157
+ export async function findInstalledPackage(packageName, fromDir) {
158
+ let dir = path.resolve(fromDir);
159
+ for (;;) {
160
+ const candidate = path.join(dir, "node_modules", packageName);
161
+ try {
162
+ await stat(path.join(candidate, "package.json"));
163
+ return candidate;
164
+ }
165
+ catch {
166
+ // not here — keep walking up
167
+ }
168
+ const parent = path.dirname(dir);
169
+ if (parent === dir) {
170
+ throw new Error(`origins: could not find installed package '${packageName}' above '${fromDir}'`);
171
+ }
172
+ dir = parent;
173
+ }
174
+ }
175
+ /**
176
+ * Where in an installed package its widgets are, from the package's own
177
+ * manifest:
178
+ *
179
+ * "loadbare": { "widgets": "./dist" }
180
+ *
181
+ * A package that says nothing has its whole directory scanned, which is what
182
+ * every package got before this field existed.
183
+ *
184
+ * The field is here because a published package is not a source tree. It is a
185
+ * distribution artifact with a shape npm defines — compiled output in `dist/`,
186
+ * the source excluded, plus a README, a licence, docs and whatever else its
187
+ * author ships — and scanning all of it makes every one of those files a
188
+ * possible widget. A package that compiles then carries the same tag twice,
189
+ * once as `.ts` and once as `.js`, and the duplicate-tag error fires on a
190
+ * package the application does not control and cannot fix.
191
+ *
192
+ * So the package says where to look, and the rest of it is none of the
193
+ * builder's business. This costs an application author nothing: they still
194
+ * list the package in `imports.ts` and write no import statement. It costs the
195
+ * package author one line, in the file npm already expects to carry exactly
196
+ * this kind of declaration.
197
+ */
198
+ async function widgetDir(root, packageName) {
199
+ let manifest;
200
+ try {
201
+ manifest = JSON.parse(await readFile(path.join(root, "package.json"), "utf-8"));
202
+ }
203
+ catch (err) {
204
+ throw new Error(`origins: could not read the package.json of '${packageName}': ${err.message}`);
205
+ }
206
+ const declared = manifest.loadbare?.widgets;
207
+ if (declared === undefined)
208
+ return root;
209
+ if (typeof declared !== "string" || declared === "") {
210
+ throw new Error(`origins: '${packageName}' has a "loadbare.widgets" that is not a path: ` +
211
+ `${JSON.stringify(declared)}`);
212
+ }
213
+ // A path out of the package would put a package's declaration in charge of
214
+ // directories it does not own — including the application's own.
215
+ const dir = path.resolve(root, declared);
216
+ const inside = path.relative(root, dir);
217
+ if (path.isAbsolute(declared) || inside.startsWith("..")) {
218
+ throw new Error(`origins: '${packageName}' declares widgets at '${declared}', which is outside the package`);
219
+ }
220
+ // A declared directory that is not there means the package shipped without
221
+ // its widgets — its build did not run, or `files` left the directory out.
222
+ // Scanning the package root instead would find some of them and look like it
223
+ // worked, so this stops.
224
+ try {
225
+ await stat(dir);
226
+ }
227
+ catch {
228
+ throw new Error(`origins: '${packageName}' declares widgets at '${declared}', which is not in the installed package`);
229
+ }
230
+ return dir;
231
+ }
232
+ /**
233
+ * The cascade, in order. A declared package is an origin like any other: the
234
+ * directory it points `loadbare.widgets` at is scanned the way `src` is — its
235
+ * whole installed directory, when it points at nothing — and a widget there is
236
+ * found by its filename, not by being named in a manifest.
237
+ *
238
+ * `hub/` is the only origin naming a specifier, because `lb-hub` is the
239
+ * package's main entry point rather than a file to reach into.
240
+ */
241
+ export async function buildOrigins(builtin, declared, src) {
242
+ const { builtinHubDir, packageName } = builtin;
243
+ const packages = [];
244
+ for (const name of declared) {
245
+ const root = await findInstalledPackage(name, src);
246
+ packages.push({ name, dirs: [await widgetDir(root, name)] });
247
+ }
248
+ return [
249
+ {
250
+ name: packageName,
251
+ dirs: [builtinHubDir],
252
+ specifier: () => packageName,
253
+ },
254
+ ...packages,
255
+ { name: src, dirs: [src] },
256
+ ];
257
+ }
258
+ /**
259
+ * The whole cascade, scanned. Everything a build reads from disk that is not
260
+ * a page, the chrome, or a page's server half comes back from here.
261
+ */
262
+ export async function scanAll(builtin, src, importsFile) {
263
+ const declared = importsFile ? await loadElementManifest(importsFile) : [];
264
+ const origins = await buildOrigins(builtin, declared, src);
265
+ return Promise.all(origins.map(scanOrigin));
266
+ }
267
+ /** Every origin's stylesheets, concatenated in cascade order. */
268
+ export function cssFrom(scans) {
269
+ return scans.flatMap((scan) => scan.css);
270
+ }
@@ -14,6 +14,7 @@ export declare const ATTR_SLOT = "lb-slot";
14
14
  export declare const ATTR_TEMPLATE = "lb-template";
15
15
  export declare const ATTR_NAV_LINK = "lb-nav-link";
16
16
  export declare const PAGE_TEMPLATE_PREFIX = "page-";
17
+ export declare const ATTR_UNKNOWN_PAGE = "lb-unknown-page";
17
18
  export declare const HUB_TAG_NAME = "lb-hub";
18
19
  export declare const ATTR_PENDING = "data-lb-pending";
19
20
  export declare const ATTR_ERROR = "data-lb-error";
@@ -1 +1 @@
1
- {"version":3,"file":"lb-constants.d.ts","sourceRoot":"","sources":["../../core/lb-constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,aAAa,CAAC;AACrC,eAAO,MAAM,QAAQ,WAAW,CAAC;AACjC,eAAO,MAAM,SAAS,YAAY,CAAC;AACnC,eAAO,MAAM,UAAU,aAAa,CAAC;AAMrC,eAAO,MAAM,UAAU,aAAa,CAAC;AAKrC,eAAO,MAAM,SAAS,YAAY,CAAC;AAOnC,eAAO,MAAM,cAAc,cAAc,CAAC;AAK1C,eAAO,MAAM,aAAa,eAAe,CAAC;AAM1C,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAKvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAOvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,SAAS,YAAY,CAAC;AAYnC,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAG3C,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAQ5C,eAAO,MAAM,YAAY,WAAW,CAAC;AAQrC,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAO9C,eAAO,MAAM,UAAU,kBAAkB,CAAC;AAG1C,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAIzC,eAAO,MAAM,qBAAqB,QAAS,CAAC"}
1
+ {"version":3,"file":"lb-constants.d.ts","sourceRoot":"","sources":["../../core/lb-constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,aAAa,CAAC;AACrC,eAAO,MAAM,QAAQ,WAAW,CAAC;AACjC,eAAO,MAAM,SAAS,YAAY,CAAC;AACnC,eAAO,MAAM,UAAU,aAAa,CAAC;AAMrC,eAAO,MAAM,UAAU,aAAa,CAAC;AAKrC,eAAO,MAAM,SAAS,YAAY,CAAC;AAOnC,eAAO,MAAM,cAAc,cAAc,CAAC;AAK1C,eAAO,MAAM,aAAa,eAAe,CAAC;AAM1C,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAKvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAOvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,SAAS,YAAY,CAAC;AAYnC,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAG3C,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAQ5C,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AAQnD,eAAO,MAAM,YAAY,WAAW,CAAC;AAQrC,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAO9C,eAAO,MAAM,UAAU,kBAAkB,CAAC;AAG1C,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAIzC,eAAO,MAAM,qBAAqB,QAAS,CAAC"}
@@ -1,7 +1,7 @@
1
1
  // Every hub attribute name lives here. Nothing writes one as a string
2
- // literal — not a widget, not a page host, not a test. See docs/guide.md,
3
- // "Attribute vocabulary".
4
- // --- Addressing (docs/data-binding.md) ---------------------------------
2
+ // literal — not a widget, not a page host, not a test. See
3
+ // docs/reference/data-binding.md.
4
+ // --- Binding (docs/reference/data-binding.md) ---------------------------
5
5
  export const ATTR_QUERY = "lb-query";
6
6
  export const ATTR_KEY = "lb-key";
7
7
  export const ATTR_CELL = "lb-cell";
@@ -17,11 +17,11 @@ export const ATTR_GROUP = "lb-group";
17
17
  export const ATTR_SORT = "lb-sort";
18
18
  // Stamped on a list widget by the shared row machinery: how many rows it is
19
19
  // showing. It is HTML's namespace rather than Loadbare App's because nothing
20
- // addresses it — it is derived from rows the hub already knows, exactly as
20
+ // binds to it — it is derived from rows the hub already knows, exactly as
21
21
  // the section heading in lb-table is. It exists so that "no members yet"
22
22
  // is a stylesheet rule rather than a conditional in every list widget.
23
23
  export const ATTR_ROW_COUNT = "data-rows";
24
- // --- Requests (docs/requests.md) ---------------------------------------
24
+ // --- Requests (docs/reference/data-binding.md) --------------------------
25
25
  // One event name, used by a widget reaching the server and by a widget
26
26
  // reaching an ancestor widget.
27
27
  export const LB_EVENT_NAME = "lb-request";
@@ -32,7 +32,7 @@ export const LB_EVENT_NAME = "lb-request";
32
32
  export const ATTR_ACTION = "lb-action";
33
33
  // Marks a native element as deleting the tuple it is inside — the CRUD
34
34
  // counterpart to lb-action. It needs no declared name: tuple-delete takes
35
- // only the row's own lb-query/lb-key coordinates, which is all the server
35
+ // only the row's own lb-query/lb-key binding, which is all the server
36
36
  // needs to know which row and whether the operation is permitted on it.
37
37
  export const ATTR_DELETE = "lb-delete";
38
38
  // Marks a <form> as building a new tuple on submit. The hub gathers every
@@ -45,7 +45,7 @@ export const ATTR_INSERT = "lb-insert";
45
45
  // a list's row template root, or sits inside one, has both from its
46
46
  // ancestors exactly as a delete button does.
47
47
  export const ATTR_UPDATE = "lb-update";
48
- // --- Expansion (docs/expansion.md) -------------------------------------
48
+ // --- Expansion (docs/reference/custom-elements.md) ---------------------
49
49
  // Marks the element in a definition whose children receive the authored
50
50
  // inner content. An attribute rather than an element because HTML content
51
51
  // models discard foreign elements inside <select> and <table>.
@@ -61,12 +61,19 @@ export const ATTR_SLOT = "lb-slot";
61
61
  // is what gets it to expansion intact — and unwrapping it is what puts real
62
62
  // elements in the shipped document.
63
63
  export const ATTR_TEMPLATE = "lb-template";
64
- // --- Host channel (docs/bundling.md) -----------------------------------
64
+ // --- Host channel (docs/reference/chrome.md) ---------------------------
65
65
  // Only anchors carrying this are intercepted, so external links are never
66
66
  // hijacked.
67
67
  export const ATTR_NAV_LINK = "lb-nav-link";
68
68
  // A page host ships as <template id="page-<name>">.
69
69
  export const PAGE_TEMPLATE_PREFIX = "page-";
70
+ // Marks the chrome's own <dialog> for a page name that resolves to no
71
+ // template — a routing miss discovered client-side, not an HTTP 404 (every
72
+ // route gets the same 200 response; see docs/tutorials/010-pages-and-navigation.md).
73
+ // Optional: an application that declares none gets today's console.error
74
+ // and nothing more. The builder requires the element it's on to be a
75
+ // <dialog> (build/assemble.ts), since the hub calls showModal() on it.
76
+ export const ATTR_UNKNOWN_PAGE = "lb-unknown-page";
70
77
  // The hub's own tag. It is exempt from the rule every other LB-* tag
71
78
  // follows: expansion does not require it to have a definition (build/expand.ts),
72
79
  // and the client build does not require it to have a script of its own
@@ -1,4 +1,4 @@
1
- /** docs/requests.md — the operation set is closed. */
1
+ /** docs/reference/data-binding.md — the operation set is closed. */
2
2
  export type HubRequest = {
3
3
  op: "cell-change";
4
4
  query: string;
@@ -43,7 +43,7 @@ export type HubRequest = {
43
43
  /** One query's result: the cells of a single tuple. Every value is a string. */
44
44
  export type QueryResult = Record<string, string>;
45
45
  /**
46
- * A query that returns many tuples — see docs/guide.md, "List processing".
46
+ * A query that returns many tuples — see docs/reference/server.md, "Queries".
47
47
  *
48
48
  * Two results, because a list widget must be told the difference. `rows` is
49
49
  * the entire set and therefore also the order; `patch` names only what
@@ -1 +1 @@
1
- {"version":3,"file":"lb-types.d.ts","sourceRoot":"","sources":["../../core/lb-types.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,MAAM,MAAM,UAAU,GAClB;IACE,EAAE,EAAE,aAAa,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACrE;IACE,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,GACD;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AACpD;;;;;;;;;;;;GAYG;GACD;IACE,EAAE,EAAE,QAAQ,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,gFAAgF;AAChF,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEjD;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,EAAE,CAAA;CAAE,GACnC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE3D,sEAAsE;AACtE,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC;AAEjD,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,IAAI,UAAU,CAGpE;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;CACtC"}
1
+ {"version":3,"file":"lb-types.d.ts","sourceRoot":"","sources":["../../core/lb-types.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,MAAM,MAAM,UAAU,GAClB;IACE,EAAE,EAAE,aAAa,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACrE;IACE,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,GACD;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AACpD;;;;;;;;;;;;GAYG;GACD;IACE,EAAE,EAAE,QAAQ,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,gFAAgF;AAChF,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEjD;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,EAAE,CAAA;CAAE,GACnC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE3D,sEAAsE;AACtE,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC;AAEjD,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,IAAI,UAAU,CAGpE;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;CACtC"}
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Landing data on a live host — see docs/guide.md, "Applying a response".
3
+ * Landing data on a live host — see docs/reference/data-binding.md.
4
4
  *
5
5
  * This is one file rather than part of the hub because a list widget needs
6
6
  * the same operation. It clones a row and fills it with a tuple, and filling
@@ -1 +1 @@
1
- {"version":3,"file":"lb-hub.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"lb-hub.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
@@ -1,13 +1,13 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_ACTION, ATTR_CELL, ATTR_DELETE, ATTR_ERROR, ATTR_INSERT, ATTR_KEY, ATTR_NAV_LINK, ATTR_PENDING, ATTR_QUERY, ATTR_UPDATE, HUB_TAG_NAME, PAGE_TEMPLATE_PREFIX, LB_DATA_ENDPOINT, LB_EVENT_NAME, LB_REQUEST_ENDPOINT, LB_REQUEST_TIMEOUT_MS, } from "../core/lb-constants";
3
- import { applyData } from "./lb-apply";
2
+ import { ATTR_ACTION, ATTR_CELL, ATTR_DELETE, ATTR_ERROR, ATTR_INSERT, ATTR_KEY, ATTR_NAV_LINK, ATTR_PENDING, ATTR_QUERY, ATTR_UNKNOWN_PAGE, ATTR_UPDATE, HUB_TAG_NAME, PAGE_TEMPLATE_PREFIX, LB_DATA_ENDPOINT, LB_EVENT_NAME, LB_REQUEST_ENDPOINT, LB_REQUEST_TIMEOUT_MS, } from "../core/lb-constants";
3
+ import { applyData, applyTuple } from "./lb-apply";
4
4
  export { applyData, applyTuple } from "./lb-apply";
5
5
  /**
6
- * Where a click happened, in the addressing vocabulary. An action carries
6
+ * Where a click happened, in the binding vocabulary. An action carries
7
7
  * these and nothing else, so there is no channel for a browser-supplied
8
8
  * argument.
9
9
  */
10
- function coordinates(el) {
10
+ function binding(el) {
11
11
  const query = el.closest(`[${ATTR_QUERY}]`)?.getAttribute(ATTR_QUERY);
12
12
  const key = el.closest(`[${ATTR_KEY}]`)?.getAttribute(ATTR_KEY);
13
13
  const cell = el.getAttribute(ATTR_CELL);
@@ -51,10 +51,15 @@ function gatherValues(form) {
51
51
  }
52
52
  return values;
53
53
  }
54
- /** A path names a page host, and nothing finer. */
54
+ /**
55
+ * A path names a page host, and nothing finer. The bare path resolves to
56
+ * `index`, the same convention a static file server follows — an
57
+ * application names its landing page that way rather than the hub singling
58
+ * one out.
59
+ */
55
60
  function pageNameFor(path) {
56
61
  const name = path.replace(/^\/+|\/+$/g, "");
57
- return name === "" ? "counter" : name;
62
+ return name === "" ? "index" : name;
58
63
  }
59
64
  /**
60
65
  * Fetch one round trip against the client-side deadline. Throws — rather
@@ -109,7 +114,7 @@ class LbHub extends HTMLElement {
109
114
  headers: { "content-type": "application/json" },
110
115
  body: JSON.stringify(request),
111
116
  })
112
- .then((data) => applyData(this.main, data))
117
+ .then((data) => applyData(this, data))
113
118
  .catch((err) => {
114
119
  console.error(`lb-hub: request failed`, err);
115
120
  origin?.setAttribute(ATTR_ERROR, "");
@@ -136,37 +141,37 @@ class LbHub extends HTMLElement {
136
141
  const detail = {
137
142
  op: "action",
138
143
  name: el.getAttribute(ATTR_ACTION),
139
- ...coordinates(el),
144
+ ...binding(el),
140
145
  };
141
146
  el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
142
147
  });
143
148
  // The CRUD counterpart to the lb-action delegation above: a native
144
149
  // element needs no declared name to delete the row it is in, only its
145
- // own query/key coordinates.
150
+ // own query/key binding.
146
151
  this.addEventListener("click", (e) => {
147
152
  const el = e.target?.closest(`[${ATTR_DELETE}]`);
148
153
  if (!el || el.localName.includes("-"))
149
154
  return;
150
155
  e.preventDefault();
151
- const { query, key } = coordinates(el);
156
+ const { query, key } = binding(el);
152
157
  if (!query || !key) {
153
- console.warn(`lb-hub: lb-delete with no query/key coordinates, ignoring`, el);
158
+ console.warn(`lb-hub: lb-delete with no query/key binding, ignoring`, el);
154
159
  return;
155
160
  }
156
161
  const detail = { op: "tuple-delete", query, key };
157
162
  el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
158
163
  });
159
- // The CRUD op that gathers rather than addresses a single cell: every
164
+ // The CRUD op that gathers rather than binds a single cell: every
160
165
  // lb-cell inside the form becomes one entry of the values map. There is
161
166
  // no key, because there is no row yet — only the query, found the same
162
- // way coordinates() finds one for a click, says what it is inserting
167
+ // way binding() finds one for a click, says what it is inserting
163
168
  // into.
164
169
  this.addEventListener("submit", (e) => {
165
170
  const form = e.target?.closest(`[${ATTR_INSERT}]`);
166
171
  if (!form)
167
172
  return;
168
173
  e.preventDefault();
169
- const { query } = coordinates(form);
174
+ const { query } = binding(form);
170
175
  if (!query) {
171
176
  console.warn(`lb-hub: lb-insert with no lb-query, ignoring`, form);
172
177
  return;
@@ -187,9 +192,9 @@ class LbHub extends HTMLElement {
187
192
  if (!form)
188
193
  return;
189
194
  e.preventDefault();
190
- const { query, key } = coordinates(form);
195
+ const { query, key } = binding(form);
191
196
  if (!query || !key) {
192
- console.warn(`lb-hub: lb-update with no query/key coordinates, ignoring`, form);
197
+ console.warn(`lb-hub: lb-update with no query/key binding, ignoring`, form);
193
198
  return;
194
199
  }
195
200
  const detail = {
@@ -223,6 +228,7 @@ class LbHub extends HTMLElement {
223
228
  const template = document.getElementById(PAGE_TEMPLATE_PREFIX + page);
224
229
  if (!(template instanceof HTMLTemplateElement)) {
225
230
  console.error(`lb-hub: no page host for '${page}'`);
231
+ this.reportUnknownPage(page);
226
232
  return;
227
233
  }
228
234
  let data = {};
@@ -236,7 +242,28 @@ class LbHub extends HTMLElement {
236
242
  // Insert and hydrate in one synchronous block: the browser does not
237
243
  // paint mid-task, so there is no empty flash.
238
244
  this.main.replaceChildren(template.content.cloneNode(true));
239
- applyData(this.main, data);
245
+ applyData(this, data);
246
+ }
247
+ /**
248
+ * A path that resolves to no page host is discovered client-side, after a
249
+ * successful 200 — every route gets the same document, so there is no
250
+ * HTTP 404 to report (see docs/tutorials/010-pages-and-navigation.md).
251
+ * lb-unknown-page is optional: an application that declares none gets
252
+ * nothing beyond navigate()'s console.error. The builder guarantees the
253
+ * element it's on is a <dialog> (build/assemble.ts), so this can call
254
+ * showModal() without checking the tag here.
255
+ *
256
+ * Scoped to the hub, like every other lookup here, so that one rule holds
257
+ * without exception: what the hub acts on is inside the hub.
258
+ */
259
+ reportUnknownPage(page) {
260
+ const dialog = this.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);
261
+ if (!dialog)
262
+ return;
263
+ applyTuple(dialog, { page });
264
+ const modal = dialog;
265
+ if (!modal.open)
266
+ modal.showModal();
240
267
  }
241
268
  }
242
269
  customElements.define(HUB_TAG_NAME, LbHub);
@@ -1 +1 @@
1
- {"version":3,"file":"lb-rows.d.ts","sourceRoot":"","sources":["../../hub/lb-rows.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE;;;;;GAKG;AACH,MAAM,MAAM,KAAK,GAAG,CAClB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,mBAAmB,KAC1B,IAAI,CAAC;AAwCV;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,KAAK,GAAE,KAAc,GACpB,IAAI,CAkDN"}
1
+ {"version":3,"file":"lb-rows.d.ts","sourceRoot":"","sources":["../../hub/lb-rows.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE;;;;;GAKG;AACH,MAAM,MAAM,KAAK,GAAG,CAClB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,mBAAmB,KAC1B,IAAI,CAAC;AAsCV;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,KAAK,GAAE,KAAc,GACpB,IAAI,CAkDN"}
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * The row machinery a list widget shares — see docs/guide.md, "List
4
- * processing".
3
+ * The row machinery a list widget shares — see docs/reference/custom-elements.md,
4
+ * "Accepting rows".
5
5
  *
6
6
  * A widget owns where a row goes and nothing else. Cloning the template,
7
7
  * matching a tuple to the row already showing it, and filling that row are
@@ -101,6 +101,6 @@ export function applyRows(scope, result, place = before) {
101
101
  // one conditional a page cannot be sent, since the server answers with rows
102
102
  // and says nothing about how many survived reconciliation. A page says what
103
103
  // an empty list looks like in a stylesheet, and no list widget carries code
104
- // for it. See docs/guide.md, "Conditional rendering".
104
+ // for it. See docs/reference/data-binding.md.
105
105
  scope.setAttribute(ATTR_ROW_COUNT, String(showing(scope, template).size));
106
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lb-express.d.ts","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAgB,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAG7D,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,UAAU,CAAC;AAEtD,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CA8DlE"}
1
+ {"version":3,"file":"lb-express.d.ts","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAgB,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAG7D,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,UAAU,CAAC;AAEtD,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CAkElE"}
@@ -41,7 +41,7 @@ export declare function patch(change: {
41
41
  /** The shape of a `<name>.queries.ts` module. */
42
42
  export type Queries = Record<string, Query>;
43
43
  /**
44
- * Where the interaction happened, in the addressing vocabulary, plus the one
44
+ * Where the interaction happened, in the binding vocabulary, plus the one
45
45
  * value a control may carry.
46
46
  *
47
47
  * The browser fills these from attributes it already has. It never names a
@@ -117,7 +117,7 @@ export interface Crud {
117
117
  *
118
118
  * `crud` is the same rule for the typed CRUD operations, keyed by the query
119
119
  * they operate on rather than by a declared name — there is nothing to name,
120
- * since the row's own coordinates say what it is.
120
+ * since the row's own binding says what it is.
121
121
  */
122
122
  export interface Hooks {
123
123
  beforeGet?: (ctx: HubContext) => void | Promise<void>;
@@ -130,8 +130,9 @@ export interface Page {
130
130
  hooks: Hooks;
131
131
  }
132
132
  /**
133
- * The page registry. Nothing scans a directory yet, so the application names
134
- * its pages — see docs/guide.md, "What Hub does not do yet".
133
+ * The page registry. The builder populates this automatically from every
134
+ * `.hooks.ts`/`.queries.ts` pair it discovers — see docs/reference/builder.md,
135
+ * "Generating the server-side page registry".
135
136
  */
136
137
  export type Pages = Record<string, Page>;
137
138
  export interface Hub {
@@ -1 +1 @@
1
- {"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,7 @@ import { tmpdir } from "node:os";
11
11
  import path from "node:path";
12
12
  import { assemble } from "../build/assemble";
13
13
  import { resolveLocations } from "../build/locations";
14
+ import { scanAll } from "../build/origins";
14
15
  async function tree(files) {
15
16
  const dir = await mkdtemp(path.join(tmpdir(), "loadbare-assemble-"));
16
17
  for (const [name, content] of Object.entries(files)) {
@@ -20,15 +21,21 @@ async function tree(files) {
20
21
  }
21
22
  return dir;
22
23
  }
24
+ async function scansFor(locations) {
25
+ return scanAll(locations, locations.srcDir, locations.importsFile);
26
+ }
23
27
  describe("assemble", () => {
24
28
  it("expands a widget written in the chrome, not only in pages", async () => {
25
29
  const src = await tree({
26
- "chrome.chrome.html": `<!doctype html><body><lb-nav exp-label="Site"><a href="/a">A</a></lb-nav><lb-hub><main></main></lb-hub></body>`,
30
+ "chrome.html": `<!doctype html><body><lb-nav exp-label="Site"><a href="/a">A</a></lb-nav><lb-hub><main></main></lb-hub></body>`,
27
31
  "lb-nav.html": `<nav aria-label="{{label}}" lb-slot></nav>`,
28
32
  });
29
33
  try {
30
34
  const locations = await resolveLocations({ src, out: `${src}/dist` });
31
- const { html, usedTags } = await assemble(locations);
35
+ const { html, usedTags } = await assemble({
36
+ ...locations,
37
+ scans: await scansFor(locations),
38
+ });
32
39
  assert.match(html, /<nav aria-label="Site"><a href="\/a">A<\/a><\/nav>/);
33
40
  assert.ok(usedTags.has("lb-nav"));
34
41
  }
@@ -38,11 +45,11 @@ describe("assemble", () => {
38
45
  });
39
46
  it("still throws when the chrome uses an undefined widget", async () => {
40
47
  const src = await tree({
41
- "chrome.chrome.html": `<!doctype html><body><lb-nope></lb-nope></body>`,
48
+ "chrome.html": `<!doctype html><body><lb-nope></lb-nope></body>`,
42
49
  });
43
50
  try {
44
51
  const locations = await resolveLocations({ src, out: `${src}/dist` });
45
- await assert.rejects(() => assemble(locations), /<lb-nope> has no definition/);
52
+ await assert.rejects(async () => assemble({ ...locations, scans: await scansFor(locations) }), /<lb-nope> has no definition/);
46
53
  }
47
54
  finally {
48
55
  await rm(src, { recursive: true, force: true });