@loadbare/app 0.4.0 → 0.5.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.
Files changed (160) hide show
  1. package/README.md +53 -82
  2. package/dist/build/assemble.d.ts +7 -5
  3. package/dist/build/assemble.d.ts.map +1 -1
  4. package/dist/build/assemble.js +29 -9
  5. package/dist/build/cli.d.ts +20 -12
  6. package/dist/build/cli.d.ts.map +1 -1
  7. package/dist/build/cli.js +34 -16
  8. package/dist/build/elements.d.ts +15 -29
  9. package/dist/build/elements.d.ts.map +1 -1
  10. package/dist/build/elements.js +25 -111
  11. package/dist/build/expand.d.ts +1 -1
  12. package/dist/build/expand.js +1 -1
  13. package/dist/build/locations.d.ts +14 -37
  14. package/dist/build/locations.d.ts.map +1 -1
  15. package/dist/build/locations.js +24 -67
  16. package/dist/build/origins.d.ts +109 -0
  17. package/dist/build/origins.d.ts.map +1 -0
  18. package/dist/build/origins.js +270 -0
  19. package/dist/core/lb-constants.d.ts +1 -0
  20. package/dist/core/lb-constants.d.ts.map +1 -1
  21. package/dist/core/lb-constants.js +15 -8
  22. package/dist/core/lb-types.d.ts +2 -2
  23. package/dist/core/lb-types.d.ts.map +1 -1
  24. package/dist/hub/lb-apply.js +1 -1
  25. package/dist/hub/lb-hub.d.ts.map +1 -1
  26. package/dist/hub/lb-hub.js +44 -17
  27. package/dist/hub/lb-rows.js +3 -3
  28. package/dist/server/lb-server.d.ts +5 -4
  29. package/dist/server/lb-server.d.ts.map +1 -1
  30. package/dist/tests/assemble.test.js +11 -4
  31. package/dist/tests/elements.test.js +47 -51
  32. package/dist/tests/expand.test.d.ts +1 -1
  33. package/dist/tests/expand.test.js +2 -2
  34. package/dist/tests/fixtures/elements/collision/imports.d.ts +3 -0
  35. package/dist/tests/fixtures/elements/collision/imports.d.ts.map +1 -0
  36. package/dist/tests/fixtures/elements/collision/imports.js +1 -0
  37. package/dist/tests/fixtures/elements/manifest/imports.d.ts +3 -0
  38. package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +1 -0
  39. package/dist/tests/fixtures/elements/manifest/imports.js +1 -0
  40. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +3 -0
  41. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +1 -0
  42. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +1 -0
  43. package/dist/tests/fixtures/elements/{collision/elements.d.ts → manifest-not-array/imports.d.ts} +1 -1
  44. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +1 -0
  45. package/dist/tests/fixtures/elements/manifest-not-array/imports.js +1 -0
  46. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts +2 -0
  47. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts.map +1 -0
  48. package/dist/tests/fixtures/elements/pkg/acme-widget.js +1 -0
  49. package/dist/tests/lb-express.test.js +1 -1
  50. package/dist/tests/origins.test.d.ts +10 -0
  51. package/dist/tests/origins.test.d.ts.map +1 -0
  52. package/dist/tests/origins.test.js +326 -0
  53. package/dist/tests/pages.test.js +3 -3
  54. package/dist/tests/styles.test.js +7 -4
  55. package/docs/reference/builder.md +128 -0
  56. package/docs/reference/chrome.md +75 -0
  57. package/docs/reference/css.md +44 -0
  58. package/docs/reference/custom-elements.md +327 -0
  59. package/docs/reference/data-binding.md +240 -0
  60. package/docs/reference/overview.md +38 -0
  61. package/docs/reference/page-files.md +175 -0
  62. package/docs/reference/server.md +123 -0
  63. package/docs/reference/widgets.md +163 -0
  64. package/docs/roadmap.md +130 -0
  65. package/docs/testing.md +228 -0
  66. package/docs/theory.md +344 -223
  67. package/docs/tutorials/000-getting-started.md +86 -0
  68. package/docs/tutorials/010-pages-and-navigation.md +129 -0
  69. package/docs/tutorials/020-css.md +103 -0
  70. package/docs/tutorials/030-html-decomposition.md +79 -0
  71. package/docs/tutorials/040-displaying-data.md +169 -0
  72. package/docs/tutorials/050-actions.md +77 -0
  73. package/docs/tutorials/060-custom-element-code.md +73 -0
  74. package/docs/tutorials/065-conditional-rendering.md +161 -0
  75. package/docs/tutorials/070-displaying-a-list.md +137 -0
  76. package/docs/tutorials/072-inserting-into-a-list.md +88 -0
  77. package/docs/tutorials/074-deleting-from-a-list.md +77 -0
  78. package/docs/tutorials/076-updating-a-list-item.md +86 -0
  79. package/docs/tutorials/080-widget-requests.md +124 -0
  80. package/docs/tutorials/090-using-widget-libraries.md +75 -0
  81. package/package.json +4 -12
  82. package/dist/client.js +0 -522
  83. package/dist/demo-static/src/widgets/app-box.d.ts +0 -15
  84. package/dist/demo-static/src/widgets/app-box.d.ts.map +0 -1
  85. package/dist/demo-static/src/widgets/app-box.js +0 -19
  86. package/dist/tests/fixtures/elements/collision/elements.d.ts.map +0 -1
  87. package/dist/tests/fixtures/elements/collision/elements.js +0 -3
  88. package/dist/tests/fixtures/elements/manifest/elements.d.ts +0 -5
  89. package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +0 -1
  90. package/dist/tests/fixtures/elements/manifest/elements.js +0 -3
  91. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +0 -5
  92. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +0 -1
  93. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +0 -3
  94. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +0 -5
  95. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +0 -1
  96. package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +0 -3
  97. package/dist/tests/golden.test.d.ts +0 -19
  98. package/dist/tests/golden.test.d.ts.map +0 -1
  99. package/dist/tests/golden.test.js +0 -60
  100. package/dist/tests/helpers/window.d.ts +0 -43
  101. package/dist/tests/helpers/window.d.ts.map +0 -1
  102. package/dist/tests/helpers/window.js +0 -78
  103. package/dist/tests/lb-input.test.d.ts +0 -9
  104. package/dist/tests/lb-input.test.d.ts.map +0 -1
  105. package/dist/tests/lb-input.test.js +0 -78
  106. package/dist/tests/lb-list.test.d.ts +0 -12
  107. package/dist/tests/lb-list.test.d.ts.map +0 -1
  108. package/dist/tests/lb-list.test.js +0 -44
  109. package/dist/tests/lb-options.test.d.ts +0 -10
  110. package/dist/tests/lb-options.test.d.ts.map +0 -1
  111. package/dist/tests/lb-options.test.js +0 -121
  112. package/dist/tests/lb-picker.test.d.ts +0 -14
  113. package/dist/tests/lb-picker.test.d.ts.map +0 -1
  114. package/dist/tests/lb-picker.test.js +0 -59
  115. package/dist/tests/lb-select.test.d.ts +0 -9
  116. package/dist/tests/lb-select.test.d.ts.map +0 -1
  117. package/dist/tests/lb-select.test.js +0 -71
  118. package/dist/tests/lb-table.test.d.ts +0 -15
  119. package/dist/tests/lb-table.test.d.ts.map +0 -1
  120. package/dist/tests/lb-table.test.js +0 -205
  121. package/dist/widgets/index.d.ts +0 -7
  122. package/dist/widgets/index.d.ts.map +0 -1
  123. package/dist/widgets/index.js +0 -6
  124. package/dist/widgets/lb-input.d.ts +0 -2
  125. package/dist/widgets/lb-input.d.ts.map +0 -1
  126. package/dist/widgets/lb-input.js +0 -48
  127. package/dist/widgets/lb-list.d.ts +0 -2
  128. package/dist/widgets/lb-list.d.ts.map +0 -1
  129. package/dist/widgets/lb-list.js +0 -17
  130. package/dist/widgets/lb-options.d.ts +0 -26
  131. package/dist/widgets/lb-options.d.ts.map +0 -1
  132. package/dist/widgets/lb-options.js +0 -72
  133. package/dist/widgets/lb-picker.d.ts +0 -2
  134. package/dist/widgets/lb-picker.d.ts.map +0 -1
  135. package/dist/widgets/lb-picker.js +0 -25
  136. package/dist/widgets/lb-select.d.ts +0 -2
  137. package/dist/widgets/lb-select.d.ts.map +0 -1
  138. package/dist/widgets/lb-select.js +0 -43
  139. package/dist/widgets/lb-table.d.ts +0 -2
  140. package/dist/widgets/lb-table.d.ts.map +0 -1
  141. package/dist/widgets/lb-table.js +0 -113
  142. package/docs/application-chrome.md +0 -36
  143. package/docs/building-html-pages.md +0 -130
  144. package/docs/getting-started.md +0 -120
  145. package/docs/guide.md +0 -1164
  146. package/docs/hosting.md +0 -218
  147. package/docs/latent-risks.md +0 -20
  148. package/widgets/index.ts +0 -6
  149. package/widgets/lb-input.html +0 -1
  150. package/widgets/lb-input.ts +0 -64
  151. package/widgets/lb-list.html +0 -1
  152. package/widgets/lb-list.ts +0 -21
  153. package/widgets/lb-options.html +0 -4
  154. package/widgets/lb-options.ts +0 -88
  155. package/widgets/lb-picker.html +0 -7
  156. package/widgets/lb-picker.ts +0 -27
  157. package/widgets/lb-select.html +0 -4
  158. package/widgets/lb-select.ts +0 -55
  159. package/widgets/lb-table.html +0 -8
  160. package/widgets/lb-table.ts +0 -126
