@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.
- package/dist/build/assemble.js +1 -0
- package/dist/build/assemble.js.map +1 -0
- package/dist/build/cli.js +1 -0
- package/dist/build/cli.js.map +1 -0
- package/dist/build/elements.js +1 -0
- package/dist/build/elements.js.map +1 -0
- package/dist/build/expand.js +1 -0
- package/dist/build/expand.js.map +1 -0
- package/dist/build/format.js +1 -0
- package/dist/build/format.js.map +1 -0
- package/dist/build/locations.d.ts +2 -2
- package/dist/build/locations.d.ts.map +1 -1
- package/dist/build/locations.js +3 -4
- package/dist/build/locations.js.map +1 -0
- package/dist/build/origins.js +1 -0
- package/dist/build/origins.js.map +1 -0
- package/dist/build/package-root.js +1 -0
- package/dist/build/package-root.js.map +1 -0
- package/dist/build/pages.d.ts +4 -0
- package/dist/build/pages.d.ts.map +1 -1
- package/dist/build/pages.js +5 -0
- package/dist/build/pages.js.map +1 -0
- package/dist/build/styles.js +1 -0
- package/dist/build/styles.js.map +1 -0
- package/dist/core/lb-constants.js +1 -0
- package/dist/core/lb-constants.js.map +1 -0
- package/dist/core/lb-types.d.ts +1 -1
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +2 -1
- package/dist/core/lb-types.js.map +1 -0
- package/dist/hub/lb-apply.d.ts +11 -1
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +50 -13
- package/dist/hub/lb-apply.js.map +1 -0
- package/dist/hub/lb-hub.browser.d.ts +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +117 -109
- package/dist/hub/lb-hub.browser.js.map +1 -0
- package/dist/server/lb-express.d.ts +1 -1
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +1 -0
- package/dist/server/lb-express.js.map +1 -0
- package/dist/server/lb-server.d.ts +1 -1
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +2 -1
- package/dist/server/lb-server.js.map +1 -0
- package/docs/TECHREF-1.0.md +149 -42
- package/docs/analysis-accidental-complexity.md +149 -0
- package/docs/reference/custom-elements.md +10 -17
- package/docs/reference/data-binding.md +56 -27
- package/docs/reference/server.md +18 -0
- package/docs/reference/widgets.md +3 -3
- package/docs/theory.md +114 -1
- package/docs/tutorials/072-inserting-into-a-list.md +14 -11
- package/docs/tutorials/080-widget-requests.md +9 -26
- package/package.json +2 -2
- package/dist/tests/assemble.test.d.ts +0 -8
- package/dist/tests/assemble.test.d.ts.map +0 -1
- package/dist/tests/assemble.test.js +0 -210
- package/dist/tests/elements.test.d.ts +0 -8
- package/dist/tests/elements.test.d.ts.map +0 -1
- package/dist/tests/elements.test.js +0 -118
- package/dist/tests/expand.test.d.ts +0 -10
- package/dist/tests/expand.test.d.ts.map +0 -1
- package/dist/tests/expand.test.js +0 -253
- package/dist/tests/fixtures/elements/collision/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/collision/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/imports.js +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.js +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.js +0 -1
- package/dist/tests/fixtures/elements/manifest/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest/imports.js +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +0 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.js +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.js +0 -1
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +0 -6
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +0 -1
- package/dist/tests/helpers/console.d.ts +0 -20
- package/dist/tests/helpers/console.d.ts.map +0 -1
- package/dist/tests/helpers/console.js +0 -28
- package/dist/tests/helpers/dom.d.ts +0 -18
- package/dist/tests/helpers/dom.d.ts.map +0 -1
- package/dist/tests/helpers/dom.js +0 -22
- package/dist/tests/helpers/hub.d.ts +0 -73
- package/dist/tests/helpers/hub.d.ts.map +0 -1
- package/dist/tests/helpers/hub.js +0 -151
- package/dist/tests/lb-apply.test.d.ts +0 -8
- package/dist/tests/lb-apply.test.d.ts.map +0 -1
- package/dist/tests/lb-apply.test.js +0 -177
- package/dist/tests/lb-express.test.d.ts +0 -14
- package/dist/tests/lb-express.test.d.ts.map +0 -1
- package/dist/tests/lb-express.test.js +0 -243
- package/dist/tests/lb-hub.test.d.ts +0 -14
- package/dist/tests/lb-hub.test.d.ts.map +0 -1
- package/dist/tests/lb-hub.test.js +0 -319
- package/dist/tests/lb-list.test.d.ts +0 -12
- package/dist/tests/lb-list.test.d.ts.map +0 -1
- package/dist/tests/lb-list.test.js +0 -339
- package/dist/tests/lb-server.test.d.ts +0 -9
- package/dist/tests/lb-server.test.d.ts.map +0 -1
- package/dist/tests/lb-server.test.js +0 -546
- package/dist/tests/origins.test.d.ts +0 -10
- package/dist/tests/origins.test.d.ts.map +0 -1
- package/dist/tests/origins.test.js +0 -387
- package/dist/tests/pages.test.d.ts +0 -6
- package/dist/tests/pages.test.d.ts.map +0 -1
- package/dist/tests/pages.test.js +0 -148
- package/dist/tests/styles.test.d.ts +0 -7
- package/dist/tests/styles.test.d.ts.map +0 -1
- 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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
</
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
</
|
|
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
|
|
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
|
|
14
|
-
|
|
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
|
|
70
|
-
`lb-
|
|
71
|
-
|
|
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.
|
|
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"}
|