@loadbare/app 0.4.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/LICENSE +201 -0
- package/README.md +95 -0
- package/dist/build/assemble.d.ts +31 -0
- package/dist/build/assemble.d.ts.map +1 -0
- package/dist/build/assemble.js +53 -0
- package/dist/build/cli.d.ts +30 -0
- package/dist/build/cli.d.ts.map +1 -0
- package/dist/build/cli.js +107 -0
- package/dist/build/elements.d.ts +61 -0
- package/dist/build/elements.d.ts.map +1 -0
- package/dist/build/elements.js +158 -0
- package/dist/build/expand.d.ts +45 -0
- package/dist/build/expand.d.ts.map +1 -0
- package/dist/build/expand.js +386 -0
- package/dist/build/format.d.ts +28 -0
- package/dist/build/format.d.ts.map +1 -0
- package/dist/build/format.js +42 -0
- package/dist/build/locations.d.ts +87 -0
- package/dist/build/locations.d.ts.map +1 -0
- package/dist/build/locations.js +173 -0
- package/dist/build/package-root.d.ts +9 -0
- package/dist/build/package-root.d.ts.map +1 -0
- package/dist/build/package-root.js +24 -0
- package/dist/build/pages.d.ts +25 -0
- package/dist/build/pages.d.ts.map +1 -0
- package/dist/build/pages.js +54 -0
- package/dist/build/styles.d.ts +13 -0
- package/dist/build/styles.d.ts.map +1 -0
- package/dist/build/styles.js +18 -0
- package/dist/client.js +522 -0
- package/dist/core/lb-constants.d.ts +23 -0
- package/dist/core/lb-constants.d.ts.map +1 -0
- package/dist/core/lb-constants.js +95 -0
- package/dist/core/lb-types.d.ts +88 -0
- package/dist/core/lb-types.d.ts.map +1 -0
- package/dist/core/lb-types.js +5 -0
- package/dist/demo-static/src/widgets/app-box.d.ts +15 -0
- package/dist/demo-static/src/widgets/app-box.d.ts.map +1 -0
- package/dist/demo-static/src/widgets/app-box.js +19 -0
- package/dist/hub/lb-apply.d.ts +13 -0
- package/dist/hub/lb-apply.d.ts.map +1 -0
- package/dist/hub/lb-apply.js +77 -0
- package/dist/hub/lb-hub.d.ts +2 -0
- package/dist/hub/lb-hub.d.ts.map +1 -0
- package/dist/hub/lb-hub.js +242 -0
- package/dist/hub/lb-rows.d.ts +18 -0
- package/dist/hub/lb-rows.d.ts.map +1 -0
- package/dist/hub/lb-rows.js +106 -0
- package/dist/server/lb-express.d.ts +28 -0
- package/dist/server/lb-express.d.ts.map +1 -0
- package/dist/server/lb-express.js +77 -0
- package/dist/server/lb-server.d.ts +174 -0
- package/dist/server/lb-server.d.ts.map +1 -0
- package/dist/server/lb-server.js +79 -0
- package/dist/tests/assemble.test.d.ts +8 -0
- package/dist/tests/assemble.test.d.ts.map +1 -0
- package/dist/tests/assemble.test.js +51 -0
- package/dist/tests/elements.test.d.ts +8 -0
- package/dist/tests/elements.test.d.ts.map +1 -0
- package/dist/tests/elements.test.js +111 -0
- package/dist/tests/expand.test.d.ts +10 -0
- package/dist/tests/expand.test.d.ts.map +1 -0
- package/dist/tests/expand.test.js +226 -0
- package/dist/tests/fixtures/elements/collision/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/collision/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/collision/elements.js +3 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts +2 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.js +1 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts +2 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.js +1 -0
- package/dist/tests/fixtures/elements/manifest/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest/elements.js +3 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +3 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +3 -0
- package/dist/tests/golden.test.d.ts +19 -0
- package/dist/tests/golden.test.d.ts.map +1 -0
- package/dist/tests/golden.test.js +60 -0
- package/dist/tests/helpers/console.d.ts +20 -0
- package/dist/tests/helpers/console.d.ts.map +1 -0
- package/dist/tests/helpers/console.js +28 -0
- package/dist/tests/helpers/dom.d.ts +18 -0
- package/dist/tests/helpers/dom.d.ts.map +1 -0
- package/dist/tests/helpers/dom.js +22 -0
- package/dist/tests/helpers/window.d.ts +43 -0
- package/dist/tests/helpers/window.d.ts.map +1 -0
- package/dist/tests/helpers/window.js +78 -0
- package/dist/tests/lb-apply.test.d.ts +8 -0
- package/dist/tests/lb-apply.test.d.ts.map +1 -0
- package/dist/tests/lb-apply.test.js +153 -0
- package/dist/tests/lb-express.test.d.ts +14 -0
- package/dist/tests/lb-express.test.d.ts.map +1 -0
- package/dist/tests/lb-express.test.js +238 -0
- package/dist/tests/lb-input.test.d.ts +9 -0
- package/dist/tests/lb-input.test.d.ts.map +1 -0
- package/dist/tests/lb-input.test.js +78 -0
- package/dist/tests/lb-list.test.d.ts +12 -0
- package/dist/tests/lb-list.test.d.ts.map +1 -0
- package/dist/tests/lb-list.test.js +44 -0
- package/dist/tests/lb-options.test.d.ts +10 -0
- package/dist/tests/lb-options.test.d.ts.map +1 -0
- package/dist/tests/lb-options.test.js +121 -0
- package/dist/tests/lb-picker.test.d.ts +14 -0
- package/dist/tests/lb-picker.test.d.ts.map +1 -0
- package/dist/tests/lb-picker.test.js +59 -0
- package/dist/tests/lb-rows.test.d.ts +12 -0
- package/dist/tests/lb-rows.test.d.ts.map +1 -0
- package/dist/tests/lb-rows.test.js +336 -0
- package/dist/tests/lb-select.test.d.ts +9 -0
- package/dist/tests/lb-select.test.d.ts.map +1 -0
- package/dist/tests/lb-select.test.js +71 -0
- package/dist/tests/lb-server.test.d.ts +9 -0
- package/dist/tests/lb-server.test.d.ts.map +1 -0
- package/dist/tests/lb-server.test.js +495 -0
- package/dist/tests/lb-table.test.d.ts +15 -0
- package/dist/tests/lb-table.test.d.ts.map +1 -0
- package/dist/tests/lb-table.test.js +205 -0
- package/dist/tests/pages.test.d.ts +6 -0
- package/dist/tests/pages.test.d.ts.map +1 -0
- package/dist/tests/pages.test.js +98 -0
- package/dist/tests/styles.test.d.ts +7 -0
- package/dist/tests/styles.test.d.ts.map +1 -0
- package/dist/tests/styles.test.js +73 -0
- package/dist/widgets/index.d.ts +7 -0
- package/dist/widgets/index.d.ts.map +1 -0
- package/dist/widgets/index.js +6 -0
- package/dist/widgets/lb-input.d.ts +2 -0
- package/dist/widgets/lb-input.d.ts.map +1 -0
- package/dist/widgets/lb-input.js +48 -0
- package/dist/widgets/lb-list.d.ts +2 -0
- package/dist/widgets/lb-list.d.ts.map +1 -0
- package/dist/widgets/lb-list.js +17 -0
- package/dist/widgets/lb-options.d.ts +26 -0
- package/dist/widgets/lb-options.d.ts.map +1 -0
- package/dist/widgets/lb-options.js +72 -0
- package/dist/widgets/lb-picker.d.ts +2 -0
- package/dist/widgets/lb-picker.d.ts.map +1 -0
- package/dist/widgets/lb-picker.js +25 -0
- package/dist/widgets/lb-select.d.ts +2 -0
- package/dist/widgets/lb-select.d.ts.map +1 -0
- package/dist/widgets/lb-select.js +43 -0
- package/dist/widgets/lb-table.d.ts +2 -0
- package/dist/widgets/lb-table.d.ts.map +1 -0
- package/dist/widgets/lb-table.js +113 -0
- package/docs/application-chrome.md +36 -0
- package/docs/building-html-pages.md +130 -0
- package/docs/getting-started.md +120 -0
- package/docs/guide.md +1164 -0
- package/docs/hosting.md +218 -0
- package/docs/latent-risks.md +20 -0
- package/docs/theory.md +226 -0
- package/package.json +85 -0
- package/widgets/index.ts +6 -0
- package/widgets/lb-input.html +1 -0
- package/widgets/lb-input.ts +64 -0
- package/widgets/lb-list.html +1 -0
- package/widgets/lb-list.ts +21 -0
- package/widgets/lb-options.html +4 -0
- package/widgets/lb-options.ts +88 -0
- package/widgets/lb-picker.html +7 -0
- package/widgets/lb-picker.ts +27 -0
- package/widgets/lb-select.html +4 -0
- package/widgets/lb-select.ts +55 -0
- package/widgets/lb-table.html +8 -0
- package/widgets/lb-table.ts +126 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Express adapter — the standard hand-off from an Express router to a
|
|
3
|
+
* Hub built by createHub(). Every Hub application wires this same router,
|
|
4
|
+
* which is why it ships from here rather than being copied into each one.
|
|
5
|
+
*
|
|
6
|
+
* Express itself is a peer dependency: nothing outside this file imports it,
|
|
7
|
+
* so an application that hosts Hub some other way never pays for it.
|
|
8
|
+
*
|
|
9
|
+
* Two endpoints, and no third:
|
|
10
|
+
*
|
|
11
|
+
* GET /hub/data?page=<name> the page's whole query set
|
|
12
|
+
* POST /hub?page=<name> one operation, then its refresh set
|
|
13
|
+
*
|
|
14
|
+
* The page rides on the query string rather than in the body, which is what
|
|
15
|
+
* lets the operation set stay closed.
|
|
16
|
+
*/
|
|
17
|
+
import express from "express";
|
|
18
|
+
import { LB_DATA_ENDPOINT, LB_REQUEST_ENDPOINT } from "../core/lb-constants";
|
|
19
|
+
export function hubRoutes(hub, contextFor) {
|
|
20
|
+
const router = express.Router();
|
|
21
|
+
// Mounted here rather than on the app so that Hub's need for a parsed
|
|
22
|
+
// body is stated where the routes that need it are.
|
|
23
|
+
router.use(express.json());
|
|
24
|
+
router.get(LB_DATA_ENDPOINT, async (req, res) => {
|
|
25
|
+
const page = String(req.query.page ?? "");
|
|
26
|
+
res.json(await hub.dataForPage(page, contextFor(req)));
|
|
27
|
+
});
|
|
28
|
+
router.post(LB_REQUEST_ENDPOINT, async (req, res) => {
|
|
29
|
+
const page = String(req.query.page ?? "");
|
|
30
|
+
// The body is unknown until op says which shape it is, so it is read
|
|
31
|
+
// once through this loose view for the case that does not match one.
|
|
32
|
+
const body = req.body;
|
|
33
|
+
const request = req.body;
|
|
34
|
+
const ctx = contextFor(req);
|
|
35
|
+
// A thrown error is how application code (a query, an action, a CRUD
|
|
36
|
+
// op) says "this request is invalid" or hits an unexpected failure —
|
|
37
|
+
// it resolves to a real response rather than an unhandled rejection, so
|
|
38
|
+
// the browser side has something to catch.
|
|
39
|
+
try {
|
|
40
|
+
switch (request?.op) {
|
|
41
|
+
case "action": {
|
|
42
|
+
const { name, query, key, cell, value } = request;
|
|
43
|
+
res.json(await hub.runAction(page, name, { query, key, cell, value }, ctx));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
case "cell-change": {
|
|
47
|
+
const { query, key, cell, value } = request;
|
|
48
|
+
res.json(await hub.runCellChange(page, { query, key, cell, value }, ctx));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
case "tuple-delete": {
|
|
52
|
+
const { query, key } = request;
|
|
53
|
+
res.json(await hub.runTupleDelete(page, { query, key }, ctx));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
case "tuple-insert": {
|
|
57
|
+
const { query, values } = request;
|
|
58
|
+
res.json(await hub.runTupleInsert(page, { query, values }, ctx));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
case "tuple-update": {
|
|
62
|
+
const { query, key, values } = request;
|
|
63
|
+
res.json(await hub.runTupleUpdate(page, { query, key, values }, ctx));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
console.warn(`loadbare: operation '${body?.op}' not implemented`);
|
|
68
|
+
res.status(400).json({});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error(`loadbare: operation '${request?.op}' threw`, err);
|
|
73
|
+
res.status(500).json({});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return router;
|
|
77
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Projection, QueryResult, HubData, HubResult } from "../core/lb-types";
|
|
2
|
+
/**
|
|
3
|
+
* Whatever the application hands the engine for the duration of one request.
|
|
4
|
+
*
|
|
5
|
+
* Hub declares it empty and never reads it. An application fills it in by
|
|
6
|
+
* declaration merging, once, anywhere in its own source:
|
|
7
|
+
*
|
|
8
|
+
* declare module "@loadbare/app/server" {
|
|
9
|
+
* interface HubContext {
|
|
10
|
+
* db: Db;
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* That is why no type on this page takes a type parameter. The context is a
|
|
15
|
+
* request-scoped handle — an authenticated database connection is the
|
|
16
|
+
* expected case — so it is passed per call rather than held by the engine.
|
|
17
|
+
*/
|
|
18
|
+
export interface HubContext {
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A declared query: the request context in, one result out.
|
|
22
|
+
*
|
|
23
|
+
* A plain object is one tuple and is the common case. A query that returns
|
|
24
|
+
* many tuples says so with `rows`, below.
|
|
25
|
+
*/
|
|
26
|
+
export type Query = (ctx: HubContext) => HubResult | Promise<HubResult>;
|
|
27
|
+
/**
|
|
28
|
+
* The entire set, and therefore also the order. A list widget reconciles to
|
|
29
|
+
* exactly this: a row whose key is not here is gone.
|
|
30
|
+
*/
|
|
31
|
+
export declare function rows(list: QueryResult[]): Projection;
|
|
32
|
+
/**
|
|
33
|
+
* Only what changed. Rows named here arrive or are updated, keys in `drop`
|
|
34
|
+
* are gone, and everything unnamed is left alone — its contents, and its
|
|
35
|
+
* place in whatever order the widget is keeping.
|
|
36
|
+
*/
|
|
37
|
+
export declare function patch(change: {
|
|
38
|
+
rows?: QueryResult[];
|
|
39
|
+
drop?: string[];
|
|
40
|
+
}): Projection;
|
|
41
|
+
/** The shape of a `<name>.queries.ts` module. */
|
|
42
|
+
export type Queries = Record<string, Query>;
|
|
43
|
+
/**
|
|
44
|
+
* Where the interaction happened, in the addressing vocabulary, plus the one
|
|
45
|
+
* value a control may carry.
|
|
46
|
+
*
|
|
47
|
+
* The browser fills these from attributes it already has. It never names a
|
|
48
|
+
* function — only a name the page declared — which is what keeps this from
|
|
49
|
+
* being an RPC endpoint. A value may ride along because a `<select>` has one
|
|
50
|
+
* and there is nowhere else to put it; it is a string from a control, not an
|
|
51
|
+
* argument list.
|
|
52
|
+
*/
|
|
53
|
+
export interface Where {
|
|
54
|
+
query?: string;
|
|
55
|
+
key?: string;
|
|
56
|
+
cell?: string;
|
|
57
|
+
value?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* What an action does, and which queries must re-run once it has.
|
|
61
|
+
*
|
|
62
|
+
* `run` may also return results of its own, which are laid over the refreshed
|
|
63
|
+
* ones. That is how a patch reaches the browser: a query answers for its
|
|
64
|
+
* whole set and cannot know why it was re-run, but the action knows exactly
|
|
65
|
+
* what it changed and can say only that.
|
|
66
|
+
*/
|
|
67
|
+
export interface Action {
|
|
68
|
+
run: (ctx: HubContext, where: Where) => void | HubData | Promise<void | HubData>;
|
|
69
|
+
refresh: string[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One CRUD operation on a declared query. Same shape as `Action` — run, then
|
|
73
|
+
* refresh — but `where` carries only what that operation is typed to carry
|
|
74
|
+
* on the wire, rather than the general `Where`.
|
|
75
|
+
*/
|
|
76
|
+
export interface CrudOp<W> {
|
|
77
|
+
run: (ctx: HubContext, where: W) => void | HubData | Promise<void | HubData>;
|
|
78
|
+
refresh: string[];
|
|
79
|
+
}
|
|
80
|
+
export type CellChangeOp = CrudOp<{
|
|
81
|
+
key: string;
|
|
82
|
+
cell: string;
|
|
83
|
+
value: string;
|
|
84
|
+
}>;
|
|
85
|
+
export type TupleDeleteOp = CrudOp<{
|
|
86
|
+
key: string;
|
|
87
|
+
}>;
|
|
88
|
+
export type TupleInsertOp = CrudOp<{
|
|
89
|
+
values: Record<string, string>;
|
|
90
|
+
}>;
|
|
91
|
+
export type TupleUpdateOp = CrudOp<{
|
|
92
|
+
key: string;
|
|
93
|
+
values: Record<string, string>;
|
|
94
|
+
}>;
|
|
95
|
+
/**
|
|
96
|
+
* The CRUD operations declared for one query, keyed by query name in
|
|
97
|
+
* `Hooks.crud`. A query with no entry here permits none of them — the
|
|
98
|
+
* wire cannot reach anything the page has not published, exactly as for a
|
|
99
|
+
* named action.
|
|
100
|
+
*/
|
|
101
|
+
export interface Crud {
|
|
102
|
+
cellChange?: CellChangeOp;
|
|
103
|
+
tupleDelete?: TupleDeleteOp;
|
|
104
|
+
tupleInsert?: TupleInsertOp;
|
|
105
|
+
tupleUpdate?: TupleUpdateOp;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The shape of a `<name>.hooks.ts` module.
|
|
109
|
+
*
|
|
110
|
+
* `beforeGet` runs on the page's data GET, before any query. It declares no
|
|
111
|
+
* refresh set: the page GET runs the whole query set afterward, so whatever
|
|
112
|
+
* the hook changed is already in the response.
|
|
113
|
+
*
|
|
114
|
+
* `actions` names what this page may be asked to do. A name the page did not
|
|
115
|
+
* declare is refused, so the wire cannot reach anything the page has not
|
|
116
|
+
* published.
|
|
117
|
+
*
|
|
118
|
+
* `crud` is the same rule for the typed CRUD operations, keyed by the query
|
|
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.
|
|
121
|
+
*/
|
|
122
|
+
export interface Hooks {
|
|
123
|
+
beforeGet?: (ctx: HubContext) => void | Promise<void>;
|
|
124
|
+
actions?: Record<string, Action>;
|
|
125
|
+
crud?: Record<string, Crud>;
|
|
126
|
+
}
|
|
127
|
+
/** A page is four files sharing a basename; two of them are these. */
|
|
128
|
+
export interface Page {
|
|
129
|
+
queries: Queries;
|
|
130
|
+
hooks: Hooks;
|
|
131
|
+
}
|
|
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".
|
|
135
|
+
*/
|
|
136
|
+
export type Pages = Record<string, Page>;
|
|
137
|
+
export interface Hub {
|
|
138
|
+
/** A page GET: run the page's beforeGet hook, then all of its queries. */
|
|
139
|
+
dataForPage(page: string, ctx: HubContext): Promise<HubData>;
|
|
140
|
+
/**
|
|
141
|
+
* An action: run what the page declared under that name, then the refresh
|
|
142
|
+
* set declared with it.
|
|
143
|
+
*/
|
|
144
|
+
runAction(page: string, name: string, where: Where, ctx: HubContext): Promise<HubData>;
|
|
145
|
+
/** Edit one cell: run the query's declared `cellChange`, then its refresh set. */
|
|
146
|
+
runCellChange(page: string, where: {
|
|
147
|
+
query: string;
|
|
148
|
+
key: string;
|
|
149
|
+
cell: string;
|
|
150
|
+
value: string;
|
|
151
|
+
}, ctx: HubContext): Promise<HubData>;
|
|
152
|
+
/** Drop one tuple: run the query's declared `tupleDelete`, then its refresh set. */
|
|
153
|
+
runTupleDelete(page: string, where: {
|
|
154
|
+
query: string;
|
|
155
|
+
key: string;
|
|
156
|
+
}, ctx: HubContext): Promise<HubData>;
|
|
157
|
+
/** Add one tuple: run the query's declared `tupleInsert`, then its refresh set. */
|
|
158
|
+
runTupleInsert(page: string, where: {
|
|
159
|
+
query: string;
|
|
160
|
+
values: Record<string, string>;
|
|
161
|
+
}, ctx: HubContext): Promise<HubData>;
|
|
162
|
+
/** Edit several cells at once: run the query's declared `tupleUpdate`, then its refresh set. */
|
|
163
|
+
runTupleUpdate(page: string, where: {
|
|
164
|
+
query: string;
|
|
165
|
+
key: string;
|
|
166
|
+
values: Record<string, string>;
|
|
167
|
+
}, ctx: HubContext): Promise<HubData>;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Build the engine over a set of pages. The pages are fixed at startup; the
|
|
171
|
+
* context is not, and arrives with each call.
|
|
172
|
+
*/
|
|
173
|
+
export declare function createHub(pages: Pages): Hub;
|
|
174
|
+
//# sourceMappingURL=lb-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The server-side contract and the engine that runs it.
|
|
2
|
+
//
|
|
3
|
+
// This is the counterpart to core/lb-constants.ts: that file is the
|
|
4
|
+
// vocabulary the browser writes, this one is the vocabulary the application
|
|
5
|
+
// writes. The engine below is the whole of Hub on the server. It ships no
|
|
6
|
+
// HTTP server, no router, and no data layer.
|
|
7
|
+
/**
|
|
8
|
+
* The entire set, and therefore also the order. A list widget reconciles to
|
|
9
|
+
* exactly this: a row whose key is not here is gone.
|
|
10
|
+
*/
|
|
11
|
+
export function rows(list) {
|
|
12
|
+
return { op: "rows", rows: list };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Only what changed. Rows named here arrive or are updated, keys in `drop`
|
|
16
|
+
* are gone, and everything unnamed is left alone — its contents, and its
|
|
17
|
+
* place in whatever order the widget is keeping.
|
|
18
|
+
*/
|
|
19
|
+
export function patch(change) {
|
|
20
|
+
return { op: "patch", ...change };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the engine over a set of pages. The pages are fixed at startup; the
|
|
24
|
+
* context is not, and arrives with each call.
|
|
25
|
+
*/
|
|
26
|
+
export function createHub(pages) {
|
|
27
|
+
async function run(page, names, ctx) {
|
|
28
|
+
const data = {};
|
|
29
|
+
for (const name of names) {
|
|
30
|
+
const query = pages[page]?.queries[name];
|
|
31
|
+
if (!query) {
|
|
32
|
+
console.warn(`loadbare: page '${page}' declares no query '${name}'`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
data[name] = await query(ctx);
|
|
36
|
+
}
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Shared by every operation kind: run what was declared, then its refresh
|
|
41
|
+
* set against the same context, laying what the operation itself stated
|
|
42
|
+
* over the refreshed queries — the narrower answer wins because it is the
|
|
43
|
+
* one that knows what actually changed.
|
|
44
|
+
*/
|
|
45
|
+
async function settle(page, declared, where, ctx, notFound) {
|
|
46
|
+
if (!declared) {
|
|
47
|
+
console.warn(notFound);
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
const stated = await declared.run(ctx, where);
|
|
51
|
+
return { ...(await run(page, declared.refresh, ctx)), ...(stated ?? {}) };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
async dataForPage(page, ctx) {
|
|
55
|
+
const entry = pages[page];
|
|
56
|
+
if (!entry) {
|
|
57
|
+
console.warn(`loadbare: no page '${page}'`);
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
await entry.hooks.beforeGet?.(ctx);
|
|
61
|
+
return run(page, Object.keys(entry.queries), ctx);
|
|
62
|
+
},
|
|
63
|
+
runAction(page, name, where, ctx) {
|
|
64
|
+
return settle(page, pages[page]?.hooks.actions?.[name], where, ctx, `loadbare: page '${page}' declares no action '${name}'`);
|
|
65
|
+
},
|
|
66
|
+
runCellChange(page, where, ctx) {
|
|
67
|
+
return settle(page, pages[page]?.hooks.crud?.[where.query]?.cellChange, { key: where.key, cell: where.cell, value: where.value }, ctx, `loadbare: page '${page}' declares no cell-change for query '${where.query}'`);
|
|
68
|
+
},
|
|
69
|
+
runTupleDelete(page, where, ctx) {
|
|
70
|
+
return settle(page, pages[page]?.hooks.crud?.[where.query]?.tupleDelete, { key: where.key }, ctx, `loadbare: page '${page}' declares no tuple-delete for query '${where.query}'`);
|
|
71
|
+
},
|
|
72
|
+
runTupleInsert(page, where, ctx) {
|
|
73
|
+
return settle(page, pages[page]?.hooks.crud?.[where.query]?.tupleInsert, { values: where.values }, ctx, `loadbare: page '${page}' declares no tuple-insert for query '${where.query}'`);
|
|
74
|
+
},
|
|
75
|
+
runTupleUpdate(page, where, ctx) {
|
|
76
|
+
return settle(page, pages[page]?.hooks.crud?.[where.query]?.tupleUpdate, { key: where.key, values: where.values }, ctx, `loadbare: page '${page}' declares no tuple-update for query '${where.query}'`);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assemble.test.d.ts","sourceRoot":"","sources":["../../tests/assemble.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
async function tree(files) {
|
|
15
|
+
const dir = await mkdtemp(path.join(tmpdir(), "loadbare-assemble-"));
|
|
16
|
+
for (const [name, content] of Object.entries(files)) {
|
|
17
|
+
const file = path.join(dir, name);
|
|
18
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
19
|
+
await writeFile(file, content, "utf-8");
|
|
20
|
+
}
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
describe("assemble", () => {
|
|
24
|
+
it("expands a widget written in the chrome, not only in pages", async () => {
|
|
25
|
+
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>`,
|
|
27
|
+
"lb-nav.html": `<nav aria-label="{{label}}" lb-slot></nav>`,
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
const locations = await resolveLocations({ src, out: `${src}/dist` });
|
|
31
|
+
const { html, usedTags } = await assemble(locations);
|
|
32
|
+
assert.match(html, /<nav aria-label="Site"><a href="\/a">A<\/a><\/nav>/);
|
|
33
|
+
assert.ok(usedTags.has("lb-nav"));
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await rm(src, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
it("still throws when the chrome uses an undefined widget", async () => {
|
|
40
|
+
const src = await tree({
|
|
41
|
+
"chrome.chrome.html": `<!doctype html><body><lb-nope></lb-nope></body>`,
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
const locations = await resolveLocations({ src, out: `${src}/dist` });
|
|
45
|
+
await assert.rejects(() => assemble(locations), /<lb-nope> has no definition/);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await rm(src, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elements.test.d.ts","sourceRoot":"","sources":["../../tests/elements.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,111 @@
|
|
|
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 { discoverSrc, packageInfo } from "../build/locations";
|
|
13
|
+
const FIXTURES = fileURLToPath(new URL("./fixtures/elements", import.meta.url));
|
|
14
|
+
/**
|
|
15
|
+
* The locations resolveElements needs, discovered the same way the CLI
|
|
16
|
+
* discovers them — but via discoverSrc/packageInfo directly rather than the
|
|
17
|
+
* full resolveLocations, since none of these fixtures has a chrome file and
|
|
18
|
+
* resolveElements does not need one.
|
|
19
|
+
*/
|
|
20
|
+
async function locationsFor(src) {
|
|
21
|
+
const [{ widgets, elementsCandidates }, info] = await Promise.all([
|
|
22
|
+
discoverSrc(src),
|
|
23
|
+
packageInfo(),
|
|
24
|
+
]);
|
|
25
|
+
return { widgets, elementsFile: elementsCandidates[0], ...info };
|
|
26
|
+
}
|
|
27
|
+
describe("customElementTags", () => {
|
|
28
|
+
it("collects every hyphenated tag, native elements excluded", () => {
|
|
29
|
+
const tags = customElementTags(`<div><lb-input></lb-input><app-box>hi</app-box><span></span></div>`);
|
|
30
|
+
assert.deepEqual([...tags].sort(), ["app-box", "lb-input"]);
|
|
31
|
+
});
|
|
32
|
+
it("reaches into template content", () => {
|
|
33
|
+
const tags = customElementTags(`<template><acme-widget></acme-widget></template>`);
|
|
34
|
+
assert.deepEqual([...tags], ["acme-widget"]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("loadElementManifest", () => {
|
|
38
|
+
it("returns the default export, keyed by tag", async () => {
|
|
39
|
+
const manifest = await loadElementManifest(`${FIXTURES}/manifest/elements.ts`);
|
|
40
|
+
assert.deepEqual(manifest, { "acme-widget": "@acme/widget" });
|
|
41
|
+
});
|
|
42
|
+
it("returns empty when there is no elements.ts", async () => {
|
|
43
|
+
const manifest = await loadElementManifest(`${FIXTURES}/local/elements.ts`);
|
|
44
|
+
assert.deepEqual(manifest, {});
|
|
45
|
+
});
|
|
46
|
+
it("rejects a key that is not a valid custom element name", async () => {
|
|
47
|
+
await assert.rejects(() => loadElementManifest(`${FIXTURES}/manifest-bad-tag/elements.ts`), /'acmewidget' is not a valid custom element name/);
|
|
48
|
+
});
|
|
49
|
+
it("rejects a value that is not an import specifier string", async () => {
|
|
50
|
+
await assert.rejects(() => loadElementManifest(`${FIXTURES}/manifest-bad-value/elements.ts`), /must map to an import specifier string/);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("resolveElements", () => {
|
|
54
|
+
it("resolves a built-in tag to the package's own widgets export", async () => {
|
|
55
|
+
const { resolved } = await resolveElements({
|
|
56
|
+
used: new Set(["lb-input"]),
|
|
57
|
+
...(await locationsFor(`${FIXTURES}/local`)),
|
|
58
|
+
});
|
|
59
|
+
assert.deepEqual(resolved.get("lb-input"), {
|
|
60
|
+
kind: "specifier",
|
|
61
|
+
value: "@loadbare/app/widgets/lb-input",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
it("resolves lb-hub the same way, by its script in hub/ rather than widgets/", async () => {
|
|
65
|
+
const { resolved } = await resolveElements({
|
|
66
|
+
used: new Set(["lb-hub"]),
|
|
67
|
+
...(await locationsFor(`${FIXTURES}/local`)),
|
|
68
|
+
});
|
|
69
|
+
assert.deepEqual(resolved.get("lb-hub"), {
|
|
70
|
+
kind: "specifier",
|
|
71
|
+
value: "@loadbare/app",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
it("resolves a local tag to its file in <src>/widgets", async () => {
|
|
75
|
+
const { resolved } = await resolveElements({
|
|
76
|
+
used: new Set(["app-box"]),
|
|
77
|
+
...(await locationsFor(`${FIXTURES}/local`)),
|
|
78
|
+
});
|
|
79
|
+
assert.equal(resolved.get("app-box")?.kind, "file");
|
|
80
|
+
assert.match(resolved.get("app-box").value, /local\/widgets\/app-box$/);
|
|
81
|
+
});
|
|
82
|
+
it("resolves a tag declared only in elements.ts", async () => {
|
|
83
|
+
const { resolved } = await resolveElements({
|
|
84
|
+
used: new Set(["acme-widget"]),
|
|
85
|
+
...(await locationsFor(`${FIXTURES}/manifest`)),
|
|
86
|
+
});
|
|
87
|
+
assert.deepEqual(resolved.get("acme-widget"), {
|
|
88
|
+
kind: "specifier",
|
|
89
|
+
value: "@acme/widget",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it("needs no script for a tag with only an .html definition", async () => {
|
|
93
|
+
const { resolved } = await resolveElements({
|
|
94
|
+
used: new Set(["lb-nav"]),
|
|
95
|
+
...(await locationsFor(`${FIXTURES}/markup-only`)),
|
|
96
|
+
});
|
|
97
|
+
assert.equal(resolved.size, 0);
|
|
98
|
+
});
|
|
99
|
+
it("throws when a used tag registers nothing", async () => {
|
|
100
|
+
await assert.rejects(async () => resolveElements({
|
|
101
|
+
used: new Set(["nowhere-widget"]),
|
|
102
|
+
...(await locationsFor(`${FIXTURES}/local`)),
|
|
103
|
+
}), /<nowhere-widget> has neither a script nor a definition/);
|
|
104
|
+
});
|
|
105
|
+
it("throws when a used tag is declared in more than one place", async () => {
|
|
106
|
+
await assert.rejects(async () => resolveElements({
|
|
107
|
+
used: new Set(["acme-widget"]),
|
|
108
|
+
...(await locationsFor(`${FIXTURES}/collision`)),
|
|
109
|
+
}), /<acme-widget> is declared in more than one place: src, .*elements\.ts/);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expansion — see docs/expansion.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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand.test.d.ts","sourceRoot":"","sources":["../../tests/expand.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|