@@ -9,20 +9,26 @@ import { describe, it } from "node:test";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { customElementTags } from "../build/expand";
11
11
  import { loadElementManifest, resolveElements } from "../build/elements";
12
- import { discoverSrc, packageInfo } from "../build/locations";
12
+ import { packageInfo } from "../build/locations";
13
+ import { scanOrigin } from "../build/origins";
13
14
  const FIXTURES = fileURLToPath(new URL("./fixtures/elements", import.meta.url));
14
15
  /**
15
- * The locations resolveElements needs, discovered the same way the CLI
16
- * discovers them — but via discoverSrc/packageInfo directly rather than the
17
- * full resolveLocations, since none of these fixtures has a chrome file and
18
- * resolveElements does not need one.
16
+ * The cascade resolveElements reads, built here rather than by
17
+ * build/origins.ts's buildOrigins: these fixtures name packages that are not
18
+ * installed, and resolution is what is under test, not package lookup.
19
19
  */
20
- async function locationsFor(src) {
21
- const [{ widgets, elementsCandidates }, info] = await Promise.all([
22
- discoverSrc(src),
23
- packageInfo(),
24
- ]);
25
- return { widgets, elementsFile: elementsCandidates[0], ...info };
20
+ async function scansFor(src, packageDir) {
21
+ const info = await packageInfo();
22
+ const origins = [
23
+ {
24
+ name: info.packageName,
25
+ dirs: [info.builtinHubDir],
26
+ specifier: () => info.packageName,
27
+ },
28
+ ...(packageDir ? [{ name: "@acme/widget", dirs: [packageDir] }] : []),
29
+ { name: src, dirs: [src] },
30
+ ];
31
+ return Promise.all(origins.map(scanOrigin));
26
32
  }
27
33
  describe("customElementTags", () => {
28
34
  it("collects every hyphenated tag, native elements excluded", () => {
@@ -35,77 +41,67 @@ describe("customElementTags", () => {
35
41
  });
36
42
  });
37
43
  describe("loadElementManifest", () => {
38
- it("returns the default export, keyed by tag", async () => {
39
- const manifest = await loadElementManifest(`${FIXTURES}/manifest/elements.ts`);
40
- assert.deepEqual(manifest, { "acme-widget": "@acme/widget" });
44
+ it("returns the declared package list", async () => {
45
+ const declared = await loadElementManifest(`${FIXTURES}/manifest/imports.ts`);
46
+ assert.deepEqual(declared, ["@acme/widget"]);
41
47
  });
42
- it("returns empty when there is no elements.ts", async () => {
43
- const manifest = await loadElementManifest(`${FIXTURES}/local/elements.ts`);
44
- assert.deepEqual(manifest, {});
48
+ it("returns empty when there is no imports.ts", async () => {
49
+ const declared = await loadElementManifest(`${FIXTURES}/local/imports.ts`);
50
+ assert.deepEqual(declared, []);
45
51
  });
46
- it("rejects a key that is not a valid custom element name", async () => {
47
- await assert.rejects(() => loadElementManifest(`${FIXTURES}/manifest-bad-tag/elements.ts`), /'acmewidget' is not a valid custom element name/);
52
+ it("rejects a default export that is not an array", async () => {
53
+ await assert.rejects(() => loadElementManifest(`${FIXTURES}/manifest-not-array/imports.ts`), /must default-export an array of package names/);
48
54
  });
49
- it("rejects a value that is not an import specifier string", async () => {
50
- await assert.rejects(() => loadElementManifest(`${FIXTURES}/manifest-bad-value/elements.ts`), /must map to an import specifier string/);
55
+ it("rejects an entry that is not a package name", async () => {
56
+ await assert.rejects(() => loadElementManifest(`${FIXTURES}/manifest-bad-entry/imports.ts`), /every entry must be a package name/);
51
57
  });
52
58
  });
53
59
  describe("resolveElements", () => {
54
- it("resolves a built-in tag to the package's own widgets export", async () => {
55
- const { resolved } = await resolveElements({
56
- used: new Set(["lb-input"]),
57
- ...(await locationsFor(`${FIXTURES}/local`)),
58
- });
59
- assert.deepEqual(resolved.get("lb-input"), {
60
- kind: "specifier",
61
- value: "@loadbare/app/widgets/lb-input",
62
- });
63
- });
64
- it("resolves lb-hub the same way, by its script in hub/ rather than widgets/", async () => {
65
- const { resolved } = await resolveElements({
60
+ it("resolves lb-hub to the package itself, the one built-in", async () => {
61
+ const { resolved } = resolveElements({
66
62
  used: new Set(["lb-hub"]),
67
- ...(await locationsFor(`${FIXTURES}/local`)),
63
+ scans: await scansFor(`${FIXTURES}/local`),
68
64
  });
69
65
  assert.deepEqual(resolved.get("lb-hub"), {
70
66
  kind: "specifier",
71
67
  value: "@loadbare/app",
72
68
  });
73
69
  });
74
- it("resolves a local tag to its file in <src>/widgets", async () => {
75
- const { resolved } = await resolveElements({
70
+ it("resolves a local tag to its file in <src>", async () => {
71
+ const { resolved } = resolveElements({
76
72
  used: new Set(["app-box"]),
77
- ...(await locationsFor(`${FIXTURES}/local`)),
73
+ scans: await scansFor(`${FIXTURES}/local`),
78
74
  });
79
75
  assert.equal(resolved.get("app-box")?.kind, "file");
80
76
  assert.match(resolved.get("app-box").value, /local\/widgets\/app-box$/);
81
77
  });
82
- it("resolves a tag declared only in elements.ts", async () => {
83
- const { resolved } = await resolveElements({
78
+ it("resolves a tag found by scanning a declared package", async () => {
79
+ const { resolved } = resolveElements({
84
80
  used: new Set(["acme-widget"]),
85
- ...(await locationsFor(`${FIXTURES}/manifest`)),
86
- });
87
- assert.deepEqual(resolved.get("acme-widget"), {
88
- kind: "specifier",
89
- value: "@acme/widget",
81
+ scans: await scansFor(`${FIXTURES}/manifest`, `${FIXTURES}/pkg`),
90
82
  });
83
+ assert.equal(resolved.get("acme-widget")?.kind, "file");
84
+ assert.match(resolved.get("acme-widget").value, /pkg\/acme-widget$/);
91
85
  });
92
86
  it("needs no script for a tag with only an .html definition", async () => {
93
- const { resolved } = await resolveElements({
87
+ const { resolved } = resolveElements({
94
88
  used: new Set(["lb-nav"]),
95
- ...(await locationsFor(`${FIXTURES}/markup-only`)),
89
+ scans: await scansFor(`${FIXTURES}/markup-only`),
96
90
  });
97
91
  assert.equal(resolved.size, 0);
98
92
  });
99
93
  it("throws when a used tag registers nothing", async () => {
100
94
  await assert.rejects(async () => resolveElements({
101
95
  used: new Set(["nowhere-widget"]),
102
- ...(await locationsFor(`${FIXTURES}/local`)),
96
+ scans: await scansFor(`${FIXTURES}/local`),
103
97
  }), /<nowhere-widget> has neither a script nor a definition/);
104
98
  });
105
- it("throws when a used tag is declared in more than one place", async () => {
106
- await assert.rejects(async () => resolveElements({
99
+ it("lets src win over a package that has the same tag", async () => {
100
+ const { resolved } = resolveElements({
107
101
  used: new Set(["acme-widget"]),
108
- ...(await locationsFor(`${FIXTURES}/collision`)),
109
- }), /<acme-widget> is declared in more than one place: src, .*elements\.ts/);
102
+ scans: await scansFor(`${FIXTURES}/collision`, `${FIXTURES}/pkg`),
103
+ });
104
+ assert.equal(resolved.get("acme-widget")?.kind, "file");
105
+ assert.match(resolved.get("acme-widget").value, /collision\/widgets\/acme-widget$/);
110
106
  });
111
107
  });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Expansion — see docs/expansion.md and docs/testing.md, tier 1.
2
+ * Expansion — see docs/reference/custom-elements.md and docs/testing.md, tier 1.
3
3
  *
4
4
  * Definitions are written inline here because `expand` takes them as a map
5
5
  * and a two-line definition beside its assertion reads better than a file
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Expansion — see docs/expansion.md and docs/testing.md, tier 1.
2
+ * Expansion — see docs/reference/custom-elements.md and docs/testing.md, tier 1.
3
3
  *
4
4
  * Definitions are written inline here because `expand` takes them as a map
5
5
  * and a two-line definition beside its assertion reads better than a file
@@ -192,7 +192,7 @@ describe("placement", () => {
192
192
  assert.match(html, /<span>row<\/span>/);
193
193
  });
194
194
  it("expands a tag outside the lb- namespace when it has a definition", () => {
195
- // An application's widget can use its own prefix — see docs/getting-started.md.
195
+ // An application's widget can use its own prefix — see docs/tutorials/000-getting-started.md.
196
196
  // The definition must still apply; discovering it is not enough.
197
197
  const html = expand(`<app-nav exp-label="Site"></app-nav>`, defs({ "app-nav": `<nav aria-label="{{label}}" lb-slot></nav>` }));
198
198
  assert.match(html, /<nav aria-label="Site"><\/nav>/);
@@ -0,0 +1,3 @@
1
+ declare const _default: string[];
2
+ export default _default;
3
+ //# sourceMappingURL=imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/collision/imports.ts"],"names":[],"mappings":";AAAA,wBAAgC"}
@@ -0,0 +1 @@
1
+ export default ["@acme/widget"];
@@ -0,0 +1,3 @@
1
+ declare const _default: string[];
2
+ export default _default;
3
+ //# sourceMappingURL=imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/manifest/imports.ts"],"names":[],"mappings":";AAAA,wBAAgC"}
@@ -0,0 +1 @@
1
+ export default ["@acme/widget"];
@@ -0,0 +1,3 @@
1
+ declare const _default: (string | number)[];
2
+ export default _default;
3
+ //# sourceMappingURL=imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/manifest-bad-entry/imports.ts"],"names":[],"mappings":";AAAA,wBAAoC"}
@@ -0,0 +1 @@
1
+ export default ["@acme/widget", 42];
@@ -2,4 +2,4 @@ declare const _default: {
2
2
  "acme-widget": string;
3
3
  };
4
4
  export default _default;
5
- //# sourceMappingURL=elements.d.ts.map
5
+ //# sourceMappingURL=imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/manifest-not-array/imports.ts"],"names":[],"mappings":";;;AAAA,wBAAiD"}
@@ -0,0 +1 @@
1
+ export default { "acme-widget": "@acme/widget" };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=acme-widget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acme-widget.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/pkg/acme-widget.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -107,7 +107,7 @@ describe("the request endpoint", () => {
107
107
  assert.equal(calls[0].name, "pick");
108
108
  assert.equal(calls[0].page, "roster");
109
109
  });
110
- it("passes on an action carrying no coordinates at all", async () => {
110
+ it("passes on an action carrying no binding at all", async () => {
111
111
  // A button carries no value and may sit outside any scope. Undefined
112
112
  // stays undefined rather than becoming the empty string.
113
113
  calls.length = 0;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The origin cascade — see build/origins.ts.
3
+ *
4
+ * Definitions, scripts and stylesheets all come from the same ordered list of
5
+ * origins, so they are all tested here: that one origin is scanned the same
6
+ * way whatever it is, that a duplicate tag inside one origin is an error, and
7
+ * that a later origin wins over an earlier one.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=origins.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"origins.test.d.ts","sourceRoot":"","sources":["../../tests/origins.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,326 @@
1
+ /**
2
+ * The origin cascade — see build/origins.ts.
3
+ *
4
+ * Definitions, scripts and stylesheets all come from the same ordered list of
5
+ * origins, so they are all tested here: that one origin is scanned the same
6
+ * way whatever it is, that a duplicate tag inside one origin is an error, and
7
+ * that a later origin wins over an earlier one.
8
+ */
9
+ import assert from "node:assert/strict";
10
+ import { describe, it } from "node:test";
11
+ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import path from "node:path";
14
+ import { buildOrigins, cssFrom, scanAll, scanOrigin } from "../build/origins";
15
+ async function tree(files) {
16
+ const dir = await mkdtemp(path.join(tmpdir(), "loadbare-origins-"));
17
+ for (const [name, content] of Object.entries(files)) {
18
+ const file = path.join(dir, name);
19
+ await mkdir(path.dirname(file), { recursive: true });
20
+ await writeFile(file, content, "utf-8");
21
+ }
22
+ return dir;
23
+ }
24
+ /** A builtin with no files of its own, so a test sees only what it wrote. */
25
+ const NO_BUILTIN = {
26
+ builtinHubDir: path.join(tmpdir(), "loadbare-absent-hub"),
27
+ packageName: "@loadbare/app",
28
+ };
29
+ const base = (name) => path.basename(name);
30
+ describe("scanOrigin", () => {
31
+ it("classifies by filename: a tag-shaped .html, .ts, and any .css", async () => {
32
+ const dir = await tree({
33
+ "app-box.html": "<div lb-slot></div>",
34
+ "app-box.ts": "export {};",
35
+ "theme.css": "* {}",
36
+ });
37
+ try {
38
+ const scan = await scanOrigin({ name: "src", dirs: [dir] });
39
+ assert.deepEqual([...scan.definitions.keys()], ["app-box"]);
40
+ assert.deepEqual([...scan.scripts.keys()], ["app-box"]);
41
+ assert.deepEqual(scan.css.map(base), ["theme.css"]);
42
+ }
43
+ finally {
44
+ await rm(dir, { recursive: true, force: true });
45
+ }
46
+ });
47
+ it("ignores files whose basename is not a tag", async () => {
48
+ const dir = await tree({
49
+ "chrome.html": "<lb-hub><main></main></lb-hub>",
50
+ "about.page.html": "<h1>About</h1>",
51
+ "about.hooks.ts": "export const hooks = {};",
52
+ "imports.ts": "export default {};",
53
+ });
54
+ try {
55
+ const scan = await scanOrigin({ name: "src", dirs: [dir] });
56
+ assert.equal(scan.definitions.size, 0);
57
+ assert.equal(scan.scripts.size, 0);
58
+ }
59
+ finally {
60
+ await rm(dir, { recursive: true, force: true });
61
+ }
62
+ });
63
+ it("finds files at any depth, and sorts css by filename not directory", async () => {
64
+ const dir = await tree({
65
+ "deep/nested/app-box.ts": "export {};",
66
+ "widgets/app-box.css": ".b {}",
67
+ "00-reset.css": "* {}",
68
+ });
69
+ try {
70
+ const scan = await scanOrigin({ name: "src", dirs: [dir] });
71
+ assert.deepEqual([...scan.scripts.keys()], ["app-box"]);
72
+ assert.deepEqual(scan.css.map(base), ["00-reset.css", "app-box.css"]);
73
+ }
74
+ finally {
75
+ await rm(dir, { recursive: true, force: true });
76
+ }
77
+ });
78
+ it("throws when one origin has two definitions for the same tag", async () => {
79
+ const dir = await tree({
80
+ "a/app-box.html": "<div></div>",
81
+ "b/app-box.html": "<span></span>",
82
+ });
83
+ try {
84
+ await assert.rejects(() => scanOrigin({ name: "src", dirs: [dir] }), /'src' has two definition files for <app-box>/);
85
+ }
86
+ finally {
87
+ await rm(dir, { recursive: true, force: true });
88
+ }
89
+ });
90
+ it("throws when one origin has two scripts for the same tag", async () => {
91
+ const dir = await tree({
92
+ "a/app-box.ts": "export {};",
93
+ "b/app-box.ts": "export {};",
94
+ });
95
+ try {
96
+ await assert.rejects(() => scanOrigin({ name: "src", dirs: [dir] }), /'src' has two script files for <app-box>/);
97
+ }
98
+ finally {
99
+ await rm(dir, { recursive: true, force: true });
100
+ }
101
+ });
102
+ it("takes a compiled .js as a script, the way a published package ships one", async () => {
103
+ const dir = await tree({
104
+ "acme-widget.html": "<div lb-slot></div>",
105
+ "acme-widget.js": "export {};",
106
+ "acme-widget.d.ts": "export {};",
107
+ });
108
+ try {
109
+ const scan = await scanOrigin({ name: "@acme/widgets", dirs: [dir] });
110
+ assert.deepEqual([...scan.definitions.keys()], ["acme-widget"]);
111
+ // The .d.ts is not a second script: stripping ".ts" leaves a dot.
112
+ assert.deepEqual([...scan.scripts.keys()], ["acme-widget"]);
113
+ assert.match(scan.scripts.get("acme-widget"), /acme-widget$/);
114
+ }
115
+ finally {
116
+ await rm(dir, { recursive: true, force: true });
117
+ }
118
+ });
119
+ });
120
+ describe("buildOrigins", () => {
121
+ it("orders the built-in first, then packages as declared, then src", async () => {
122
+ const src = await tree({
123
+ "node_modules/pkg-a/package.json": "{}",
124
+ "node_modules/pkg-b/package.json": "{}",
125
+ });
126
+ try {
127
+ const origins = await buildOrigins(NO_BUILTIN, ["pkg-b", "pkg-a"], src);
128
+ assert.deepEqual(origins.map((o) => o.name), ["@loadbare/app", "pkg-b", "pkg-a", src]);
129
+ }
130
+ finally {
131
+ await rm(src, { recursive: true, force: true });
132
+ }
133
+ });
134
+ it("throws when a declared package is not installed", async () => {
135
+ const src = await tree({});
136
+ try {
137
+ await assert.rejects(() => buildOrigins(NO_BUILTIN, ["nowhere-pkg"], src), /could not find installed package 'nowhere-pkg'/);
138
+ }
139
+ finally {
140
+ await rm(src, { recursive: true, force: true });
141
+ }
142
+ });
143
+ it("walks up from src to find node_modules at the application root", async () => {
144
+ const src = await tree({
145
+ "node_modules/pkg-a/package.json": "{}",
146
+ "node_modules/pkg-a/style.css": "a {}",
147
+ "app/src/chrome.html": "<lb-hub><main></main></lb-hub>",
148
+ });
149
+ try {
150
+ const inner = path.join(src, "app", "src");
151
+ const origins = await buildOrigins(NO_BUILTIN, ["pkg-a"], inner);
152
+ const scans = await Promise.all(origins.map(scanOrigin));
153
+ assert.deepEqual(cssFrom(scans).map(base), ["style.css"]);
154
+ }
155
+ finally {
156
+ await rm(src, { recursive: true, force: true });
157
+ }
158
+ });
159
+ });
160
+ describe("a package's declared widget directory", () => {
161
+ /**
162
+ * The published shape this exists for: compiled output beside the source it
163
+ * was built from. Scanning the whole package finds both and calls the tag
164
+ * a duplicate; scanning what the package declared finds the one that ships.
165
+ */
166
+ it("scans only loadbare.widgets, so source beside dist is not a duplicate", async () => {
167
+ const src = await tree({
168
+ "node_modules/acme/package.json": JSON.stringify({
169
+ loadbare: { widgets: "./dist" },
170
+ }),
171
+ "node_modules/acme/dist/acme-widget.js": "export {};",
172
+ "node_modules/acme/dist/acme-widget.html": "<b lb-slot></b>",
173
+ "node_modules/acme/src/acme-widget.ts": "export {};",
174
+ "node_modules/acme/src/acme-widget.html": "<b lb-slot></b>",
175
+ "node_modules/acme/README.md": "# acme",
176
+ "imports.ts": `export default ["acme"];`,
177
+ });
178
+ try {
179
+ const scans = await scanAll(NO_BUILTIN, src, path.join(src, "imports.ts"));
180
+ const acme = scans.find((scan) => scan.name === "acme");
181
+ assert.deepEqual([...acme.scripts.keys()], ["acme-widget"]);
182
+ assert.match(acme.scripts.get("acme-widget"), /dist[/\\]acme-widget$/);
183
+ assert.match(acme.definitions.get("acme-widget"), /dist[/\\]acme-widget\.html$/);
184
+ }
185
+ finally {
186
+ await rm(src, { recursive: true, force: true });
187
+ }
188
+ });
189
+ it("scans the whole package when it declares nothing", async () => {
190
+ const src = await tree({
191
+ "node_modules/acme/package.json": "{}",
192
+ "node_modules/acme/widgets/acme-widget.ts": "export {};",
193
+ });
194
+ try {
195
+ const origins = await buildOrigins(NO_BUILTIN, ["acme"], src);
196
+ const scans = await Promise.all(origins.map(scanOrigin));
197
+ assert.deepEqual([...scans[1].scripts.keys()], ["acme-widget"]);
198
+ }
199
+ finally {
200
+ await rm(src, { recursive: true, force: true });
201
+ }
202
+ });
203
+ it("refuses a declared directory the installed package does not have", async () => {
204
+ const src = await tree({
205
+ "node_modules/acme/package.json": JSON.stringify({
206
+ loadbare: { widgets: "./dist" },
207
+ }),
208
+ "node_modules/acme/widgets/acme-widget.ts": "export {};",
209
+ });
210
+ try {
211
+ await assert.rejects(() => buildOrigins(NO_BUILTIN, ["acme"], src), /'acme' declares widgets at '\.\/dist', which is not in the installed package/);
212
+ }
213
+ finally {
214
+ await rm(src, { recursive: true, force: true });
215
+ }
216
+ });
217
+ it("refuses a declared directory outside the package", async () => {
218
+ const src = await tree({
219
+ "node_modules/acme/package.json": JSON.stringify({
220
+ loadbare: { widgets: "../../src" },
221
+ }),
222
+ });
223
+ try {
224
+ await assert.rejects(() => buildOrigins(NO_BUILTIN, ["acme"], src), /which is outside the package/);
225
+ }
226
+ finally {
227
+ await rm(src, { recursive: true, force: true });
228
+ }
229
+ });
230
+ it("refuses a loadbare.widgets that is not a path", async () => {
231
+ const src = await tree({
232
+ "node_modules/acme/package.json": JSON.stringify({
233
+ loadbare: { widgets: ["./dist"] },
234
+ }),
235
+ });
236
+ try {
237
+ await assert.rejects(() => buildOrigins(NO_BUILTIN, ["acme"], src), /is not a path/);
238
+ }
239
+ finally {
240
+ await rm(src, { recursive: true, force: true });
241
+ }
242
+ });
243
+ });
244
+ describe("the css cascade", () => {
245
+ it("collects one package's CSS, sorted by filename", async () => {
246
+ const src = await tree({
247
+ "node_modules/acme-widgets/package.json": "{}",
248
+ "node_modules/acme-widgets/z.css": "z {}",
249
+ "node_modules/acme-widgets/a.css": "a {}",
250
+ "node_modules/acme-widgets/dist/00-reset.css": "* { margin: 0; }",
251
+ "imports.ts": `export default ["acme-widgets"];`,
252
+ });
253
+ try {
254
+ const scans = await scanAll(NO_BUILTIN, src, path.join(src, "imports.ts"));
255
+ assert.deepEqual(cssFrom(scans).map(base), [
256
+ "00-reset.css",
257
+ "a.css",
258
+ "z.css",
259
+ ]);
260
+ }
261
+ finally {
262
+ await rm(src, { recursive: true, force: true });
263
+ }
264
+ });
265
+ it("keeps packages in declaration order, without interleaving their files", async () => {
266
+ const src = await tree({
267
+ "node_modules/pkg-a/package.json": "{}",
268
+ "node_modules/pkg-a/a-only.css": "a {}",
269
+ "node_modules/pkg-b/package.json": "{}",
270
+ "node_modules/pkg-b/aaa-early.css": "b {}",
271
+ "imports.ts": `export default ["pkg-b", "pkg-a"];`,
272
+ });
273
+ try {
274
+ const scans = await scanAll(NO_BUILTIN, src, path.join(src, "imports.ts"));
275
+ assert.deepEqual(cssFrom(scans).map(base), [
276
+ "aaa-early.css",
277
+ "a-only.css",
278
+ ]);
279
+ }
280
+ finally {
281
+ await rm(src, { recursive: true, force: true });
282
+ }
283
+ });
284
+ it("puts the application's own css last, after every package's", async () => {
285
+ const src = await tree({
286
+ "node_modules/pkg-a/package.json": "{}",
287
+ "node_modules/pkg-a/zzz-late.css": "a {}",
288
+ "00-reset.css": "* {}",
289
+ "imports.ts": `export default ["pkg-a"];`,
290
+ });
291
+ try {
292
+ const scans = await scanAll(NO_BUILTIN, src, path.join(src, "imports.ts"));
293
+ assert.deepEqual(cssFrom(scans).map(base), [
294
+ "zzz-late.css",
295
+ "00-reset.css",
296
+ ]);
297
+ }
298
+ finally {
299
+ await rm(src, { recursive: true, force: true });
300
+ }
301
+ });
302
+ it("ignores a package's own nested node_modules", async () => {
303
+ const src = await tree({
304
+ "node_modules/pkg-a/package.json": "{}",
305
+ "node_modules/pkg-a/style.css": "a {}",
306
+ "node_modules/pkg-a/node_modules/transitive/style.css": "t {}",
307
+ "imports.ts": `export default ["pkg-a"];`,
308
+ });
309
+ try {
310
+ const scans = await scanAll(NO_BUILTIN, src, path.join(src, "imports.ts"));
311
+ assert.deepEqual(cssFrom(scans).map(base), ["style.css"]);
312
+ }
313
+ finally {
314
+ await rm(src, { recursive: true, force: true });
315
+ }
316
+ });
317
+ it("returns nothing when there is no css anywhere", async () => {
318
+ const src = await tree({ "chrome.html": "<lb-hub><main></main></lb-hub>" });
319
+ try {
320
+ assert.deepEqual(cssFrom(await scanAll(NO_BUILTIN, src)), []);
321
+ }
322
+ finally {
323
+ await rm(src, { recursive: true, force: true });
324
+ }
325
+ });
326
+ });
@@ -22,7 +22,7 @@ async function tree(files) {
22
22
  describe("discovering hooks and queries", () => {
23
23
  it("pairs a *.hooks.ts and *.queries.ts with the page sharing their basename", async () => {
24
24
  const src = await tree({
25
- "chrome.chrome.html": CHROME,
25
+ "chrome.html": CHROME,
26
26
  "pages/hello.page.html": `<p>hi</p>`,
27
27
  "pages/hello.hooks.ts": `export const hooks = {};`,
28
28
  "pages/hello.queries.ts": `export const queries = {};`,
@@ -42,7 +42,7 @@ describe("discovering hooks and queries", () => {
42
42
  });
43
43
  it("allows a page to have only one of the two", async () => {
44
44
  const src = await tree({
45
- "chrome.chrome.html": CHROME,
45
+ "chrome.html": CHROME,
46
46
  "pages/counter.page.html": `<p>0</p>`,
47
47
  "pages/counter.queries.ts": `export const queries = {};`,
48
48
  });
@@ -61,7 +61,7 @@ describe("discovering hooks and queries", () => {
61
61
  });
62
62
  it("throws when a hooks or queries file has no matching page", async () => {
63
63
  const src = await tree({
64
- "chrome.chrome.html": CHROME,
64
+ "chrome.html": CHROME,
65
65
  "pages/orphan.hooks.ts": `export const hooks = {};`,
66
66
  });
67
67
  try {