@loadbare/app 0.8.0 → 0.8.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.
@@ -0,0 +1,134 @@
1
+ # The Builder
2
+
3
+ The application runs `loadbare-app-build`. The builder reads the application's
4
+ source tree and writes the files the server serves.
5
+
6
+ ## Running the builder
7
+
8
+ ```
9
+ loadbare-app-build [--src src] [--out dist] [--watch] [--minify]
10
+ ```
11
+
12
+ | Option | Effect |
13
+ |------------|------------------------------------------------------|
14
+ | `--src` | The tree to scan, default `src` |
15
+ | `--out` | Where output lands, default `dist` |
16
+ | `--watch` | Build once, then again on every change under `--src` |
17
+ | `--minify` | Minify `client.js` and `app.css` |
18
+
19
+ Run it from the application's own `package.json`:
20
+
21
+ ```json
22
+ {
23
+ "scripts": {
24
+ "dev": "loadbare-app-build --watch & tsx server.ts"
25
+ }
26
+ }
27
+ ```
28
+
29
+ `--watch` watches every `.html`, `.ts`, and `.css` file under `--src`, and
30
+ reports a failed build to the console without stopping. It runs no dev server
31
+ and reloads no browser.
32
+
33
+ `--minify` leaves `app.html` alone.
34
+
35
+ ## What the builder reads
36
+
37
+ The builder classifies by name, not location.
38
+
39
+ | Name | Role |
40
+ |----------------------------|---------------------------------------------------|
41
+ | `chrome.html` | The chrome — see [`chrome.html`](./chrome.md) |
42
+ | `*.page.html` | A page |
43
+ | `*.requests.ts` | A page's requests, matched by base name |
44
+ | `*.queries.ts` | A page's queries, matched by base name |
45
+ | `imports.ts` | The packages this app takes widgets from |
46
+ | `*.css` | A stylesheet — see [CSS](./css.md) |
47
+ | Any other `.html` | A widget definition, named for the tag it defines |
48
+ | `<tag>.browser.ts` | A widget script, named for the tag it registers |
49
+
50
+ Give the application exactly one `chrome.html` and at most one `imports.ts`.
51
+ Give every `.requests.ts` and `.queries.ts` a `.page.html` of the same base name.
52
+
53
+ Name a widget script `<tag>.browser.ts`, not `<tag>.ts`. Only a file whose
54
+ name carries `.browser` is bundled for the browser; every other module under
55
+ `--src` stays on the server side of the build, whatever it is called. Write
56
+ `<tag>.ts` and the tag goes unregistered — the build says so, and names the
57
+ file to rename.
58
+
59
+ ## Widgets from packages
60
+
61
+ List a package in `imports.ts` to take widgets from it:
62
+
63
+ ```ts
64
+ // src/imports.ts
65
+ export default ["@acme/widgets"];
66
+ ```
67
+
68
+ The builder scans one directory of that package the same way it scans `--src`,
69
+ finding a widget by its filename. Nothing names a tag: `acme-widget.html` and
70
+ `acme-widget.js` there define and register `<acme-widget>`.
71
+
72
+ Which directory is the package's own to declare, in its `package.json`:
73
+
74
+ ```json
75
+ {
76
+ "loadbare": { "widgets": "./dist" }
77
+ }
78
+ ```
79
+
80
+ A package that declares nothing has its whole installed directory scanned.
81
+ That is what a package of hand-written widgets wants and it needs no field at
82
+ all; a package that compiles wants the field, because its source and its
83
+ compiled output both carry `acme-widget`, and two files claiming one tag in one
84
+ origin is an error. Declaring the directory that ships settles which one the
85
+ builder means, and leaves the rest of the package — the README, the docs, the
86
+ tests — out of the question entirely.
87
+
88
+ The path must be inside the package and must exist in it once installed;
89
+ either failure names the package and stops the build.
90
+
91
+ An application author writes nothing for this and installs the package as they
92
+ would any other. The declaration is the package author's, made once.
93
+
94
+ ## Where the builder looks
95
+
96
+ Definitions, scripts and stylesheets all come from the same ordered origins:
97
+
98
+ | Order | Origin |
99
+ |-------|----------------------------------------------------|
100
+ | 1 | `@loadbare/app` itself, which supplies `lb-hub` |
101
+ | 2 | Each package in `imports.ts`, in the order listed |
102
+ | 3 | The application's own `--src` tree |
103
+
104
+ The first origin holds one tag. Every application has a hub whether or not it
105
+ says so, so the builder supplies that one and nothing else; a widget comes
106
+ from a package the application lists, including
107
+ [`@loadbare/widgets`](./widgets.md).
108
+
109
+ Two files claiming one tag within a single origin is an error, naming both.
110
+ Across origins the later one wins, so the application overrides a package,
111
+ and a package overrides `lb-hub`. Stylesheets follow the same order — see
112
+ [CSS](./css.md).
113
+
114
+ A tag with neither a script nor a definition in any origin is an error.
115
+
116
+ ## What the builder writes
117
+
118
+ | File | Contents |
119
+ |-------------|-----------------------------------------------------|
120
+ | `app.html` | The chrome, with every page inside a `<template>` |
121
+ | `client.js` | Every widget class the document uses, in one bundle |
122
+ | `app.css` | Every stylesheet, concatenated |
123
+ | `pages.ts` | The `hub` the server passes to `hubRoutes` |
124
+
125
+ The builder expands the chrome and every page against the available widget
126
+ definitions — see [Custom Elements](./custom-elements.md#html) for the
127
+ substitution rules — wraps each expanded page in
128
+ `<template lb-page="<name>">`, and splices them into the chrome's `<body>`. It formats the result with
129
+ Prettier when the application has it installed.
130
+
131
+ The builder writes `app.css` only when it finds a stylesheet, and `pages.ts`
132
+ only when some page has a `.requests.ts` or a `.queries.ts` file. Import `hub`
133
+ from `pages.ts` — see [The Express Server](./server.md) for the rest of the
134
+ wiring.
@@ -0,0 +1,158 @@
1
+ # chrome.html
2
+
3
+ The chrome is the application's one HTML document. Banner, navigation,
4
+ footer, dialogs — everything that is not a page lives in the chrome, written
5
+ once.
6
+
7
+ The builder finds the chrome by name: exactly one file called `chrome.html`,
8
+ anywhere in the `src/` tree. Any directory will do.
9
+
10
+ ## A complete chrome
11
+
12
+ Here is a minimal but fully complaint chrome for a typical app:
13
+
14
+ ```html
15
+ <!-- src/chrome.html -->
16
+ <!doctype html>
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="utf-8" />
20
+ <title>Membership Roster</title>
21
+ <script src="/client.js" defer></script>
22
+ <link rel="stylesheet" href="/app.css" />
23
+ <noscript><style>body[hidden] { display: block; }</style></noscript>
24
+ </head>
25
+ <body hidden>
26
+ <lb-hub>
27
+ <header lb-row="lb-navigation">
28
+ <h1>Membership Roster</h1>
29
+ <h2 lb-cell="page-label"></h2>
30
+ </header>
31
+ <nav>
32
+ <a href="/" lb-nav-link>Home</a>
33
+ <a href="/members" lb-nav-link>Members</a>
34
+ <a href="https://example.org/">Our website</a>
35
+ </nav>
36
+ <main></main>
37
+ <dialog lb-unknown-page lb-row="lb-navigation">
38
+ The URL <span lb-cell="page-uri"></span> is not in this app.
39
+ </dialog>
40
+ </lb-hub>
41
+ </body>
42
+ </html>
43
+ ```
44
+
45
+ The chrome is plain HTML; nothing in it is generated or templated.
46
+
47
+ ## The parts of a chrome
48
+
49
+ | Required | Description |
50
+ |-------------------------------------|-------------------------------------------|
51
+ | Exactly one `chrome.html` in `src/` | The one chrome file, found by name |
52
+ | A complete HTML document | Doctype, `<html>`, `<head>`, `<body>` |
53
+ | `<lb-hub>` inside `<body>` | The application's live element |
54
+ | An empty `<main>` inside `<lb-hub>` | Holds the current page |
55
+ | `<script src="/client.js" defer>` | Defines `<lb-hub>` and every other widget |
56
+
57
+ Everything else is optional:
58
+
59
+ | Optional | Description |
60
+ |-------------------------------------------|---------------------------------------------|
61
+ | `<link rel="stylesheet" href="/app.css">` | The bundled stylesheet |
62
+ | `<a lb-nav-link>` | Navigation between pages |
63
+ | `lb-row="lb-navigation"` | Where the page is — see below |
64
+ | `<dialog lb-unknown-page>` | A message when a URL matches no page |
65
+ | Custom elements | The chrome, decomposed into widget files |
66
+ | `<body hidden>` + a `<noscript>` fallback | Avoids a first-load blink — see below |
67
+ | Any other HTML | Header, footer, skip links, meta tags, etc. |
68
+
69
+ ## Rules for writing chrome
70
+
71
+ Anything the user interacts with must be inside `<lb-hub>`. The hub normally
72
+ sits directly inside `<body>`, with banner, nav, footer and `<main>` inside
73
+ it, so Loadbare can act on all of them.
74
+
75
+ A navigation anchor's `href` is a path, and the path names a page:
76
+ `/members` shows `members.page.html`. A bare `/` resolves to `index`, so the
77
+ landing page is the one named `index.page.html`. An anchor without
78
+ `lb-nav-link` is left alone and behaves like any other link.
79
+
80
+ The `lb-unknown-page` attribute, if used, must appear on a `<dialog>` inside
81
+ `<lb-hub>`; the builder rejects it anywhere else. The hub opens it when a
82
+ path names no page. What it says is up to the chrome:
83
+ the dialog is a subtree like any other, and it displays where the page is by
84
+ naming the hub's own query, described next.
85
+
86
+ ## Where the page is
87
+
88
+ On every navigation the hub lands a query of its own, `lb-navigation`, on
89
+ any subtree inside the hub that names it. It arrives the way a server's
90
+ row arrives — `lb-row` on the subtree, `lb-cell` on each element that
91
+ shows a value — so a chrome displays the current page with no code at all:
92
+
93
+ ```html
94
+ <header lb-row="lb-navigation">
95
+ <h1>Membership Roster</h1>
96
+ <h2 lb-cell="page-label"></h2>
97
+ </header>
98
+ ```
99
+
100
+ | Cell | Holds |
101
+ |--------------|-----------------------------------------------------------|
102
+ | `page-label` | The text of the `lb-nav-link` anchor for the path, or empty if none |
103
+ | `page-uri` | The path as the browser has it, such as `/members` |
104
+
105
+ The label is the nav's. The hub takes it from the first `lb-nav-link`
106
+ anchor whose `href` names the current page, so a click, a reload and the
107
+ back button all land the same text, and a path no anchor names lands an
108
+ empty label. A chrome that shows the label somewhere fixed should expect
109
+ that case for a page reachable only by URL.
110
+
111
+ The `lb-` prefix on the query name is what keeps it out of the server's
112
+ namespace: no server answers a query so named. It is landed only where a
113
+ subtree names it, so a chrome that displays no navigation is not warned
114
+ about a query with no scope.
115
+
116
+ The same row is what an unknown-page dialog has to work with. It lands
117
+ before the page host is looked up, so a miss has it too. Name the query on
118
+ the dialog and show whichever cell fits:
119
+
120
+ ```html
121
+ <dialog lb-unknown-page lb-row="lb-navigation">
122
+ The URL <span lb-cell="page-uri"></span> is not in this app.
123
+ </dialog>
124
+ ```
125
+
126
+ `@loadbare/widgets` ships this dialog as a widget, `<lb-unknown-page>`, for a
127
+ chrome that would rather write one tag — see
128
+ [The Basic Widget Library](./widgets.md#lb-unknown-page).
129
+
130
+ ## Preventing the first-load blink
131
+
132
+ `client.js` loads with `defer`, so the browser can — and typically does —
133
+ paint the document before the script has run. Without help, that means a
134
+ visible flash: the chrome appears first, then `<main>`'s real content pops in
135
+ a moment later and shifts everything around it.
136
+
137
+ `<body hidden>` avoids this by hiding the whole document, not just `<main>`,
138
+ until the hub has something to show. `<lb-hub>` un-hides `<body>` itself the
139
+ first time `navigate()` finishes, so the chrome and the first page's content
140
+ always appear together, already in their final layout — there is no
141
+ intermediate state to flash.
142
+
143
+ Hiding `<main>` alone doesn't work: an empty `<main>` already renders at zero
144
+ height, so hiding it changes nothing visible. The pop-in comes from the
145
+ chrome being shown *before* `<main>` has real content, not from `<main>`
146
+ being visibly empty — so it's the whole document that needs to wait, not
147
+ just the piece that was empty.
148
+
149
+ The `<noscript>` block is the escape hatch for a visitor with JavaScript
150
+ disabled. `<lb-hub>` is what removes `hidden` from `<body>`, so a browser
151
+ that never runs `client.js` would otherwise be stuck looking at a
152
+ permanently blank page. `<noscript>` content is only rendered when scripting
153
+ is off, so the fallback rule only ever applies in exactly that case — it
154
+ never runs, and never races with the hub, on a normal visit.
155
+
156
+ Both are optional. An app that doesn't mind the blink, or has no chrome
157
+ complex enough for it to be noticeable, can leave `<body>` unhidden and skip
158
+ the `<noscript>` block entirely.
@@ -0,0 +1,44 @@
1
+ # CSS
2
+
3
+ The builder concatenates every `.css` file it finds into one stylesheet,
4
+ `<out>/app.css`. Nothing is scoped, renamed, or removed.
5
+
6
+ Write ordinary CSS against ordinary markup. Loadbare uses light DOM, so
7
+ every selector reaches every element, in the chrome and in a widget alike.
8
+
9
+ Put stylesheets anywhere under `src/`. The builder finds them by extension
10
+ and pairs them with nothing — a `.css` file is not a widget's, a page's, or
11
+ the chrome's.
12
+
13
+ ## Order
14
+
15
+ The bundle is assembled in partitions, one per origin:
16
+
17
+ | Partition | Holds |
18
+ |----------------------------------|-------------------------------------------|
19
+ | Each package in `imports.ts` | Every `.css` file inside that package |
20
+ | The application's own `src` tree | Every `.css` file under `--src` |
21
+
22
+ Package partitions come first, in the order `imports.ts` first mentions each
23
+ package. The application's own partition is always last, so its rules have
24
+ the final say over any imported package's.
25
+
26
+ A package's partition excludes that package's own nested `node_modules`.
27
+
28
+ Within a partition, files sort by filename. The directory does not affect
29
+ the order, and the full path only breaks a tie. Name a global stylesheet
30
+ `00-reset.css`, or similar, to sort it to the front of its partition.
31
+
32
+ ## What ships
33
+
34
+ The builder writes `app.css` only when it finds at least one stylesheet.
35
+ Serve it at `/app.css` and link it from the chrome:
36
+
37
+ ```html
38
+ <link rel="stylesheet" href="/app.css" />
39
+ ```
40
+
41
+ See [`chrome.html`](./chrome.md) for the link, [The Express
42
+ server](./server.md) for the route, and
43
+ [`loadbare-app-build`](./builder.md#running-the-builder) for `--minify`.
44
+ </content>