@loadbare/app 0.5.1 → 0.5.3

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.
Files changed (39) hide show
  1. package/dist/build/assemble.js +1 -1
  2. package/dist/build/elements.d.ts +4 -1
  3. package/dist/build/elements.d.ts.map +1 -1
  4. package/dist/build/elements.js +17 -1
  5. package/dist/build/origins.d.ts +29 -9
  6. package/dist/build/origins.d.ts.map +1 -1
  7. package/dist/build/origins.js +50 -18
  8. package/dist/hub/{lb-hub.d.ts → lb-hub.browser.d.ts} +1 -1
  9. package/dist/hub/lb-hub.browser.d.ts.map +1 -0
  10. package/dist/server/lb-express.js +1 -1
  11. package/dist/tests/elements.test.js +14 -3
  12. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +2 -0
  13. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +1 -0
  14. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +2 -0
  15. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +1 -0
  16. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +2 -0
  17. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +1 -0
  18. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +6 -0
  19. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +1 -0
  20. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +1 -0
  21. package/dist/tests/origins.test.js +56 -13
  22. package/docs/reference/builder.md +7 -1
  23. package/docs/reference/custom-elements.md +14 -8
  24. package/docs/testing.md +1 -1
  25. package/docs/tutorials/060-custom-element-code.md +11 -4
  26. package/docs/tutorials/065-conditional-rendering.md +1 -1
  27. package/docs/tutorials/080-widget-requests.md +2 -2
  28. package/package.json +2 -2
  29. package/dist/hub/lb-hub.d.ts.map +0 -1
  30. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts +0 -2
  31. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts.map +0 -1
  32. package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts +0 -2
  33. package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts.map +0 -1
  34. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts +0 -2
  35. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts.map +0 -1
  36. /package/dist/hub/{lb-hub.js → lb-hub.browser.js} +0 -0
  37. /package/dist/tests/fixtures/elements/collision/widgets/{acme-widget.js → acme-widget.browser.js} +0 -0
  38. /package/dist/tests/fixtures/elements/local/widgets/{app-box.js → app-box.browser.js} +0 -0
  39. /package/dist/tests/fixtures/elements/pkg/{acme-widget.js → acme-widget.browser.js} +0 -0
@@ -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.
@@ -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;;;;;;;;;;;;;;;;;;;GAmBG;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,CAiChC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,EAC/B,MAAM,EAAE,MAAM,GACb,MAAM,CAcR"}
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"}
@@ -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
  }
@@ -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 `.css`
6
- * stylesheet. All three arrive from the same places, so all three are found
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, extension stripped. */
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` or `.ts` file is a widget's only when its basename is a tag —
62
- * which is what excludes `chrome.html`, `about.page.html`, `about.hooks.ts`
63
- * and `imports.ts` here without naming any of them. Those are the
64
- * application's own files, and build/locations.ts picks them out of `src`
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAOH,gHAAgH;AAChH,eAAO,MAAM,kBAAkB,QAAkC,CAAC;AAMlE,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,mDAAmD;IACnD,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,oFAAoF;IACpF,GAAG,EAAE,MAAM,EAAE,CAAC;CACf;AAsBD;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA4DpE;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"}
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"}
@@ -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 `.css`
6
- * stylesheet. All three arrive from the same places, so all three are found
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
- const SCRIPT_SUFFIXES = [".ts", ".js"];
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` or `.ts` file is a widget's only when its basename is a tag —
66
- * which is what excludes `chrome.html`, `about.page.html`, `about.hooks.ts`
67
- * and `imports.ts` here without naming any of them. Those are the
68
- * application's own files, and build/locations.ts picks them out of `src`
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 suffix = SCRIPT_SUFFIXES.find((s) => name.endsWith(s));
97
- if (!suffix)
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
- const tag = name.slice(0, -suffix.length).toLowerCase();
100
- // Without the extension: this becomes an import specifier, and
101
- // esbuild resolves ".ts" from the bare path the same way as ".js".
102
- // A `.d.ts` never reaches here — stripping ".ts" leaves a dot, which
103
- // is not a tag.
104
- if (CUSTOM_ELEMENT_TAG.test(tag)) {
105
- claim(scripts, "script", tag, file.slice(0, -suffix.length));
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"}
@@ -15,7 +15,7 @@
15
15
  * lets the operation set stay closed.
16
16
  */
17
17
  import express from "express";
18
- import { LB_DATA_ENDPOINT, LB_REQUEST_ENDPOINT } from "../core/lb-constants";
18
+ import { LB_DATA_ENDPOINT, LB_REQUEST_ENDPOINT } from "../core/lb-constants.js";
19
19
  export function hubRoutes(hub, contextFor) {
20
20
  const router = express.Router();
21
21
  // Mounted here rather than on the app so that Hub's need for a parsed
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=acme-widget.browser.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=app-box.browser.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=acme-widget.browser.d.ts.map
@@ -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,6 @@
1
+ /**
2
+ * A widget script that forgot to say `.browser` — the fixture behind the
3
+ * error that names the file instead of reporting an absence.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=app-box.d.ts.map
@@ -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"}
@@ -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
- | Any other tag-shaped `.ts` | A widget script, named for the tag it registers |
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 | Holds |
8
- |------------------|----------------------------------|
9
- | `<tag-name>.html` | The markup the tag expands into |
10
- | `<tag-name>.ts` | The class the tag registers |
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 `.ts` file named for a tag registers that tag's class. A widget is an
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: `visit-count.ts`. The loadbare builder will recognize
33
- the file as being associated with a custom element that was used
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 {
@@ -47,7 +47,7 @@ arithmetic of its own.
47
47
  ## Writing the class
48
48
 
49
49
  ```ts
50
- // src/lb-wizard.ts
50
+ // src/lb-wizard.browser.ts
51
51
  import { ATTR_VALUE } from "@loadbare/app/constants";
52
52
 
53
53
  class LbWizard 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.1",
4
+ "version": "0.5.3",
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",
@@ -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,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=acme-widget.d.ts.map
@@ -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,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=app-box.d.ts.map
@@ -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,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=acme-widget.d.ts.map
@@ -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