@loadbare/app 0.5.5 → 0.6.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/README.md +3 -4
- package/dist/build/assemble.d.ts +1 -1
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +80 -7
- package/dist/build/cli.d.ts +2 -2
- package/dist/build/cli.js +2 -2
- package/dist/build/expand.d.ts.map +1 -1
- package/dist/build/expand.js +30 -34
- package/dist/build/locations.d.ts +3 -3
- package/dist/build/locations.d.ts.map +1 -1
- package/dist/build/locations.js +15 -3
- package/dist/build/origins.d.ts +0 -13
- package/dist/build/origins.d.ts.map +1 -1
- package/dist/build/origins.js +32 -8
- package/dist/build/pages.d.ts +3 -3
- package/dist/build/pages.d.ts.map +1 -1
- package/dist/build/pages.js +9 -7
- package/dist/core/lb-constants.d.ts +17 -12
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +106 -43
- package/dist/core/lb-types.d.ts +103 -62
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +11 -3
- package/dist/hub/lb-apply.d.ts +18 -4
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +184 -34
- 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 +165 -83
- package/dist/server/lb-express.d.ts +7 -4
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +45 -33
- package/dist/server/lb-server.d.ts +67 -45
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +55 -18
- package/dist/tests/assemble.test.js +154 -2
- package/dist/tests/expand.test.js +30 -3
- package/dist/tests/helpers/hub.d.ts +73 -0
- package/dist/tests/helpers/hub.d.ts.map +1 -0
- package/dist/tests/helpers/hub.js +151 -0
- package/dist/tests/lb-apply.test.js +86 -62
- package/dist/tests/lb-express.test.d.ts +1 -1
- package/dist/tests/lb-express.test.js +43 -38
- package/dist/tests/lb-hub.test.d.ts +14 -0
- package/dist/tests/lb-hub.test.d.ts.map +1 -0
- package/dist/tests/lb-hub.test.js +319 -0
- package/dist/tests/{lb-rows.test.d.ts → lb-list.test.d.ts} +1 -1
- package/dist/tests/lb-list.test.d.ts.map +1 -0
- package/dist/tests/{lb-rows.test.js → lb-list.test.js} +109 -106
- package/dist/tests/lb-server.test.js +151 -100
- package/dist/tests/origins.test.js +19 -1
- package/dist/tests/pages.test.d.ts +1 -1
- package/dist/tests/pages.test.js +64 -14
- package/docs/TECHREF-1.0.md +1000 -0
- package/docs/reference/builder.md +4 -4
- package/docs/reference/chrome.md +56 -5
- package/docs/reference/custom-elements.md +59 -36
- package/docs/reference/data-binding.md +142 -84
- package/docs/reference/overview.md +1 -1
- package/docs/reference/page-files.md +64 -49
- package/docs/reference/server.md +7 -6
- package/docs/reference/widgets.md +43 -32
- package/docs/roadmap.md +68 -22
- package/docs/testing.md +47 -17
- package/docs/theory.md +2 -2
- package/docs/tutorials/010-pages-and-navigation.md +14 -8
- package/docs/tutorials/040-displaying-data.md +9 -9
- package/docs/tutorials/050-actions.md +5 -5
- package/docs/tutorials/060-custom-element-code.md +1 -1
- package/docs/tutorials/065-conditional-rendering.md +4 -4
- package/docs/tutorials/070-displaying-a-list.md +24 -47
- package/docs/tutorials/072-inserting-into-a-list.md +17 -17
- package/docs/tutorials/074-deleting-from-a-list.md +15 -17
- package/docs/tutorials/076-updating-a-list-item.md +20 -22
- package/docs/tutorials/080-widget-requests.md +27 -23
- package/docs/tutorials/090-using-widget-libraries.md +23 -1
- package/package.json +1 -2
- package/dist/hub/lb-rows.d.ts +0 -18
- package/dist/hub/lb-rows.d.ts.map +0 -1
- package/dist/hub/lb-rows.js +0 -106
- package/dist/tests/lb-rows.test.d.ts.map +0 -1
|
@@ -6,70 +6,82 @@
|
|
|
6
6
|
* Express itself is a peer dependency: nothing outside this file imports it,
|
|
7
7
|
* so an application that hosts Hub some other way never pays for it.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* One endpoint, and no second:
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* POST /lb?page=<name> an empty body asks for the page's whole query
|
|
12
|
+
* set; a body carrying an action runs that action
|
|
13
|
+
* and answers with its refresh set
|
|
13
14
|
*
|
|
14
15
|
* The page rides on the query string rather than in the body, which is what
|
|
15
|
-
* lets the operation set stay closed.
|
|
16
|
+
* lets the operation set stay closed. Every hub call is a POST, so an
|
|
17
|
+
* application's own GET routes never collide with this one and no page name
|
|
18
|
+
* is reserved.
|
|
16
19
|
*/
|
|
17
20
|
import express from "express";
|
|
18
|
-
import {
|
|
21
|
+
import { ACTION_CELL_CHANGE, ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, LB_ENDPOINT, } from "../core/lb-constants.js";
|
|
22
|
+
import { isOperation } from "../core/lb-types.js";
|
|
19
23
|
export function hubRoutes(hub, contextFor) {
|
|
20
24
|
const router = express.Router();
|
|
21
25
|
// Mounted here rather than on the app so that Hub's need for a parsed
|
|
22
26
|
// body is stated where the routes that need it are.
|
|
23
27
|
router.use(express.json());
|
|
24
|
-
router.
|
|
28
|
+
router.post(LB_ENDPOINT, async (req, res) => {
|
|
25
29
|
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
30
|
const request = req.body;
|
|
34
31
|
const ctx = contextFor(req);
|
|
32
|
+
// No body at all is the page load: the browser is asking for this
|
|
33
|
+
// page's whole query set, which is every request it makes that names no
|
|
34
|
+
// action. A body that carries something but not an action is malformed.
|
|
35
|
+
if (request === undefined || Object.keys(request).length === 0) {
|
|
36
|
+
res.json(await hub.dataForPage(page, ctx));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!request.action) {
|
|
40
|
+
console.warn(`loadbare: request without an action`);
|
|
41
|
+
res.status(400).json({});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
35
44
|
// A thrown error is how application code (a query, an action, a CRUD
|
|
36
45
|
// op) says "this request is invalid" or hits an unexpected failure —
|
|
37
46
|
// it resolves to a real response rather than an unhandled rejection, so
|
|
38
47
|
// the browser side has something to catch.
|
|
39
48
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
// The reserved prefix is the whole of the discriminant: a name so
|
|
50
|
+
// prefixed is one of the four operations, and anything else is a name
|
|
51
|
+
// the page declared. Nothing else tells them apart, here or anywhere.
|
|
52
|
+
if (!isOperation(request)) {
|
|
53
|
+
const { action, list, row, key, cell, value } = request;
|
|
54
|
+
res.json(await hub.runAction(page, action, { list, row, key, cell, value }, ctx));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
switch (request.action) {
|
|
58
|
+
case ACTION_CELL_CHANGE: {
|
|
59
|
+
const { list, key, cell, value } = request;
|
|
60
|
+
res.json(await hub.runCellChange(page, { list, key, cell, value }, ctx));
|
|
49
61
|
return;
|
|
50
62
|
}
|
|
51
|
-
case
|
|
52
|
-
const {
|
|
53
|
-
res.json(await hub.
|
|
63
|
+
case ACTION_ROW_DELETE: {
|
|
64
|
+
const { list, key } = request;
|
|
65
|
+
res.json(await hub.runRowDelete(page, { list, key }, ctx));
|
|
54
66
|
return;
|
|
55
67
|
}
|
|
56
|
-
case
|
|
57
|
-
const {
|
|
58
|
-
res.json(await hub.
|
|
68
|
+
case ACTION_ROW_INSERT: {
|
|
69
|
+
const { list, values } = request;
|
|
70
|
+
res.json(await hub.runRowInsert(page, { list, values }, ctx));
|
|
59
71
|
return;
|
|
60
72
|
}
|
|
61
|
-
case
|
|
62
|
-
const {
|
|
63
|
-
res.json(await hub.
|
|
73
|
+
case ACTION_ROW_UPDATE: {
|
|
74
|
+
const { list, key, values } = request;
|
|
75
|
+
res.json(await hub.runRowUpdate(page, { list, key, values }, ctx));
|
|
64
76
|
return;
|
|
65
77
|
}
|
|
66
78
|
default:
|
|
67
|
-
console.warn(`loadbare: operation '${
|
|
79
|
+
console.warn(`loadbare: operation '${request.action}' not implemented`);
|
|
68
80
|
res.status(400).json({});
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
catch (err) {
|
|
72
|
-
console.error(`loadbare:
|
|
84
|
+
console.error(`loadbare: '${request.action}' threw`, err);
|
|
73
85
|
res.status(500).json({});
|
|
74
86
|
}
|
|
75
87
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Patch, Row, HubData, HubResult } from "../core/lb-types";
|
|
2
2
|
/**
|
|
3
3
|
* Whatever the application hands the engine for the duration of one request.
|
|
4
4
|
*
|
|
@@ -20,24 +20,40 @@ export interface HubContext {
|
|
|
20
20
|
/**
|
|
21
21
|
* A declared query: the request context in, one result out.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Cardinality is a property of the name rather than of any one answer, so it
|
|
24
|
+
* is declared here and never inferred from what comes back. One name answers
|
|
25
|
+
* with one shape, always. A page that wants the roster once as a single row
|
|
26
|
+
* and once as a set declares two queries.
|
|
27
|
+
*
|
|
28
|
+
* Write one with `row()` or `list()` below; nothing else builds one.
|
|
25
29
|
*/
|
|
26
|
-
export
|
|
30
|
+
export interface Query {
|
|
31
|
+
readonly kind: "row" | "list";
|
|
32
|
+
readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;
|
|
33
|
+
}
|
|
34
|
+
/** A query that answers with one row. */
|
|
35
|
+
export declare function row(run: (ctx: HubContext) => Row | Promise<Row>): Query;
|
|
27
36
|
/**
|
|
28
|
-
*
|
|
29
|
-
* exactly this: a row whose key is not here is gone.
|
|
37
|
+
* A query that answers with the entire set, and therefore also the order. A
|
|
38
|
+
* list reconciles to exactly this: a row whose key is not here is gone.
|
|
39
|
+
*
|
|
40
|
+
* There is no wrapper around the array, because the declaration already said
|
|
41
|
+
* this name answers with rows.
|
|
30
42
|
*/
|
|
31
|
-
export declare function
|
|
43
|
+
export declare function list(run: (ctx: HubContext) => Row[] | Promise<Row[]>): Query;
|
|
32
44
|
/**
|
|
33
|
-
* Only what changed
|
|
34
|
-
*
|
|
35
|
-
*
|
|
45
|
+
* Only what changed, returned from a `crud` run rather than from a query.
|
|
46
|
+
* Rows named here arrive or are updated, keys in `drop` are gone, and
|
|
47
|
+
* everything unnamed is left alone — its contents, and its place in whatever
|
|
48
|
+
* order the widget is keeping.
|
|
49
|
+
*
|
|
50
|
+
* `Array.isArray` is what tells a patch from a whole set, so a patch needs no
|
|
51
|
+
* marker of its own and no column name is reserved to carry one.
|
|
36
52
|
*/
|
|
37
53
|
export declare function patch(change: {
|
|
38
|
-
rows?:
|
|
54
|
+
rows?: Row[];
|
|
39
55
|
drop?: string[];
|
|
40
|
-
}):
|
|
56
|
+
}): Patch;
|
|
41
57
|
/** The shape of a `<name>.queries.ts` module. */
|
|
42
58
|
export type Queries = Record<string, Query>;
|
|
43
59
|
/**
|
|
@@ -51,7 +67,8 @@ export type Queries = Record<string, Query>;
|
|
|
51
67
|
* argument list.
|
|
52
68
|
*/
|
|
53
69
|
export interface Where {
|
|
54
|
-
|
|
70
|
+
list?: string;
|
|
71
|
+
row?: string;
|
|
55
72
|
key?: string;
|
|
56
73
|
cell?: string;
|
|
57
74
|
value?: string;
|
|
@@ -82,87 +99,92 @@ export type CellChangeOp = CrudOp<{
|
|
|
82
99
|
cell: string;
|
|
83
100
|
value: string;
|
|
84
101
|
}>;
|
|
85
|
-
export type
|
|
102
|
+
export type RowDeleteOp = CrudOp<{
|
|
86
103
|
key: string;
|
|
87
104
|
}>;
|
|
88
|
-
export type
|
|
105
|
+
export type RowInsertOp = CrudOp<{
|
|
89
106
|
values: Record<string, string>;
|
|
90
107
|
}>;
|
|
91
|
-
export type
|
|
108
|
+
export type RowUpdateOp = CrudOp<{
|
|
92
109
|
key: string;
|
|
93
110
|
values: Record<string, string>;
|
|
94
111
|
}>;
|
|
95
112
|
/**
|
|
96
|
-
* The CRUD operations declared for one
|
|
97
|
-
* `
|
|
98
|
-
*
|
|
99
|
-
*
|
|
113
|
+
* The CRUD operations declared for one list, keyed by its name in
|
|
114
|
+
* `Requests.crud`. A name with no entry here permits none of them — the wire
|
|
115
|
+
* cannot reach anything the page has not published, exactly as for a named
|
|
116
|
+
* action.
|
|
117
|
+
*
|
|
118
|
+
* Every key here is the reserved `lb-action` value with the prefix stripped
|
|
119
|
+
* and the rest camel-cased, so the attribute, the wire field and this key
|
|
120
|
+
* are one vocabulary. All four are list operations: each needs a key, and a
|
|
121
|
+
* key exists only on a live row inside a list.
|
|
100
122
|
*/
|
|
101
123
|
export interface Crud {
|
|
102
124
|
cellChange?: CellChangeOp;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
rowDelete?: RowDeleteOp;
|
|
126
|
+
rowInsert?: RowInsertOp;
|
|
127
|
+
rowUpdate?: RowUpdateOp;
|
|
106
128
|
}
|
|
107
129
|
/**
|
|
108
|
-
* The shape of a `<name>.
|
|
130
|
+
* The shape of a `<name>.requests.ts` module.
|
|
109
131
|
*
|
|
110
|
-
* `
|
|
111
|
-
* refresh set: the page
|
|
112
|
-
* the hook changed is already in the response.
|
|
132
|
+
* `onPageEnter` runs once when the page is entered, before any query. It
|
|
133
|
+
* declares no refresh set: entering the page runs the whole query set
|
|
134
|
+
* afterward, so whatever the hook changed is already in the response.
|
|
113
135
|
*
|
|
114
136
|
* `actions` names what this page may be asked to do. A name the page did not
|
|
115
137
|
* declare is refused, so the wire cannot reach anything the page has not
|
|
116
138
|
* published.
|
|
117
139
|
*
|
|
118
|
-
* `crud` is the same rule for the typed CRUD operations, keyed by the
|
|
140
|
+
* `crud` is the same rule for the typed CRUD operations, keyed by the list
|
|
119
141
|
* they operate on rather than by a declared name — there is nothing to name,
|
|
120
142
|
* since the row's own binding says what it is.
|
|
121
143
|
*/
|
|
122
|
-
export interface
|
|
123
|
-
|
|
144
|
+
export interface Requests {
|
|
145
|
+
onPageEnter?: (ctx: HubContext) => void | Promise<void>;
|
|
124
146
|
actions?: Record<string, Action>;
|
|
125
147
|
crud?: Record<string, Crud>;
|
|
126
148
|
}
|
|
127
|
-
/** A page is
|
|
149
|
+
/** A page is three files sharing a basename; two of them are these. */
|
|
128
150
|
export interface Page {
|
|
129
151
|
queries: Queries;
|
|
130
|
-
|
|
152
|
+
requests: Requests;
|
|
131
153
|
}
|
|
132
154
|
/**
|
|
133
155
|
* The page registry. The builder populates this automatically from every
|
|
134
|
-
* `.
|
|
156
|
+
* `.requests.ts`/`.queries.ts` pair it discovers — see docs/reference/builder.md,
|
|
135
157
|
* "Generating the server-side page registry".
|
|
136
158
|
*/
|
|
137
159
|
export type Pages = Record<string, Page>;
|
|
138
160
|
export interface Hub {
|
|
139
|
-
/**
|
|
161
|
+
/** Entering a page: run its onPageEnter hook, then all of its queries. */
|
|
140
162
|
dataForPage(page: string, ctx: HubContext): Promise<HubData>;
|
|
141
163
|
/**
|
|
142
164
|
* An action: run what the page declared under that name, then the refresh
|
|
143
165
|
* set declared with it.
|
|
144
166
|
*/
|
|
145
167
|
runAction(page: string, name: string, where: Where, ctx: HubContext): Promise<HubData>;
|
|
146
|
-
/** Edit one cell: run the
|
|
168
|
+
/** Edit one cell: run the list's declared `cellChange`, then its refresh set. */
|
|
147
169
|
runCellChange(page: string, where: {
|
|
148
|
-
|
|
170
|
+
list: string;
|
|
149
171
|
key: string;
|
|
150
172
|
cell: string;
|
|
151
173
|
value: string;
|
|
152
174
|
}, ctx: HubContext): Promise<HubData>;
|
|
153
|
-
/** Drop one
|
|
154
|
-
|
|
155
|
-
|
|
175
|
+
/** Drop one row: run the list's declared `rowDelete`, then its refresh set. */
|
|
176
|
+
runRowDelete(page: string, where: {
|
|
177
|
+
list: string;
|
|
156
178
|
key: string;
|
|
157
179
|
}, ctx: HubContext): Promise<HubData>;
|
|
158
|
-
/** Add one
|
|
159
|
-
|
|
160
|
-
|
|
180
|
+
/** Add one row: run the list's declared `rowInsert`, then its refresh set. */
|
|
181
|
+
runRowInsert(page: string, where: {
|
|
182
|
+
list: string;
|
|
161
183
|
values: Record<string, string>;
|
|
162
184
|
}, ctx: HubContext): Promise<HubData>;
|
|
163
|
-
/** Edit several cells at once: run the
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
/** Edit several cells at once: run the list's declared `rowUpdate`, then its refresh set. */
|
|
186
|
+
runRowUpdate(page: string, where: {
|
|
187
|
+
list: string;
|
|
166
188
|
key: string;
|
|
167
189
|
values: Record<string, string>;
|
|
168
190
|
}, ctx: HubContext): Promise<HubData>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
|
package/dist/server/lb-server.js
CHANGED
|
@@ -4,26 +4,54 @@
|
|
|
4
4
|
// vocabulary the browser writes, this one is the vocabulary the application
|
|
5
5
|
// writes. The engine below is the whole of Hub on the server. It ships no
|
|
6
6
|
// HTTP server, no router, and no data layer.
|
|
7
|
+
import { LB_RESERVED_PREFIX } from "../core/lb-constants";
|
|
8
|
+
/** A query that answers with one row. */
|
|
9
|
+
export function row(run) {
|
|
10
|
+
return { kind: "row", run };
|
|
11
|
+
}
|
|
7
12
|
/**
|
|
8
|
-
*
|
|
9
|
-
* exactly this: a row whose key is not here is gone.
|
|
13
|
+
* A query that answers with the entire set, and therefore also the order. A
|
|
14
|
+
* list reconciles to exactly this: a row whose key is not here is gone.
|
|
15
|
+
*
|
|
16
|
+
* There is no wrapper around the array, because the declaration already said
|
|
17
|
+
* this name answers with rows.
|
|
10
18
|
*/
|
|
11
|
-
export function
|
|
12
|
-
return {
|
|
19
|
+
export function list(run) {
|
|
20
|
+
return { kind: "list", run };
|
|
13
21
|
}
|
|
14
22
|
/**
|
|
15
|
-
* Only what changed
|
|
16
|
-
*
|
|
17
|
-
*
|
|
23
|
+
* Only what changed, returned from a `crud` run rather than from a query.
|
|
24
|
+
* Rows named here arrive or are updated, keys in `drop` are gone, and
|
|
25
|
+
* everything unnamed is left alone — its contents, and its place in whatever
|
|
26
|
+
* order the widget is keeping.
|
|
27
|
+
*
|
|
28
|
+
* `Array.isArray` is what tells a patch from a whole set, so a patch needs no
|
|
29
|
+
* marker of its own and no column name is reserved to carry one.
|
|
18
30
|
*/
|
|
19
31
|
export function patch(change) {
|
|
20
|
-
return {
|
|
32
|
+
return { ...change };
|
|
21
33
|
}
|
|
22
34
|
/**
|
|
23
35
|
* Build the engine over a set of pages. The pages are fixed at startup; the
|
|
24
36
|
* context is not, and arrives with each call.
|
|
25
37
|
*/
|
|
26
38
|
export function createHub(pages) {
|
|
39
|
+
// Names beginning with the reserved prefix are Loadbare's — the hub's
|
|
40
|
+
// own query, the CRUD operations — so an application cannot declare one.
|
|
41
|
+
// Refused at startup, because a name is a fact about the page and not
|
|
42
|
+
// about any request.
|
|
43
|
+
for (const [page, entry] of Object.entries(pages)) {
|
|
44
|
+
const declared = [
|
|
45
|
+
...Object.keys(entry.queries),
|
|
46
|
+
...Object.keys(entry.requests.actions ?? {}),
|
|
47
|
+
];
|
|
48
|
+
for (const name of declared) {
|
|
49
|
+
if (name.startsWith(LB_RESERVED_PREFIX)) {
|
|
50
|
+
throw new Error(`loadbare: page '${page}' declares '${name}', but names ` +
|
|
51
|
+
`beginning with '${LB_RESERVED_PREFIX}' are reserved`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
27
55
|
async function run(page, names, ctx) {
|
|
28
56
|
const data = {};
|
|
29
57
|
for (const name of names) {
|
|
@@ -32,7 +60,16 @@ export function createHub(pages) {
|
|
|
32
60
|
console.warn(`loadbare: page '${page}' declares no query '${name}'`);
|
|
33
61
|
continue;
|
|
34
62
|
}
|
|
35
|
-
|
|
63
|
+
const result = await query.run(ctx);
|
|
64
|
+
// Cardinality is declared, so an answer that disagrees is a mistake in
|
|
65
|
+
// the query rather than a case to handle. Refused here, because a page
|
|
66
|
+
// is better off missing one name than showing the wrong shape for it.
|
|
67
|
+
if (Array.isArray(result) !== (query.kind === "list")) {
|
|
68
|
+
console.warn(`loadbare: query '${name}' is declared ${query.kind} but answered ` +
|
|
69
|
+
`with ${Array.isArray(result) ? "rows" : "one row"}`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
data[name] = result;
|
|
36
73
|
}
|
|
37
74
|
return data;
|
|
38
75
|
}
|
|
@@ -57,23 +94,23 @@ export function createHub(pages) {
|
|
|
57
94
|
console.warn(`loadbare: no page '${page}'`);
|
|
58
95
|
return {};
|
|
59
96
|
}
|
|
60
|
-
await entry.
|
|
97
|
+
await entry.requests.onPageEnter?.(ctx);
|
|
61
98
|
return run(page, Object.keys(entry.queries), ctx);
|
|
62
99
|
},
|
|
63
100
|
runAction(page, name, where, ctx) {
|
|
64
|
-
return settle(page, pages[page]?.
|
|
101
|
+
return settle(page, pages[page]?.requests.actions?.[name], where, ctx, `loadbare: page '${page}' declares no action '${name}'`);
|
|
65
102
|
},
|
|
66
103
|
runCellChange(page, where, ctx) {
|
|
67
|
-
return settle(page, pages[page]?.
|
|
104
|
+
return settle(page, pages[page]?.requests.crud?.[where.list]?.cellChange, { key: where.key, cell: where.cell, value: where.value }, ctx, `loadbare: page '${page}' declares no cellChange for '${where.list}'`);
|
|
68
105
|
},
|
|
69
|
-
|
|
70
|
-
return settle(page, pages[page]?.
|
|
106
|
+
runRowDelete(page, where, ctx) {
|
|
107
|
+
return settle(page, pages[page]?.requests.crud?.[where.list]?.rowDelete, { key: where.key }, ctx, `loadbare: page '${page}' declares no rowDelete for '${where.list}'`);
|
|
71
108
|
},
|
|
72
|
-
|
|
73
|
-
return settle(page, pages[page]?.
|
|
109
|
+
runRowInsert(page, where, ctx) {
|
|
110
|
+
return settle(page, pages[page]?.requests.crud?.[where.list]?.rowInsert, { values: where.values }, ctx, `loadbare: page '${page}' declares no rowInsert for '${where.list}'`);
|
|
74
111
|
},
|
|
75
|
-
|
|
76
|
-
return settle(page, pages[page]?.
|
|
112
|
+
runRowUpdate(page, where, ctx) {
|
|
113
|
+
return settle(page, pages[page]?.requests.crud?.[where.list]?.rowUpdate, { key: where.key, values: where.values }, ctx, `loadbare: page '${page}' declares no rowUpdate for '${where.list}'`);
|
|
77
114
|
},
|
|
78
115
|
};
|
|
79
116
|
}
|
|
@@ -43,16 +43,168 @@ describe("assemble", () => {
|
|
|
43
43
|
await rm(src, { recursive: true, force: true });
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
|
-
it("
|
|
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.
|
|
47
49
|
const src = await tree({
|
|
48
50
|
"chrome.html": `<!doctype html><body><lb-nope></lb-nope></body>`,
|
|
49
51
|
});
|
|
50
52
|
try {
|
|
51
53
|
const locations = await resolveLocations({ src, out: `${src}/dist` });
|
|
52
|
-
|
|
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>/);
|
|
53
104
|
}
|
|
54
105
|
finally {
|
|
55
106
|
await rm(src, { recursive: true, force: true });
|
|
56
107
|
}
|
|
57
108
|
});
|
|
58
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
|
+
});
|