@loadbare/app 0.6.0 → 0.7.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 (122) hide show
  1. package/dist/build/assemble.js +1 -0
  2. package/dist/build/assemble.js.map +1 -0
  3. package/dist/build/cli.js +1 -0
  4. package/dist/build/cli.js.map +1 -0
  5. package/dist/build/elements.js +1 -0
  6. package/dist/build/elements.js.map +1 -0
  7. package/dist/build/expand.js +1 -0
  8. package/dist/build/expand.js.map +1 -0
  9. package/dist/build/format.js +1 -0
  10. package/dist/build/format.js.map +1 -0
  11. package/dist/build/locations.d.ts +2 -2
  12. package/dist/build/locations.d.ts.map +1 -1
  13. package/dist/build/locations.js +3 -4
  14. package/dist/build/locations.js.map +1 -0
  15. package/dist/build/origins.js +1 -0
  16. package/dist/build/origins.js.map +1 -0
  17. package/dist/build/package-root.js +1 -0
  18. package/dist/build/package-root.js.map +1 -0
  19. package/dist/build/pages.d.ts +4 -0
  20. package/dist/build/pages.d.ts.map +1 -1
  21. package/dist/build/pages.js +5 -0
  22. package/dist/build/pages.js.map +1 -0
  23. package/dist/build/styles.js +1 -0
  24. package/dist/build/styles.js.map +1 -0
  25. package/dist/core/lb-constants.js +1 -0
  26. package/dist/core/lb-constants.js.map +1 -0
  27. package/dist/core/lb-types.d.ts +1 -1
  28. package/dist/core/lb-types.d.ts.map +1 -1
  29. package/dist/core/lb-types.js +2 -1
  30. package/dist/core/lb-types.js.map +1 -0
  31. package/dist/hub/lb-apply.d.ts +11 -1
  32. package/dist/hub/lb-apply.d.ts.map +1 -1
  33. package/dist/hub/lb-apply.js +50 -13
  34. package/dist/hub/lb-apply.js.map +1 -0
  35. package/dist/hub/lb-hub.browser.d.ts +1 -1
  36. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  37. package/dist/hub/lb-hub.browser.js +117 -109
  38. package/dist/hub/lb-hub.browser.js.map +1 -0
  39. package/dist/server/lb-express.d.ts +1 -1
  40. package/dist/server/lb-express.d.ts.map +1 -1
  41. package/dist/server/lb-express.js +1 -0
  42. package/dist/server/lb-express.js.map +1 -0
  43. package/dist/server/lb-server.d.ts +1 -1
  44. package/dist/server/lb-server.d.ts.map +1 -1
  45. package/dist/server/lb-server.js +2 -1
  46. package/dist/server/lb-server.js.map +1 -0
  47. package/docs/TECHREF-1.0.md +149 -42
  48. package/docs/analysis-accidental-complexity.md +149 -0
  49. package/docs/reference/custom-elements.md +10 -17
  50. package/docs/reference/data-binding.md +56 -27
  51. package/docs/reference/server.md +18 -0
  52. package/docs/reference/widgets.md +3 -3
  53. package/docs/theory.md +114 -1
  54. package/docs/tutorials/072-inserting-into-a-list.md +14 -11
  55. package/docs/tutorials/080-widget-requests.md +9 -26
  56. package/package.json +2 -2
  57. package/dist/tests/assemble.test.d.ts +0 -8
  58. package/dist/tests/assemble.test.d.ts.map +0 -1
  59. package/dist/tests/assemble.test.js +0 -210
  60. package/dist/tests/elements.test.d.ts +0 -8
  61. package/dist/tests/elements.test.d.ts.map +0 -1
  62. package/dist/tests/elements.test.js +0 -118
  63. package/dist/tests/expand.test.d.ts +0 -10
  64. package/dist/tests/expand.test.d.ts.map +0 -1
  65. package/dist/tests/expand.test.js +0 -253
  66. package/dist/tests/fixtures/elements/collision/imports.d.ts +0 -3
  67. package/dist/tests/fixtures/elements/collision/imports.d.ts.map +0 -1
  68. package/dist/tests/fixtures/elements/collision/imports.js +0 -1
  69. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +0 -2
  70. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +0 -1
  71. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.js +0 -1
  72. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +0 -2
  73. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +0 -1
  74. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.js +0 -1
  75. package/dist/tests/fixtures/elements/manifest/imports.d.ts +0 -3
  76. package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +0 -1
  77. package/dist/tests/fixtures/elements/manifest/imports.js +0 -1
  78. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +0 -3
  79. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +0 -1
  80. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +0 -1
  81. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts +0 -5
  82. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +0 -1
  83. package/dist/tests/fixtures/elements/manifest-not-array/imports.js +0 -1
  84. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +0 -2
  85. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +0 -1
  86. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.js +0 -1
  87. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +0 -6
  88. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +0 -1
  89. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +0 -1
  90. package/dist/tests/helpers/console.d.ts +0 -20
  91. package/dist/tests/helpers/console.d.ts.map +0 -1
  92. package/dist/tests/helpers/console.js +0 -28
  93. package/dist/tests/helpers/dom.d.ts +0 -18
  94. package/dist/tests/helpers/dom.d.ts.map +0 -1
  95. package/dist/tests/helpers/dom.js +0 -22
  96. package/dist/tests/helpers/hub.d.ts +0 -73
  97. package/dist/tests/helpers/hub.d.ts.map +0 -1
  98. package/dist/tests/helpers/hub.js +0 -151
  99. package/dist/tests/lb-apply.test.d.ts +0 -8
  100. package/dist/tests/lb-apply.test.d.ts.map +0 -1
  101. package/dist/tests/lb-apply.test.js +0 -177
  102. package/dist/tests/lb-express.test.d.ts +0 -14
  103. package/dist/tests/lb-express.test.d.ts.map +0 -1
  104. package/dist/tests/lb-express.test.js +0 -243
  105. package/dist/tests/lb-hub.test.d.ts +0 -14
  106. package/dist/tests/lb-hub.test.d.ts.map +0 -1
  107. package/dist/tests/lb-hub.test.js +0 -319
  108. package/dist/tests/lb-list.test.d.ts +0 -12
  109. package/dist/tests/lb-list.test.d.ts.map +0 -1
  110. package/dist/tests/lb-list.test.js +0 -339
  111. package/dist/tests/lb-server.test.d.ts +0 -9
  112. package/dist/tests/lb-server.test.d.ts.map +0 -1
  113. package/dist/tests/lb-server.test.js +0 -546
  114. package/dist/tests/origins.test.d.ts +0 -10
  115. package/dist/tests/origins.test.d.ts.map +0 -1
  116. package/dist/tests/origins.test.js +0 -387
  117. package/dist/tests/pages.test.d.ts +0 -6
  118. package/dist/tests/pages.test.d.ts.map +0 -1
  119. package/dist/tests/pages.test.js +0 -148
  120. package/dist/tests/styles.test.d.ts +0 -7
  121. package/dist/tests/styles.test.d.ts.map +0 -1
  122. package/dist/tests/styles.test.js +0 -76
