@loadbare/app 0.5.1 → 0.5.2
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 -1
- package/dist/build/elements.d.ts +4 -1
- package/dist/build/elements.d.ts.map +1 -1
- package/dist/build/elements.js +17 -1
- package/dist/build/origins.d.ts +29 -9
- package/dist/build/origins.d.ts.map +1 -1
- package/dist/build/origins.js +50 -18
- package/dist/hub/{lb-hub.d.ts → lb-hub.browser.d.ts} +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -0
- package/dist/tests/elements.test.js +14 -3
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +2 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +2 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +2 -0
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +6 -0
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +1 -0
- package/dist/tests/origins.test.js +56 -13
- package/docs/reference/builder.md +7 -1
- package/docs/reference/custom-elements.md +14 -8
- package/docs/testing.md +1 -1
- package/docs/tutorials/060-custom-element-code.md +11 -4
- package/docs/tutorials/065-conditional-rendering.md +1 -1
- package/docs/tutorials/080-widget-requests.md +2 -2
- package/package.json +2 -2
- package/dist/hub/lb-hub.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts +0 -2
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts +0 -2
- package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts +0 -2
- package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts.map +0 -1
- /package/dist/hub/{lb-hub.js → lb-hub.browser.js} +0 -0
- /package/dist/tests/fixtures/elements/collision/widgets/{acme-widget.js → acme-widget.browser.js} +0 -0
- /package/dist/tests/fixtures/elements/local/widgets/{app-box.js → app-box.browser.js} +0 -0
- /package/dist/tests/fixtures/elements/pkg/{acme-widget.js → acme-widget.browser.js} +0 -0
package/dist/build/assemble.js
CHANGED
|
@@ -22,7 +22,7 @@ import { addDefinitions, customElementTags, expand, } from "./expand.js";
|
|
|
22
22
|
import { formatHtml } from "./format.js";
|
|
23
23
|
import { ATTR_UNKNOWN_PAGE, PAGE_TEMPLATE_PREFIX, } from "../core/lb-constants.js";
|
|
24
24
|
/**
|
|
25
|
-
* The hub calls showModal() on whatever carries lb-unknown-page (hub/lb-hub.ts),
|
|
25
|
+
* The hub calls showModal() on whatever carries lb-unknown-page (hub/lb-hub.browser.ts),
|
|
26
26
|
* so a chrome that puts the attribute on anything else would fail at
|
|
27
27
|
* runtime, on whichever page happens to 404 first, rather than at build
|
|
28
28
|
* time. The attribute is optional; only a wrong element on it is an error.
|
package/dist/build/elements.d.ts
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* survives as an inert wrapper (see expand.ts).
|
|
11
11
|
*
|
|
12
12
|
* A tag with neither — no script anywhere and no definition — would do
|
|
13
|
-
* nothing were it ever to reach the browser, so that is an error.
|
|
13
|
+
* nothing were it ever to reach the browser, so that is an error. When a
|
|
14
|
+
* module named after the tag exists but never said `.browser`, the error
|
|
15
|
+
* names that file: forgetting the word is the ordinary way to arrive here,
|
|
16
|
+
* and it is the one an author can fix in a rename.
|
|
14
17
|
*
|
|
15
18
|
* Both halves are looked for in the same ordered origins, and the later
|
|
16
19
|
* origin wins: an application's own script overrides one from a package it
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../build/elements.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../build/elements.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,mBAAmB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAEzE,8FAA8F;AAC9F,MAAM,MAAM,QAAQ,GAClB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,8EAA8E;IAC9E,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CACjC;AAED,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,KAAK,GACN,EAAE,cAAc,GAAG,aAAa,CA+ChC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,EAC/B,MAAM,EAAE,MAAM,GACb,MAAM,CAcR"}
|
package/dist/build/elements.js
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* survives as an inert wrapper (see expand.ts).
|
|
11
11
|
*
|
|
12
12
|
* A tag with neither — no script anywhere and no definition — would do
|
|
13
|
-
* nothing were it ever to reach the browser, so that is an error.
|
|
13
|
+
* nothing were it ever to reach the browser, so that is an error. When a
|
|
14
|
+
* module named after the tag exists but never said `.browser`, the error
|
|
15
|
+
* names that file: forgetting the word is the ordinary way to arrive here,
|
|
16
|
+
* and it is the one an author can fix in a rename.
|
|
14
17
|
*
|
|
15
18
|
* Both halves are looked for in the same ordered origins, and the later
|
|
16
19
|
* origin wins: an application's own script overrides one from a package it
|
|
@@ -44,6 +47,19 @@ export function resolveElements({ used, scans, }) {
|
|
|
44
47
|
// content — otherwise nothing, ever, would upgrade or fill it, and the
|
|
45
48
|
// tag would do nothing were it to reach the browser.
|
|
46
49
|
if (!hasDefinition) {
|
|
50
|
+
// The likeliest reason, checked first: a module named after the tag is
|
|
51
|
+
// sitting right there without the word that would ship it. Say which
|
|
52
|
+
// file, because "there is no script" reads as a lie to someone looking
|
|
53
|
+
// straight at one.
|
|
54
|
+
for (const scan of scans) {
|
|
55
|
+
const near = scan.unmarked.get(tag);
|
|
56
|
+
if (near === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
throw new Error(`build: <${tag}> has no script, but '${near}' is named after it. ` +
|
|
59
|
+
`A script reaches the browser only when its name says so — ` +
|
|
60
|
+
`rename it '${path.basename(near, path.extname(near))}.browser` +
|
|
61
|
+
`${path.extname(near)}' if that is what it is.`);
|
|
62
|
+
}
|
|
47
63
|
throw new Error(`build: <${tag}> has neither a script nor a definition, so it would ` +
|
|
48
64
|
`do nothing (checked ${scans.map((s) => `'${s.name}'`).join(", ")})`);
|
|
49
65
|
}
|
package/dist/build/origins.d.ts
CHANGED
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
* Where a build's files come from — see docs/reference/builder.md.
|
|
3
3
|
*
|
|
4
4
|
* Three kinds of file reach the assembled application: a `.html` definition
|
|
5
|
-
* that expands a tag, a `.ts` script that registers its class, and a
|
|
6
|
-
* stylesheet. All three arrive from the same places, so all three are
|
|
7
|
-
* the same way: one ordered list of origins, scanned once each, read by
|
|
5
|
+
* that expands a tag, a `.browser.ts` script that registers its class, and a
|
|
6
|
+
* `.css` stylesheet. All three arrive from the same places, so all three are
|
|
7
|
+
* found the same way: one ordered list of origins, scanned once each, read by
|
|
8
8
|
* whoever needs them.
|
|
9
9
|
*
|
|
10
|
+
* A script says `.browser` because shipping is the thing worth being sure
|
|
11
|
+
* about. Under a plain `lb-thing.ts` rule a filename alone decides what
|
|
12
|
+
* crosses to the browser, so a module named after a tag someone else writes
|
|
13
|
+
* later is bundled without anyone having chosen that — and the author who
|
|
14
|
+
* would most like to know is the one who never considered it. Requiring the
|
|
15
|
+
* word puts the decision where a reader can see it, and leaves every
|
|
16
|
+
* unmarked module in a source tree exactly what it looks like: not browser
|
|
17
|
+
* code. It costs a widget author one word, once.
|
|
18
|
+
*
|
|
10
19
|
* The order is the cascade, and it is the same for every file kind:
|
|
11
20
|
*
|
|
12
21
|
* 1. this package's own `hub/`
|
|
@@ -50,19 +59,30 @@ export interface OriginScan {
|
|
|
50
59
|
specifier?: (tag: string) => string;
|
|
51
60
|
/** Tag to its `.html` definition. */
|
|
52
61
|
definitions: Map<string, string>;
|
|
53
|
-
/** Tag to its `.ts` script,
|
|
62
|
+
/** Tag to its `.browser.ts` script, the trailing `.ts` stripped. */
|
|
54
63
|
scripts: Map<string, string>;
|
|
64
|
+
/**
|
|
65
|
+
* Tag to the unmarked module that would have claimed it — `lb-thing.ts`
|
|
66
|
+
* where `lb-thing.browser.ts` was meant. Nothing is built from this. It is
|
|
67
|
+
* here so build/elements.ts can name the file when the tag turns out to
|
|
68
|
+
* have nothing to register it.
|
|
69
|
+
*/
|
|
70
|
+
unmarked: Map<string, string>;
|
|
55
71
|
/** Every `.css` file here, sorted by filename with the full path breaking a tie. */
|
|
56
72
|
css: string[];
|
|
57
73
|
}
|
|
58
74
|
/**
|
|
59
75
|
* One origin's contribution, classified by filename alone.
|
|
60
76
|
*
|
|
61
|
-
* A `.html`
|
|
62
|
-
* which is what excludes `chrome.html`,
|
|
63
|
-
* and `imports.ts` here without naming
|
|
64
|
-
* application's own files, and
|
|
65
|
-
* separately.
|
|
77
|
+
* A `.html` file is a widget's only when its basename is a tag, and a module
|
|
78
|
+
* only when it is `<tag>.browser.ts` — which is what excludes `chrome.html`,
|
|
79
|
+
* `about.page.html`, `about.hooks.ts` and `imports.ts` here without naming
|
|
80
|
+
* any of them. Those are the application's own files, and
|
|
81
|
+
* build/locations.ts picks them out of `src` separately.
|
|
82
|
+
*
|
|
83
|
+
* A module named after a tag without the `.browser` goes in `unmarked`, so
|
|
84
|
+
* an author who forgot the word gets told which file to rename rather than
|
|
85
|
+
* being told their tag does not exist.
|
|
66
86
|
*/
|
|
67
87
|
export declare function scanOrigin(origin: Origin): Promise<OriginScan>;
|
|
68
88
|
/** The shape of an `imports.ts` module's default export: the packages an application pulls widgets from. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"origins.d.ts","sourceRoot":"","sources":["../../build/origins.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"origins.d.ts","sourceRoot":"","sources":["../../build/origins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAOH,gHAAgH;AAChH,eAAO,MAAM,kBAAkB,QAAkC,CAAC;AASlE,MAAM,WAAW,MAAM;IACrB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,qCAAqC;IACrC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,oEAAoE;IACpE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,oFAAoF;IACpF,GAAG,EAAE,MAAM,EAAE,CAAC;CACf;AAsBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA4EpE;AAED,4GAA4G;AAC5G,MAAM,MAAM,eAAe,GAAG,MAAM,EAAE,CAAC;AAEvC;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,CAAC,CA4B1B;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAkBjB;AAwED,MAAM,WAAW,WAAW;IAC1B,+FAA+F;IAC/F,aAAa,EAAE,MAAM,CAAC;IACtB,kGAAkG;IAClG,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,eAAe,EACzB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,EAAE,CAAC,CAkBnB;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,MAAM,EACX,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,UAAU,EAAE,CAAC,CAIvB;AAED,iEAAiE;AACjE,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,EAAE,CAErD"}
|
package/dist/build/origins.js
CHANGED
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
* Where a build's files come from — see docs/reference/builder.md.
|
|
3
3
|
*
|
|
4
4
|
* Three kinds of file reach the assembled application: a `.html` definition
|
|
5
|
-
* that expands a tag, a `.ts` script that registers its class, and a
|
|
6
|
-
* stylesheet. All three arrive from the same places, so all three are
|
|
7
|
-
* the same way: one ordered list of origins, scanned once each, read by
|
|
5
|
+
* that expands a tag, a `.browser.ts` script that registers its class, and a
|
|
6
|
+
* `.css` stylesheet. All three arrive from the same places, so all three are
|
|
7
|
+
* found the same way: one ordered list of origins, scanned once each, read by
|
|
8
8
|
* whoever needs them.
|
|
9
9
|
*
|
|
10
|
+
* A script says `.browser` because shipping is the thing worth being sure
|
|
11
|
+
* about. Under a plain `lb-thing.ts` rule a filename alone decides what
|
|
12
|
+
* crosses to the browser, so a module named after a tag someone else writes
|
|
13
|
+
* later is bundled without anyone having chosen that — and the author who
|
|
14
|
+
* would most like to know is the one who never considered it. Requiring the
|
|
15
|
+
* word puts the decision where a reader can see it, and leaves every
|
|
16
|
+
* unmarked module in a source tree exactly what it looks like: not browser
|
|
17
|
+
* code. It costs a widget author one word, once.
|
|
18
|
+
*
|
|
10
19
|
* The order is the cascade, and it is the same for every file kind:
|
|
11
20
|
*
|
|
12
21
|
* 1. this package's own `hub/`
|
|
@@ -36,7 +45,10 @@ import { pathToFileURL } from "node:url";
|
|
|
36
45
|
/** Lowercase, hyphenated — the one shape every custom element tag has, and the one native elements never do. */
|
|
37
46
|
export const CUSTOM_ELEMENT_TAG = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
|
|
38
47
|
const HTML_SUFFIX = ".html";
|
|
39
|
-
|
|
48
|
+
/** A widget script says so in its name: `lb-thing.browser.ts` and nothing else. */
|
|
49
|
+
const BROWSER_SUFFIXES = [".browser.ts", ".browser.js"];
|
|
50
|
+
/** What an unmarked module ends in — enough to recognise a near miss and say so. */
|
|
51
|
+
const MODULE_SUFFIXES = [".ts", ".js"];
|
|
40
52
|
const CSS_SUFFIX = ".css";
|
|
41
53
|
/**
|
|
42
54
|
* Every file under `dir`, its own nested `node_modules` excluded — those
|
|
@@ -62,15 +74,20 @@ async function filesUnder(dir) {
|
|
|
62
74
|
/**
|
|
63
75
|
* One origin's contribution, classified by filename alone.
|
|
64
76
|
*
|
|
65
|
-
* A `.html`
|
|
66
|
-
* which is what excludes `chrome.html`,
|
|
67
|
-
* and `imports.ts` here without naming
|
|
68
|
-
* application's own files, and
|
|
69
|
-
* separately.
|
|
77
|
+
* A `.html` file is a widget's only when its basename is a tag, and a module
|
|
78
|
+
* only when it is `<tag>.browser.ts` — which is what excludes `chrome.html`,
|
|
79
|
+
* `about.page.html`, `about.hooks.ts` and `imports.ts` here without naming
|
|
80
|
+
* any of them. Those are the application's own files, and
|
|
81
|
+
* build/locations.ts picks them out of `src` separately.
|
|
82
|
+
*
|
|
83
|
+
* A module named after a tag without the `.browser` goes in `unmarked`, so
|
|
84
|
+
* an author who forgot the word gets told which file to rename rather than
|
|
85
|
+
* being told their tag does not exist.
|
|
70
86
|
*/
|
|
71
87
|
export async function scanOrigin(origin) {
|
|
72
88
|
const definitions = new Map();
|
|
73
89
|
const scripts = new Map();
|
|
90
|
+
const unmarked = new Map();
|
|
74
91
|
const css = [];
|
|
75
92
|
const claim = (into, what, tag, file) => {
|
|
76
93
|
const already = into.get(tag);
|
|
@@ -93,16 +110,30 @@ export async function scanOrigin(origin) {
|
|
|
93
110
|
}
|
|
94
111
|
}
|
|
95
112
|
else {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
113
|
+
const browser = BROWSER_SUFFIXES.find((s) => name.endsWith(s));
|
|
114
|
+
if (browser) {
|
|
115
|
+
const tag = name.slice(0, -browser.length).toLowerCase();
|
|
116
|
+
// Only the trailing ".ts" comes off, never the ".browser": what is
|
|
117
|
+
// left becomes an import specifier, and esbuild resolves
|
|
118
|
+
// "lb-thing.browser" to "lb-thing.browser.ts" the same way as to
|
|
119
|
+
// ".js".
|
|
120
|
+
if (CUSTOM_ELEMENT_TAG.test(tag)) {
|
|
121
|
+
const ext = MODULE_SUFFIXES.find((e) => name.endsWith(e));
|
|
122
|
+
claim(scripts, "script", tag, file.slice(0, -ext.length));
|
|
123
|
+
}
|
|
98
124
|
continue;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// is
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
}
|
|
126
|
+
const ext = MODULE_SUFFIXES.find((e) => name.endsWith(e));
|
|
127
|
+
if (!ext)
|
|
128
|
+
continue;
|
|
129
|
+
// A module whose name is a tag but which never said it was browser
|
|
130
|
+
// code. It contributes nothing. Remembering it costs a map entry and
|
|
131
|
+
// buys an error message that names the file instead of describing an
|
|
132
|
+
// absence. A `.d.ts` never reaches here — stripping ".ts" leaves a
|
|
133
|
+
// dot, which is not a tag.
|
|
134
|
+
const tag = name.slice(0, -ext.length).toLowerCase();
|
|
135
|
+
if (CUSTOM_ELEMENT_TAG.test(tag) && !unmarked.has(tag)) {
|
|
136
|
+
unmarked.set(tag, file);
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
139
|
}
|
|
@@ -115,6 +146,7 @@ export async function scanOrigin(origin) {
|
|
|
115
146
|
...(origin.specifier ? { specifier: origin.specifier } : {}),
|
|
116
147
|
definitions,
|
|
117
148
|
scripts,
|
|
149
|
+
unmarked,
|
|
118
150
|
css,
|
|
119
151
|
};
|
|
120
152
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { applyData, applyTuple } from "./lb-apply";
|
|
2
|
-
//# sourceMappingURL=lb-hub.d.ts.map
|
|
2
|
+
//# sourceMappingURL=lb-hub.browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-hub.browser.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -73,7 +73,7 @@ describe("resolveElements", () => {
|
|
|
73
73
|
scans: await scansFor(`${FIXTURES}/local`),
|
|
74
74
|
});
|
|
75
75
|
assert.equal(resolved.get("app-box")?.kind, "file");
|
|
76
|
-
assert.match(resolved.get("app-box").value, /local\/widgets\/app-box$/);
|
|
76
|
+
assert.match(resolved.get("app-box").value, /local\/widgets\/app-box\.browser$/);
|
|
77
77
|
});
|
|
78
78
|
it("resolves a tag found by scanning a declared package", async () => {
|
|
79
79
|
const { resolved } = resolveElements({
|
|
@@ -81,7 +81,7 @@ describe("resolveElements", () => {
|
|
|
81
81
|
scans: await scansFor(`${FIXTURES}/manifest`, `${FIXTURES}/pkg`),
|
|
82
82
|
});
|
|
83
83
|
assert.equal(resolved.get("acme-widget")?.kind, "file");
|
|
84
|
-
assert.match(resolved.get("acme-widget").value, /pkg\/acme-widget$/);
|
|
84
|
+
assert.match(resolved.get("acme-widget").value, /pkg\/acme-widget\.browser$/);
|
|
85
85
|
});
|
|
86
86
|
it("needs no script for a tag with only an .html definition", async () => {
|
|
87
87
|
const { resolved } = resolveElements({
|
|
@@ -90,6 +90,17 @@ describe("resolveElements", () => {
|
|
|
90
90
|
});
|
|
91
91
|
assert.equal(resolved.size, 0);
|
|
92
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
|
+
});
|
|
93
104
|
it("throws when a used tag registers nothing", async () => {
|
|
94
105
|
await assert.rejects(async () => resolveElements({
|
|
95
106
|
used: new Set(["nowhere-widget"]),
|
|
@@ -102,6 +113,6 @@ describe("resolveElements", () => {
|
|
|
102
113
|
scans: await scansFor(`${FIXTURES}/collision`, `${FIXTURES}/pkg`),
|
|
103
114
|
});
|
|
104
115
|
assert.equal(resolved.get("acme-widget")?.kind, "file");
|
|
105
|
-
assert.match(resolved.get("acme-widget").value, /collision\/widgets\/acme-widget$/);
|
|
116
|
+
assert.match(resolved.get("acme-widget").value, /collision\/widgets\/acme-widget\.browser$/);
|
|
106
117
|
});
|
|
107
118
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acme-widget.browser.d.ts","sourceRoot":"","sources":["../../../../../../tests/fixtures/elements/collision/widgets/acme-widget.browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-box.browser.d.ts","sourceRoot":"","sources":["../../../../../../tests/fixtures/elements/local/widgets/app-box.browser.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acme-widget.browser.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/pkg/acme-widget.browser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-box.d.ts","sourceRoot":"","sources":["../../../../../../tests/fixtures/elements/unmarked/widgets/app-box.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -28,10 +28,10 @@ const NO_BUILTIN = {
|
|
|
28
28
|
};
|
|
29
29
|
const base = (name) => path.basename(name);
|
|
30
30
|
describe("scanOrigin", () => {
|
|
31
|
-
it("classifies by filename: a tag-shaped .html, .ts, and any .css", async () => {
|
|
31
|
+
it("classifies by filename: a tag-shaped .html, .browser.ts, and any .css", async () => {
|
|
32
32
|
const dir = await tree({
|
|
33
33
|
"app-box.html": "<div lb-slot></div>",
|
|
34
|
-
"app-box.ts": "export {};",
|
|
34
|
+
"app-box.browser.ts": "export {};",
|
|
35
35
|
"theme.css": "* {}",
|
|
36
36
|
});
|
|
37
37
|
try {
|
|
@@ -44,6 +44,49 @@ describe("scanOrigin", () => {
|
|
|
44
44
|
await rm(dir, { recursive: true, force: true });
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
|
+
it("leaves a module alone when its name does not say .browser", async () => {
|
|
48
|
+
const dir = await tree({
|
|
49
|
+
"app-box.html": "<div lb-slot></div>",
|
|
50
|
+
"secrets.ts": "export const KEY = 'shh';",
|
|
51
|
+
"app-box.ts": "export const KEY = 'shh';",
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const scan = await scanOrigin({ name: "src", dirs: [dir] });
|
|
55
|
+
assert.equal(scan.scripts.size, 0);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await rm(dir, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
it("remembers an unmarked module named after a tag, so the error can name it", async () => {
|
|
62
|
+
const dir = await tree({
|
|
63
|
+
"app-box.ts": "export {};",
|
|
64
|
+
"helpers.ts": "export {};",
|
|
65
|
+
"app-box.d.ts": "export {};",
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
const scan = await scanOrigin({ name: "src", dirs: [dir] });
|
|
69
|
+
assert.deepEqual([...scan.unmarked.keys()], ["app-box"]);
|
|
70
|
+
assert.match(scan.unmarked.get("app-box"), /app-box\.ts$/);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await rm(dir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
it("takes the marked file when both names are present", async () => {
|
|
77
|
+
const dir = await tree({
|
|
78
|
+
"app-box.ts": "export {};",
|
|
79
|
+
"app-box.browser.ts": "export {};",
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
const scan = await scanOrigin({ name: "src", dirs: [dir] });
|
|
83
|
+
assert.match(scan.scripts.get("app-box"), /app-box\.browser$/);
|
|
84
|
+
assert.match(scan.unmarked.get("app-box"), /app-box\.ts$/);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await rm(dir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
47
90
|
it("ignores files whose basename is not a tag", async () => {
|
|
48
91
|
const dir = await tree({
|
|
49
92
|
"chrome.html": "<lb-hub><main></main></lb-hub>",
|
|
@@ -62,7 +105,7 @@ describe("scanOrigin", () => {
|
|
|
62
105
|
});
|
|
63
106
|
it("finds files at any depth, and sorts css by filename not directory", async () => {
|
|
64
107
|
const dir = await tree({
|
|
65
|
-
"deep/nested/app-box.ts": "export {};",
|
|
108
|
+
"deep/nested/app-box.browser.ts": "export {};",
|
|
66
109
|
"widgets/app-box.css": ".b {}",
|
|
67
110
|
"00-reset.css": "* {}",
|
|
68
111
|
});
|
|
@@ -89,8 +132,8 @@ describe("scanOrigin", () => {
|
|
|
89
132
|
});
|
|
90
133
|
it("throws when one origin has two scripts for the same tag", async () => {
|
|
91
134
|
const dir = await tree({
|
|
92
|
-
"a/app-box.ts": "export {};",
|
|
93
|
-
"b/app-box.ts": "export {};",
|
|
135
|
+
"a/app-box.browser.ts": "export {};",
|
|
136
|
+
"b/app-box.browser.ts": "export {};",
|
|
94
137
|
});
|
|
95
138
|
try {
|
|
96
139
|
await assert.rejects(() => scanOrigin({ name: "src", dirs: [dir] }), /'src' has two script files for <app-box>/);
|
|
@@ -102,15 +145,15 @@ describe("scanOrigin", () => {
|
|
|
102
145
|
it("takes a compiled .js as a script, the way a published package ships one", async () => {
|
|
103
146
|
const dir = await tree({
|
|
104
147
|
"acme-widget.html": "<div lb-slot></div>",
|
|
105
|
-
"acme-widget.js": "export {};",
|
|
106
|
-
"acme-widget.d.ts": "export {};",
|
|
148
|
+
"acme-widget.browser.js": "export {};",
|
|
149
|
+
"acme-widget.browser.d.ts": "export {};",
|
|
107
150
|
});
|
|
108
151
|
try {
|
|
109
152
|
const scan = await scanOrigin({ name: "@acme/widgets", dirs: [dir] });
|
|
110
153
|
assert.deepEqual([...scan.definitions.keys()], ["acme-widget"]);
|
|
111
154
|
// The .d.ts is not a second script: stripping ".ts" leaves a dot.
|
|
112
155
|
assert.deepEqual([...scan.scripts.keys()], ["acme-widget"]);
|
|
113
|
-
assert.match(scan.scripts.get("acme-widget"), /acme-widget$/);
|
|
156
|
+
assert.match(scan.scripts.get("acme-widget"), /acme-widget\.browser$/);
|
|
114
157
|
}
|
|
115
158
|
finally {
|
|
116
159
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -168,9 +211,9 @@ describe("a package's declared widget directory", () => {
|
|
|
168
211
|
"node_modules/acme/package.json": JSON.stringify({
|
|
169
212
|
loadbare: { widgets: "./dist" },
|
|
170
213
|
}),
|
|
171
|
-
"node_modules/acme/dist/acme-widget.js": "export {};",
|
|
214
|
+
"node_modules/acme/dist/acme-widget.browser.js": "export {};",
|
|
172
215
|
"node_modules/acme/dist/acme-widget.html": "<b lb-slot></b>",
|
|
173
|
-
"node_modules/acme/src/acme-widget.ts": "export {};",
|
|
216
|
+
"node_modules/acme/src/acme-widget.browser.ts": "export {};",
|
|
174
217
|
"node_modules/acme/src/acme-widget.html": "<b lb-slot></b>",
|
|
175
218
|
"node_modules/acme/README.md": "# acme",
|
|
176
219
|
"imports.ts": `export default ["acme"];`,
|
|
@@ -179,7 +222,7 @@ describe("a package's declared widget directory", () => {
|
|
|
179
222
|
const scans = await scanAll(NO_BUILTIN, src, path.join(src, "imports.ts"));
|
|
180
223
|
const acme = scans.find((scan) => scan.name === "acme");
|
|
181
224
|
assert.deepEqual([...acme.scripts.keys()], ["acme-widget"]);
|
|
182
|
-
assert.match(acme.scripts.get("acme-widget"), /dist[/\\]acme-widget$/);
|
|
225
|
+
assert.match(acme.scripts.get("acme-widget"), /dist[/\\]acme-widget\.browser$/);
|
|
183
226
|
assert.match(acme.definitions.get("acme-widget"), /dist[/\\]acme-widget\.html$/);
|
|
184
227
|
}
|
|
185
228
|
finally {
|
|
@@ -189,7 +232,7 @@ describe("a package's declared widget directory", () => {
|
|
|
189
232
|
it("scans the whole package when it declares nothing", async () => {
|
|
190
233
|
const src = await tree({
|
|
191
234
|
"node_modules/acme/package.json": "{}",
|
|
192
|
-
"node_modules/acme/widgets/acme-widget.ts": "export {};",
|
|
235
|
+
"node_modules/acme/widgets/acme-widget.browser.ts": "export {};",
|
|
193
236
|
});
|
|
194
237
|
try {
|
|
195
238
|
const origins = await buildOrigins(NO_BUILTIN, ["acme"], src);
|
|
@@ -205,7 +248,7 @@ describe("a package's declared widget directory", () => {
|
|
|
205
248
|
"node_modules/acme/package.json": JSON.stringify({
|
|
206
249
|
loadbare: { widgets: "./dist" },
|
|
207
250
|
}),
|
|
208
|
-
"node_modules/acme/widgets/acme-widget.ts": "export {};",
|
|
251
|
+
"node_modules/acme/widgets/acme-widget.browser.ts": "export {};",
|
|
209
252
|
});
|
|
210
253
|
try {
|
|
211
254
|
await assert.rejects(() => buildOrigins(NO_BUILTIN, ["acme"], src), /'acme' declares widgets at '\.\/dist', which is not in the installed package/);
|
|
@@ -45,11 +45,17 @@ The builder classifies by name, not location.
|
|
|
45
45
|
| `imports.ts` | The packages this app takes widgets from |
|
|
46
46
|
| `*.css` | A stylesheet — see [CSS](./css.md) |
|
|
47
47
|
| Any other `.html` | A widget definition, named for the tag it defines |
|
|
48
|
-
|
|
|
48
|
+
| `<tag>.browser.ts` | A widget script, named for the tag it registers |
|
|
49
49
|
|
|
50
50
|
Give the application exactly one `chrome.html` and at most one `imports.ts`.
|
|
51
51
|
Give every `.hooks.ts` and `.queries.ts` a `.page.html` of the same base name.
|
|
52
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
|
+
|
|
53
59
|
## Widgets from packages
|
|
54
60
|
|
|
55
61
|
List a package in `imports.ts` to take widgets from it:
|
|
@@ -4,14 +4,20 @@ A widget is a custom element, written as a set of files sharing one tag
|
|
|
4
4
|
name. It serves two purposes, and an application uses it for either or for
|
|
5
5
|
both at once.
|
|
6
6
|
|
|
7
|
-
| File
|
|
8
|
-
|
|
9
|
-
| `<tag-name>.html`
|
|
10
|
-
| `<tag-name>.ts`
|
|
7
|
+
| File | Holds |
|
|
8
|
+
|--------------------------|----------------------------------|
|
|
9
|
+
| `<tag-name>.html` | The markup the tag expands into |
|
|
10
|
+
| `<tag-name>.browser.ts` | The class the tag registers |
|
|
11
11
|
|
|
12
12
|
Write one of the two, or both, but write at least one. A tag with neither is
|
|
13
13
|
a build error.
|
|
14
14
|
|
|
15
|
+
The script says `.browser` because that is the file that crosses to the
|
|
16
|
+
browser. Name it `<tag-name>.ts` and the builder leaves it alone: an
|
|
17
|
+
unmarked module is server-side code as far as the build is concerned, which
|
|
18
|
+
is what keeps a `stripe.ts` or a `config.ts` out of the bundle no matter what
|
|
19
|
+
tag someone writes later.
|
|
20
|
+
|
|
15
21
|
Custom elements are the only mechanism Loadbare offers for either purpose.
|
|
16
22
|
An application decomposes its HTML by defining a tag, and delivers behavior
|
|
17
23
|
to the browser by registering one; the builder recognizes no other way to do
|
|
@@ -197,7 +203,7 @@ an optional parameter out.
|
|
|
197
203
|
|
|
198
204
|
## Code
|
|
199
205
|
|
|
200
|
-
A
|
|
206
|
+
A `<tag>.browser.ts` file registers that tag's class. A widget is an
|
|
201
207
|
ordinary custom element — Loadbare imposes no base class — and it takes part
|
|
202
208
|
in data binding through three contracts: it receives a value, it sends a
|
|
203
209
|
request, and it accepts a set of rows.
|
|
@@ -224,7 +230,7 @@ text — see [Data Binding](./data-binding.md#where-a-bound-value-lands).
|
|
|
224
230
|
Observe it, and render the value however the widget renders things:
|
|
225
231
|
|
|
226
232
|
```ts
|
|
227
|
-
// src/visit-count.ts
|
|
233
|
+
// src/visit-count.browser.ts
|
|
228
234
|
import { ATTR_VALUE } from "@loadbare/app/constants";
|
|
229
235
|
|
|
230
236
|
class VisitCount extends HTMLElement {
|
|
@@ -309,8 +315,8 @@ Supply a `place` function to decide where a row goes — `(row, tuple,
|
|
|
309
315
|
template) => void`, called with a fresh or reordered row. The default
|
|
310
316
|
inserts immediately before the template, so rows accumulate in arrival
|
|
311
317
|
order. A widget that groups or sorts supplies its own `place` instead of
|
|
312
|
-
reimplementing matching and cloning around it; `lb-options.ts` and
|
|
313
|
-
`lb-table.ts` in [`@loadbare/widgets`](./widgets.md) are two different
|
|
318
|
+
reimplementing matching and cloning around it; `lb-options.browser.ts` and
|
|
319
|
+
`lb-table.browser.ts` in [`@loadbare/widgets`](./widgets.md) are two different
|
|
314
320
|
`place` functions over the same `applyRows`.
|
|
315
321
|
|
|
316
322
|
Style an empty list against `data-rows` rather than carrying an empty-state
|
package/docs/testing.md
CHANGED
|
@@ -20,7 +20,7 @@ emulation has given up the only failure mode it was looking for.
|
|
|
20
20
|
| 1 | Expansion and the build — `build/` | node |
|
|
21
21
|
| 2 | The engine — `server/`, and the Express adapter | node |
|
|
22
22
|
| 3 | Landing — `hub/lb-apply.ts`, `hub/lb-rows.ts` | jsdom |
|
|
23
|
-
| 4 | The hub — `hub/lb-hub.ts` | jsdom |
|
|
23
|
+
| 4 | The hub — `hub/lb-hub.browser.ts` | jsdom |
|
|
24
24
|
|
|
25
25
|
All four tiers run under `npm test` today and need no dependency that is not
|
|
26
26
|
already installed. A fifth environment — a real browser — is discussed at the
|
|
@@ -29,13 +29,20 @@ using `lb-cell="count"`.
|
|
|
29
29
|
## Writing the class
|
|
30
30
|
|
|
31
31
|
We put the Javascript class implementation into a file named
|
|
32
|
-
after the tag
|
|
33
|
-
the file as being associated with a custom element
|
|
34
|
-
in the app, and add its class to `client.js`.
|
|
32
|
+
after the tag, plus `.browser`: `visit-count.browser.ts`. The loadbare
|
|
33
|
+
builder will recognize the file as being associated with a custom element
|
|
34
|
+
that was used in the app, and add its class to `client.js`.
|
|
35
|
+
|
|
36
|
+
The `.browser` is required, and it is the only thing that puts a file in
|
|
37
|
+
`client.js`. Name the file `visit-count.ts` and the builder will leave it
|
|
38
|
+
alone — it treats an unmarked module as server-side code, and reports the tag
|
|
39
|
+
as having no script. That is deliberate: `src/database.ts` from the earlier
|
|
40
|
+
chapters sits in the same tree, and nothing an author writes in a page should
|
|
41
|
+
be able to pull it into the browser.
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
```ts
|
|
38
|
-
// src/visit-count.ts
|
|
45
|
+
// src/visit-count.browser.ts
|
|
39
46
|
import { ATTR_VALUE } from "@loadbare/app/constants";
|
|
40
47
|
|
|
41
48
|
class VisitCount extends HTMLElement {
|
|
@@ -21,7 +21,7 @@ how Loadbare internals work.
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
// src/note-input.ts
|
|
24
|
+
// src/note-input.browser.ts
|
|
25
25
|
import {
|
|
26
26
|
ATTR_CELL,
|
|
27
27
|
ATTR_KEY,
|
|
@@ -59,7 +59,7 @@ class NoteInput extends HTMLElement {
|
|
|
59
59
|
customElements.define("note-input", NoteInput);
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
A widget with both a `.html` and a `.ts` file shares one tag name; the
|
|
62
|
+
A widget with both a `.html` and a `.browser.ts` file shares one tag name; the
|
|
63
63
|
build finds each half independently. On change, it reads its own
|
|
64
64
|
`lb-query`/`lb-key`/`lb-cell` coordinates and dispatches a `cell-change`
|
|
65
65
|
request itself, the way `lb-update` does for a whole form.
|
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.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"loadbare-app-build": "dist/build/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
|
-
".": "./dist/hub/lb-hub.js",
|
|
18
|
+
".": "./dist/hub/lb-hub.browser.js",
|
|
19
19
|
"./constants": "./dist/core/lb-constants.js",
|
|
20
20
|
"./types": "./dist/core/lb-types.js",
|
|
21
21
|
"./rows": "./dist/hub/lb-rows.js",
|
package/dist/hub/lb-hub.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lb-hub.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"acme-widget.d.ts","sourceRoot":"","sources":["../../../../../../tests/fixtures/elements/collision/widgets/acme-widget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"app-box.d.ts","sourceRoot":"","sources":["../../../../../../tests/fixtures/elements/local/widgets/app-box.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"acme-widget.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/pkg/acme-widget.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC"}
|
|
File without changes
|
/package/dist/tests/fixtures/elements/collision/widgets/{acme-widget.js → acme-widget.browser.js}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|