@@ -27,9 +27,9 @@ for where a listed package sits in the cascade.
27
27
 
28
28
  Wraps an `<input>`. `lb-value` sets the input's `.value`. The widget sends
29
29
  nothing on its own: `lb-action` names what the input's `change` sends. The
30
- reserved `lb-cell-change` sends `lb-cell-change`, addressed by the widget's own
31
- `lb-list`/`lb-key-value`/`lb-cell` coordinates; any other name sends that
32
- action with the input's value.
30
+ reserved `lb-cell-change` sends `lb-cell-change`; any other name sends that
31
+ action. Either carries the input's value, and the hub adds the scope the
32
+ input sits in.
33
33
 
34
34
  An input inside an `lb-row-insert` or `lb-row-update` form leaves the attribute off.
35
35
  The form reads every `lb-cell` in it on submit and sends one request for all
package/docs/theory.md CHANGED
@@ -344,4 +344,117 @@ I wrote Loadbare originally in 2003, as "Andromeda", before Node existed
344
344
  and even 4 years before we had JQuery. Andromeda was not nearly as
345
345
  optimized as Loadbare is now, but the principles were the same then as
346
346
  they are now. It has always worked for me, and I hope that it will work
347
- for you.
347
+ for you.
348
+
349
+ ## A Consistency Model
350
+
351
+ > **LLM-authored, not yet revised by a person.** Drafted by Claude on
352
+ > 2026-09-13, from a design session against `@loadbare/app` 0.6.0. Treat
353
+ > it as a proposal for this essay, not as the author's statement.
354
+
355
+ ### From a subjective claim to a checkable one
356
+
357
+ Whether Loadbare carries less accidental complexity than another tool will
358
+ always be disputable. A developer fluent in React has muscle memory for its
359
+ rules and will see Loadbare's rules as foreign, and foreign work feels
360
+ accidental. That objection is about familiarity, and familiarity is a cost
361
+ paid once, in the same way this essay treats the Express boilerplate.
362
+
363
+ A stronger claim can be checked, and a single counterexample in markup
364
+ would refute it. The model for applying data to HTML is:
365
+
366
+ 1. **Internally consistent.** A few rules, each applied the same way
367
+ everywhere, with no exceptions.
368
+ 2. **Consistent with HTML.** Each rule has a counterpart in how HTML
369
+ already behaves, and none contradicts it.
370
+ 3. **Consistent with relational data.** What the server sends is the shape
371
+ a SQL query already returns, so nothing is translated on the way.
372
+
373
+ ### Where the vocabulary comes from
374
+
375
+ This essay says the vocabulary is small because the operations a database
376
+ affords are small and coherent. A sharper statement is that the vocabulary
377
+ sits where two systems that are already coherent meet: the relational model
378
+ and HTML's containment model. Each attribute is a relational idea placed on
379
+ an element.
380
+
381
+ | Loadbare | Relational | HTML precedent |
382
+ | ---------------------------------------- | -------------------------------------------- | ------------------------------------------------------------ |
383
+ | `lb-list` | a relation, a set of rows | `<select>`, `<ul>`: a container whose contents are its items |
384
+ | `lb-row` | a single row | `<form>`: one record of fields |
385
+ | `lb-cell` | a column | `name` on a control: which field this is |
386
+ | `lb-key`, `lb-key-value` | the primary key | `value` on `<option>`: identity apart from the label |
387
+ | `lb-action` | a closed set of writes, and named procedures | `action` on `<form>`: where a submission goes |
388
+ | scope from ancestors | | form ownership, `lang`, `<fieldset disabled>` |
389
+ | a nested scope begins a new one | | a nested element owns its own contents |
390
+ | `lb-row-count`, `lb-pending`, `lb-error` | | state attributes such as `open` on `<details>` |
391
+
392
+ ### The rules
393
+
394
+ 1. **An element's own attributes describe what it displays. Its ancestors
395
+ describe where it belongs.** A picker carrying `lb-list` displays that
396
+ list, and its choice is addressed to the row it sits in, the way a
397
+ `<select>` displays its options and submits to the form around it.
398
+ 2. **A name answers with one shape, a row or a set of rows, and a cell holds
399
+ one value.** Master-detail is a row and a list under two names. Many
400
+ masters with their details is one list of joined rows, grouped for
401
+ display.
402
+ 3. **A value lands where the element shows its state, and a form gathers
403
+ from the same places.** A widget receives `lb-value`, a form control its
404
+ `value`, and any other element its text.
405
+ 4. **A request is an action and a position.** The hub supplies the position
406
+ from the document. Only an interaction supplies a value.
407
+ 5. **The hub owns position and reconciliation. A widget owns interaction and
408
+ placement.**
409
+
410
+ ### Evidence
411
+
412
+ The following decisions each removed an exception or a conflict, and none
413
+ added an attribute. Over the same span the model gained pickers inside rows,
414
+ native selects that show their own state, and nested lists that do not
415
+ destroy each other.
416
+
417
+ | Decision | Removed |
418
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
419
+ | The hub scopes every request | Native requests were scoped and widget requests were not |
420
+ | Scope comes from ancestors, never the element's own `lb-list` | `lb-list` meant "display" in one place and "address" in another |
421
+ | A form control receives its value as `value` | A `<select>` was treated as text, against HTML, and landing did not mirror gathering |
422
+ | Row and template lookups stop at a nested scope | Cells respected nesting while rows and templates did not |
423
+ | Master-detail is a row and a list | An open question born of document-shaped data |
424
+ | `lb-value` on every cell | Only a widget's landed value was visible to a stylesheet |
425
+
426
+ Two signs suggest these are rules rather than patches. Fixing where a
427
+ request's scope comes from also fixed an unrelated console error about insert
428
+ forms, which was never worked on directly. Making rows respect nested scopes
429
+ needed no change to the reference, which already described that behavior.
430
+
431
+ ### Where the claim is not yet proven
432
+
433
+ - **Parameters and view state.** A query takes no argument from the
434
+ browser, so there is nowhere to put which record is selected, a filter, a
435
+ sort, or a collapsed section. The model is incomplete here rather than
436
+ inconsistent. If the answer is HTML's own, the URL, in the way
437
+ `<form method="get">` puts its fields in the query string, the claim grows
438
+ stronger. If it needs a state mechanism with no HTML precedent, that will
439
+ be the model's first real exception.
440
+ - **Checkboxes and radio buttons.** HTML's boolean convention is presence or
441
+ absence, as with `checked`, `hidden` and `disabled`. A rule following it
442
+ would avoid inventing truthiness, though it still touches data types.
443
+ - **Data types.** HTML attributes are strings, so comparing values as strings
444
+ in a stylesheet is consistent with HTML. What is not yet reconciled is
445
+ that a database's typed values arrive untyped.
446
+ - **A nested list shows the same rows in every outer row.** That follows
447
+ from the second rule: one name is one relation. Only a new outer row
448
+ starting with an empty nested list is a mechanical gap.
449
+
450
+ ### A test for every change
451
+
452
+ Before a change is made, ask:
453
+
454
+ - Does it remove an exception, or add one?
455
+ - Does it have an HTML precedent?
456
+ - Does it have a relational counterpart?
457
+
458
+ Stamping every column of a row onto the row element as `data-*` fails the
459
+ first two: attributes nobody wrote, with no precedent in HTML. Writing
460
+ `lb-value` on every cell passes all three.
@@ -8,20 +8,23 @@ that allows a user to add a note.
8
8
  ```html
9
9
  <!-- src/pages/about.page.html -->
10
10
  <h2>Notes</h2>
11
- <form lb-action="lb-row-insert" lb-list="notes">
12
- <input lb-cell="text" placeholder="Write a note" />
13
- <button type="submit">Add</button>
14
- </form>
15
-
16
- <ul lb-list="notes">
17
- <template lb-key="id">
18
- <li lb-cell="text"></li>
19
- </template>
20
- </ul>
11
+ <section lb-list="notes">
12
+ <form lb-action="lb-row-insert">
13
+ <input lb-cell="text" placeholder="Write a note" />
14
+ <button type="submit">Add</button>
15
+ </form>
16
+
17
+ <ul>
18
+ <template lb-key="id">
19
+ <li lb-cell="text"></li>
20
+ </template>
21
+ </ul>
22
+ </section>
21
23
  ```
22
24
 
23
25
  `lb-action="lb-row-insert"` on a `<form>` gathers its `lb-cell`s into a values
24
- map on submit and sends them against `lb-list`. The name is one of
26
+ map on submit and sends them against the `lb-list` around it, the way an
27
+ `<input>` belongs to the `<form>` around it. The name is one of
25
28
  Loadbare's reserved ones — every value beginning with `lb-` is — so it
26
29
  needs no declaring, and nothing of yours may be called that. There's no `lb-key-value` — there's no row
27
30
  yet.
@@ -9,11 +9,9 @@ changes.
9
9
 
10
10
  ## Writing the widget
11
11
 
12
- Our new custom input will no longer rely on Loadbare's form handling,
13
- and so it must reimplement the data scoping logic that is in
14
- Loadbare. This reimplementation approach is neither encouraged nor
15
- discouraged. This tutorial shows only that it is possible, and exposes
16
- how Loadbare internals work.
12
+ Our new custom input will no longer rely on Loadbare's form handling. It
13
+ says what happened and what its value is. Where it sits, the list, the row
14
+ and the cell, is added by the hub.
17
15
 
18
16
  ```html
19
17
  <!-- src/note-input.html -->
@@ -22,13 +20,7 @@ how Loadbare internals work.
22
20
 
23
21
  ```ts
24
22
  // src/note-input.browser.ts
25
- import {
26
- ATTR_CELL,
27
- ATTR_KEY_VALUE,
28
- ATTR_LIST,
29
- ATTR_VALUE,
30
- LB_EVENT_NAME,
31
- } from "@loadbare/app/constants";
23
+ import { ATTR_VALUE, LB_EVENT_NAME } from "@loadbare/app/constants";
32
24
 
33
25
  class NoteInput extends HTMLElement {
34
26
  static observedAttributes = [ATTR_VALUE];
@@ -42,20 +34,10 @@ class NoteInput extends HTMLElement {
42
34
  this.addEventListener("change", () => {
43
35
  const input = this.querySelector("input");
44
36
  if (!input) return;
45
- const list = this.closest(`[${ATTR_LIST}]`)?.getAttribute(ATTR_LIST);
46
- const key = this.closest(`[${ATTR_KEY_VALUE}]`)?.getAttribute(ATTR_KEY_VALUE);
47
- const cell = this.getAttribute(ATTR_CELL);
48
- if (!list || !key || !cell) return;
49
37
  this.dispatchEvent(
50
38
  new CustomEvent(LB_EVENT_NAME, {
51
39
  bubbles: true,
52
- detail: {
53
- action: "lb-cell-change",
54
- list,
55
- key,
56
- cell,
57
- value: input.value,
58
- },
40
+ detail: { action: "lb-cell-change", value: input.value },
59
41
  }),
60
42
  );
61
43
  });
@@ -66,9 +48,10 @@ customElements.define("note-input", NoteInput);
66
48
  ```
67
49
 
68
50
  A widget with both a `.html` and a `.browser.ts` file shares one tag name; the
69
- build finds each half independently. On change, it reads its own
70
- `lb-list`/`lb-key-value`/`lb-cell` coordinates and dispatches a `lb-cell-change`
71
- request itself, the way `lb-row-update` does for a whole form.
51
+ build finds each half independently. On change, it dispatches a
52
+ `lb-cell-change` carrying the input's value. The hub adds the list, the key
53
+ of the row it is in and its own `lb-cell`, the same way it does for a
54
+ `lb-row-update` form.
72
55
 
73
56
  ## Using it
74
57
 
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.6.0",
4
+ "version": "0.7.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -32,7 +32,7 @@
32
32
  "prebuild": "node --eval \"fs.rmSync('dist',{recursive:true,force:true})\" --input-type=module",
33
33
  "build:server": "tsc --project tsconfig.build.json",
34
34
  "build": "npm run build:server",
35
- "postbuild": "node --eval \"fs.chmodSync('dist/build/cli.js', 0o755)\" --input-type=module",
35
+ "postbuild": "node --eval \"fs.chmodSync('dist/build/cli.js', 0o755)\" --input-type=module && node ../../scripts/check-dist-imports.mjs",
36
36
  "test": "tsx --test \"tests/**/*.test.ts\"",
37
37
  "typecheck": "tsc --noEmit",
38
38
  "format": "prettier --write .",
@@ -1,8 +0,0 @@
1
- /**
2
- * Assembling a chrome file and its pages — see build/assemble.ts.
3
- *
4
- * The chrome and pages together are one write to a temp tree per test,
5
- * since assemble() reads from disk rather than taking sources in memory.
6
- */
7
- export {};
8
- //# sourceMappingURL=assemble.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"assemble.test.d.ts","sourceRoot":"","sources":["../../tests/assemble.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -1,210 +0,0 @@
1
- /**
2
- * Assembling a chrome file and its pages — see build/assemble.ts.
3
- *
4
- * The chrome and pages together are one write to a temp tree per test,
5
- * since assemble() reads from disk rather than taking sources in memory.
6
- */
7
- import assert from "node:assert/strict";
8
- import { describe, it } from "node:test";
9
- import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
10
- import { tmpdir } from "node:os";
11
- import path from "node:path";
12
- import { assemble } from "../build/assemble";
13
- import { resolveLocations } from "../build/locations";
14
- import { scanAll } from "../build/origins";
15
- async function tree(files) {
16
- const dir = await mkdtemp(path.join(tmpdir(), "loadbare-assemble-"));
17
- for (const [name, content] of Object.entries(files)) {
18
- const file = path.join(dir, name);
19
- await mkdir(path.dirname(file), { recursive: true });
20
- await writeFile(file, content, "utf-8");
21
- }
22
- return dir;
23
- }
24
- async function scansFor(locations) {
25
- return scanAll(locations, locations.srcDir, locations.importsFile);
26
- }
27
- describe("assemble", () => {
28
- it("expands a widget written in the chrome, not only in pages", async () => {
29
- const src = await tree({
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>`,
31
- "lb-nav.html": `<nav aria-label="{{label}}" lb-slot></nav>`,
32
- });
33
- try {
34
- const locations = await resolveLocations({ src, out: `${src}/dist` });
35
- const { html, usedTags } = await assemble({
36
- ...locations,
37
- scans: await scansFor(locations),
38
- });
39
- assert.match(html, /<nav aria-label="Site"><a href="\/a">A<\/a><\/nav>/);
40
- assert.ok(usedTags.has("lb-nav"));
41
- }
42
- finally {
43
- await rm(src, { recursive: true, force: true });
44
- }
45
- });
46
- it("reports a chrome widget with no definition as a used tag, for elements.ts to refuse", async () => {
47
- // Expansion leaves an undefined tag alone; whether it can do anything is
48
- // one rule in one place — see build/elements.ts.
49
- const src = await tree({
50
- "chrome.html": `<!doctype html><body><lb-nope></lb-nope></body>`,
51
- });
52
- try {
53
- const locations = await resolveLocations({ src, out: `${src}/dist` });
54
- const { html, usedTags } = await assemble({
55
- ...locations,
56
- scans: await scansFor(locations),
57
- });
58
- assert.match(html, /<lb-nope><\/lb-nope>/);
59
- assert.ok(usedTags.has("lb-nope"));
60
- }
61
- finally {
62
- await rm(src, { recursive: true, force: true });
63
- }
64
- });
65
- });
66
- describe("the unknown-page dialog", () => {
67
- it("accepts a chrome that declares none", async () => {
68
- const src = await tree({
69
- "chrome.html": `<!doctype html><body><lb-hub><main></main></lb-hub></body>`,
70
- });
71
- try {
72
- const locations = await resolveLocations({ src, out: `${src}/dist` });
73
- const { html } = await assemble({
74
- ...locations,
75
- scans: await scansFor(locations),
76
- });
77
- assert.doesNotMatch(html, /lb-unknown-page/);
78
- }
79
- finally {
80
- await rm(src, { recursive: true, force: true });
81
- }
82
- });
83
- it("refuses the attribute on anything but a <dialog>", async () => {
84
- const src = await tree({
85
- "chrome.html": `<!doctype html><body><lb-hub><main></main><div lb-unknown-page></div></lb-hub></body>`,
86
- });
87
- try {
88
- const locations = await resolveLocations({ src, out: `${src}/dist` });
89
- const scans = await scansFor(locations);
90
- await assert.rejects(() => assemble({ ...locations, scans }), /lb-unknown-page on <div>, must be a <dialog>/);
91
- }
92
- finally {
93
- await rm(src, { recursive: true, force: true });
94
- }
95
- });
96
- it("refuses a dialog outside the hub, where the hub cannot reach it", async () => {
97
- const src = await tree({
98
- "chrome.html": `<!doctype html><body><dialog lb-unknown-page></dialog><lb-hub><main></main></lb-hub></body>`,
99
- });
100
- try {
101
- const locations = await resolveLocations({ src, out: `${src}/dist` });
102
- const scans = await scansFor(locations);
103
- await assert.rejects(() => assemble({ ...locations, scans }), /lb-unknown-page outside <lb-hub>/);
104
- }
105
- finally {
106
- await rm(src, { recursive: true, force: true });
107
- }
108
- });
109
- });
110
- /**
111
- * The binding vocabulary, checked against the finished document rather than
112
- * discovered one console error at a time in a browser — see walkBinding in
113
- * build/assemble.ts. Every case here is a build that must not produce output.
114
- */
115
- describe("the binding check", () => {
116
- const CHROME = `<!doctype html><body><lb-hub><main></main></lb-hub></body>`;
117
- /** Assemble a chrome and one page, and hand back whatever it did. */
118
- async function build(files) {
119
- const src = await tree({ "chrome.html": CHROME, ...files });
120
- try {
121
- const locations = await resolveLocations({ src, out: `${src}/dist` });
122
- const scans = await scansFor(locations);
123
- return await assemble({ ...locations, scans });
124
- }
125
- finally {
126
- await rm(src, { recursive: true, force: true });
127
- }
128
- }
129
- async function refuses(page, pattern) {
130
- await assert.rejects(() => build({ "a.page.html": page }), pattern);
131
- }
132
- it("refuses both scope attributes on one element", async () => {
133
- await refuses(`<ul lb-list="roster" lb-row="prefs"></ul>`, /carries both lb-list and lb-row/);
134
- });
135
- it("refuses a cell with no scope around it", async () => {
136
- await refuses(`<p><span lb-cell="name"></span></p>`, /carries lb-cell="name" with no lb-list or lb-row scope/);
137
- });
138
- it("refuses a cell whose only scope is inside a template with none around it", async () => {
139
- // A template's children are walked in the scope enclosing the template,
140
- // because a row template's clones land in the list around it.
141
- await refuses(`<template><span lb-cell="name"></span></template>`, /carries lb-cell="name" with no lb-list or lb-row scope/);
142
- });
143
- it("refuses lb-key on anything but a template", async () => {
144
- await refuses(`<ul lb-list="roster"><li lb-key="id"></li></ul>`, /<li> carries lb-key; it names the column/);
145
- });
146
- it("refuses lb-key on a template outside any list scope", async () => {
147
- await refuses(`<div lb-row="prefs"><template lb-key="id"><p></p></template></div>`, /carries lb-key outside any lb-list scope/);
148
- });
149
- it("refuses a template carrying lb-key with no scope at all", async () => {
150
- await refuses(`<template lb-key="id"><p></p></template>`, /carries lb-key outside any lb-list scope/);
151
- });
152
- it("reports every problem in one build, not the first", async () => {
153
- await refuses(`<span lb-cell="name"></span><ul lb-list="a" lb-row="b"></ul>`, /no lb-list or lb-row scope[\s\S]*carries both lb-list and lb-row/);
154
- });
155
- it("names the file the problem is in", async () => {
156
- await refuses(`<span lb-cell="name"></span>`, /a\.page\.html' binding:/);
157
- });
158
- it("checks the chrome as well as the pages", async () => {
159
- const src = await tree({
160
- "chrome.html": `<!doctype html><body><lb-hub><main></main><span lb-cell="name"></span></lb-hub></body>`,
161
- });
162
- try {
163
- const locations = await resolveLocations({ src, out: `${src}/dist` });
164
- const scans = await scansFor(locations);
165
- await assert.rejects(() => assemble({ ...locations, scans }), /chrome\.html' binding:/);
166
- }
167
- finally {
168
- await rm(src, { recursive: true, force: true });
169
- }
170
- });
171
- it("accepts a cell in either kind of scope", async () => {
172
- const { html } = await build({
173
- "a.page.html": `
174
- <div lb-row="prefs"><span lb-cell="theme"></span></div>
175
- <ul lb-list="roster">
176
- <template lb-key="id"><li lb-cell="name"></li></template>
177
- </ul>`,
178
- });
179
- assert.match(html, /lb-cell="theme"/);
180
- assert.match(html, /lb-cell="name"/);
181
- });
182
- it("accepts a nested scope, which begins a scope of its own", async () => {
183
- const { html } = await build({
184
- "a.page.html": `
185
- <div lb-row="prefs">
186
- <ul lb-list="roster">
187
- <template lb-key="id"><li lb-cell="name"></li></template>
188
- </ul>
189
- </div>`,
190
- });
191
- assert.match(html, /lb-cell="name"/);
192
- });
193
- it("scopes a template's content by the scope the template itself carries", async () => {
194
- // A template with no scope of its own is walked in the scope enclosing
195
- // it, which is what makes a row template's cells legal. One that carries
196
- // a scope establishes it for its own content instead. That is today's
197
- // behavior, and this is the test that flips if it changes.
198
- const { html } = await build({
199
- "a.page.html": `<template lb-row="prefs"><span lb-cell="theme"></span></template>`,
200
- });
201
- assert.match(html, /lb-cell="theme"/);
202
- });
203
- it("accepts a list scope with no row template, which displays nothing", async () => {
204
- // An insert form names the list it adds to and has no rows of its own.
205
- const { html } = await build({
206
- "a.page.html": `<form lb-list="roster" lb-action="lb-row-insert"></form>`,
207
- });
208
- assert.match(html, /lb-list="roster"/);
209
- });
210
- });
@@ -1,8 +0,0 @@
1
- /**
2
- * Resolving a used tag to the script that registers it — see build/elements.ts.
3
- *
4
- * Fixtures under tests/fixtures/elements/ each stand in for one <src> tree,
5
- * the same role tests/fixtures/definitions plays for expand.ts.
6
- */
7
- export {};
8
- //# sourceMappingURL=elements.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"elements.test.d.ts","sourceRoot":"","sources":["../../tests/elements.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -1,118 +0,0 @@
1
- /**
2
- * Resolving a used tag to the script that registers it — see build/elements.ts.
3
- *
4
- * Fixtures under tests/fixtures/elements/ each stand in for one <src> tree,
5
- * the same role tests/fixtures/definitions plays for expand.ts.
6
- */
7
- import assert from "node:assert/strict";
8
- import { describe, it } from "node:test";
9
- import { fileURLToPath } from "node:url";
10
- import { customElementTags } from "../build/expand";
11
- import { loadElementManifest, resolveElements } from "../build/elements";
12
- import { packageInfo } from "../build/locations";
13
- import { scanOrigin } from "../build/origins";
14
- const FIXTURES = fileURLToPath(new URL("./fixtures/elements", import.meta.url));
15
- /**
16
- * The cascade resolveElements reads, built here rather than by
17
- * build/origins.ts's buildOrigins: these fixtures name packages that are not
18
- * installed, and resolution is what is under test, not package lookup.
19
- */
20
- async function scansFor(src, packageDir) {
21
- const info = await packageInfo();
22
- const origins = [
23
- {
24
- name: info.packageName,
25
- dirs: [info.builtinHubDir],
26
- specifier: () => info.packageName,
27
- },
28
- ...(packageDir ? [{ name: "@acme/widget", dirs: [packageDir] }] : []),
29
- { name: src, dirs: [src] },
30
- ];
31
- return Promise.all(origins.map(scanOrigin));
32
- }
33
- describe("customElementTags", () => {
34
- it("collects every hyphenated tag, native elements excluded", () => {
35
- const tags = customElementTags(`<div><lb-input></lb-input><app-box>hi</app-box><span></span></div>`);
36
- assert.deepEqual([...tags].sort(), ["app-box", "lb-input"]);
37
- });
38
- it("reaches into template content", () => {
39
- const tags = customElementTags(`<template><acme-widget></acme-widget></template>`);
40
- assert.deepEqual([...tags], ["acme-widget"]);
41
- });
42
- });
43
- describe("loadElementManifest", () => {
44
- it("returns the declared package list", async () => {
45
- const declared = await loadElementManifest(`${FIXTURES}/manifest/imports.ts`);
46
- assert.deepEqual(declared, ["@acme/widget"]);
47
- });
48
- it("returns empty when there is no imports.ts", async () => {
49
- const declared = await loadElementManifest(`${FIXTURES}/local/imports.ts`);
50
- assert.deepEqual(declared, []);
51
- });
52
- it("rejects a default export that is not an array", async () => {
53
- await assert.rejects(() => loadElementManifest(`${FIXTURES}/manifest-not-array/imports.ts`), /must default-export an array of package names/);
54
- });
55
- it("rejects an entry that is not a package name", async () => {
56
- await assert.rejects(() => loadElementManifest(`${FIXTURES}/manifest-bad-entry/imports.ts`), /every entry must be a package name/);
57
- });
58
- });
59
- describe("resolveElements", () => {
60
- it("resolves lb-hub to the package itself, the one built-in", async () => {
61
- const { resolved } = resolveElements({
62
- used: new Set(["lb-hub"]),
63
- scans: await scansFor(`${FIXTURES}/local`),
64
- });
65
- assert.deepEqual(resolved.get("lb-hub"), {
66
- kind: "specifier",
67
- value: "@loadbare/app",
68
- });
69
- });
70
- it("resolves a local tag to its file in <src>", async () => {
71
- const { resolved } = resolveElements({
72
- used: new Set(["app-box"]),
73
- scans: await scansFor(`${FIXTURES}/local`),
74
- });
75
- assert.equal(resolved.get("app-box")?.kind, "file");
76
- assert.match(resolved.get("app-box").value, /local\/widgets\/app-box\.browser$/);
77
- });
78
- it("resolves a tag found by scanning a declared package", async () => {
79
- const { resolved } = resolveElements({
80
- used: new Set(["acme-widget"]),
81
- scans: await scansFor(`${FIXTURES}/manifest`, `${FIXTURES}/pkg`),
82
- });
83
- assert.equal(resolved.get("acme-widget")?.kind, "file");
84
- assert.match(resolved.get("acme-widget").value, /pkg\/acme-widget\.browser$/);
85
- });
86
- it("needs no script for a tag with only an .html definition", async () => {
87
- const { resolved } = resolveElements({
88
- used: new Set(["lb-nav"]),
89
- scans: await scansFor(`${FIXTURES}/markup-only`),
90
- });
91
- assert.equal(resolved.size, 0);
92
- });
93
- it("names the unmarked file when a tag's script forgot to say .browser", async () => {
94
- await assert.rejects(async () => resolveElements({
95
- used: new Set(["app-box"]),
96
- scans: await scansFor(`${FIXTURES}/unmarked`),
97
- }), (err) => {
98
- assert.match(err.message, /<app-box> has no script/);
99
- assert.match(err.message, /widgets[/\\]app-box\.ts' is named after it/);
100
- assert.match(err.message, /rename it 'app-box\.browser\.ts'/);
101
- return true;
102
- });
103
- });
104
- it("throws when a used tag registers nothing", async () => {
105
- await assert.rejects(async () => resolveElements({
106
- used: new Set(["nowhere-widget"]),
107
- scans: await scansFor(`${FIXTURES}/local`),
108
- }), /<nowhere-widget> has neither a script nor a definition/);
109
- });
110
- it("lets src win over a package that has the same tag", async () => {
111
- const { resolved } = resolveElements({
112
- used: new Set(["acme-widget"]),
113
- scans: await scansFor(`${FIXTURES}/collision`, `${FIXTURES}/pkg`),
114
- });
115
- assert.equal(resolved.get("acme-widget")?.kind, "file");
116
- assert.match(resolved.get("acme-widget").value, /collision\/widgets\/acme-widget\.browser$/);
117
- });
118
- });
@@ -1,10 +0,0 @@
1
- /**
2
- * Expansion — see docs/reference/custom-elements.md and docs/testing.md, tier 1.
3
- *
4
- * Definitions are written inline here because `expand` takes them as a map
5
- * and a two-line definition beside its assertion reads better than a file
6
- * reference. The cases that genuinely need files — the checks `loadDefinitions`
7
- * runs, and directory ordering — use tests/fixtures/definitions.
8
- */
9
- export {};
10
- //# sourceMappingURL=expand.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"expand.test.d.ts","sourceRoot":"","sources":["../../tests/expand.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}