@weatherboard/gyde-design 0.4.3 → 0.4.4

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/themehost.mjs CHANGED
@@ -1,43 +1,9 @@
1
1
  /** Static host contract for app shells Gyde knows how to inspect. */
2
2
  import { readFileSync, existsSync } from "node:fs";
3
3
  import { join, resolve, relative, sep } from "node:path";
4
- import { parse } from "parse5";
4
+ import { htmlElements } from "./htmlscan.mjs";
5
5
  import { hasThemeStartup } from "./theme-entry.mjs";
6
6
 
7
- const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
8
-
9
- /**
10
- * Read browser-parsed elements in a plain HTML entry. The parser handles
11
- * raw text, RCDATA, and template contents as the browser does, so markup
12
- * written inside them cannot masquerade as executable scripts.
13
- */
14
- function htmlElements(html) {
15
- const nodes = [];
16
- const visit = (node, parent = null) => {
17
- if (node.tagName && node.namespaceURI === HTML_NAMESPACE) {
18
- if (node.tagName === "noscript") return;
19
- // An implied head is not evidence that the source declares one.
20
- if (node.sourceCodeLocation) {
21
- nodes.push({
22
- name: node.tagName,
23
- attrs: new Map(node.attrs.map(({ name, value }) => [name, value])),
24
- parent,
25
- inert: false,
26
- index: node.sourceCodeLocation.startOffset,
27
- text: node.childNodes?.filter((child) => child.nodeName === "#text")
28
- .map((child) => child.value).join("") ?? "",
29
- });
30
- }
31
- parent = node.tagName;
32
- }
33
- // parse5 stores template children in `content`, separate from childNodes.
34
- // Do not visit them: their scripts are inert in the host document.
35
- for (const child of node.childNodes ?? []) visit(child, parent);
36
- };
37
- visit(parse(html, { sourceCodeLocationInfo: true, scriptingEnabled: true }));
38
- return nodes;
39
- }
40
-
41
7
  function within(root, path) {
42
8
  if (typeof path !== "string" || !path || path.startsWith("/")) return null;
43
9
  const full = resolve(root, path);
@@ -83,11 +49,11 @@ export function checkThemeHosts(root, design = {}, { packages = null } = {}) {
83
49
  if (expected === null) errors.push(`${label}: emitted theme-bootstrap.js is missing or unreadable`);
84
50
  if (html === null || entry === null || expected === null) continue;
85
51
  const elements = htmlElements(html);
86
- if (!elements.some((n) => n.name === "head" && !n.inert)) {
52
+ if (!elements.some((n) => n.name === "head")) {
87
53
  errors.push(`${label}: HTML has no head element`);
88
54
  continue;
89
55
  }
90
- const headNodes = elements.filter((n) => n.parent === "head" && !n.inert);
56
+ const headNodes = elements.filter((n) => n.parent === "head");
91
57
  const inline = headNodes.find((n) => n.name === "script" &&
92
58
  !["src", "type", "defer", "async", "nomodule"].some((attr) => n.attrs.has(attr)) &&
93
59
  n.text.trim() === expected.trim());
@@ -102,7 +68,7 @@ export function checkThemeHosts(root, design = {}, { packages = null } = {}) {
102
68
  }
103
69
  }
104
70
  const src = `/${host.entry?.replace(/^\/+/, "")}`;
105
- const moduleTag = elements.some((n) => n.name === "script" && !n.inert &&
71
+ const moduleTag = elements.some((n) => n.name === "script" &&
106
72
  n.attrs.get("type")?.toLowerCase() === "module" && n.attrs.get("src") === src);
107
73
  if (!moduleTag) errors.push(`${label}: HTML must load the declared entry as a module script`);
108
74
  if (!hasThemeStartup(entry, systemPackage)) {
@@ -0,0 +1,43 @@
1
+ # Vendored third-party source
2
+
3
+ Files here are **copied verbatim from an npm package** and imported by relative
4
+ path. They are not edited, ever. `selfcontained.test.mjs` asserts each one is
5
+ byte-identical to the installed package it came from, so a hand edit or a stale
6
+ copy is a failing gate rather than a divergence nobody can see.
7
+
8
+ ## Why anything is vendored at all
9
+
10
+ `action.yml` runs `packages/design/cli.mjs` straight out of the action checkout
11
+ and **installs nothing** — no network call, no lockfile, no registry credential
12
+ in a consumer's CI. That is the whole of G-63, and it means every module the
13
+ action reaches may import `node:` builtins and files inside the action path,
14
+ and nothing else.
15
+
16
+ G-138 is what happens when that is stated in a comment and enforced by nothing:
17
+ `themehost.mjs` imported `parse5` and `theme-entry.mjs` imported `acorn`, the
18
+ `v1` tag moved, and every consumer's `design-system` check went red with
19
+ `ERR_MODULE_NOT_FOUND` — in a repository whose own CI installs both and was
20
+ therefore green. `selfcontained.test.mjs` is the guard that would have caught
21
+ it; this directory is how a genuinely needed package is carried.
22
+
23
+ ## Why acorn, and why not parse5
24
+
25
+ Vendoring is the second choice. The first is not needing the package.
26
+
27
+ `parse5`'s job in `themehost.mjs` was narrow and fully pinned by
28
+ `themehost.test.mjs`, and parse5 v8 is a fifteen-file ESM tree with its own
29
+ `entities` dependency — carrying it means carrying a dependency tree by hand.
30
+ So it was **dropped** for `../htmlscan.mjs`, 120 lines of our own code.
31
+
32
+ `acorn` parses arbitrary JavaScript, which must not be hand-rolled: the gate's
33
+ whole claim is that a call inside a string, a template, a regex or a dead
34
+ branch is not startup wiring, and that claim is only as good as the parser. It
35
+ ships `dist/acorn.mjs` as a **single self-contained ESM file with no
36
+ dependencies of its own**, so carrying it is one file and one equality check.
37
+
38
+ | file | package | from |
39
+ | --- | --- | --- |
40
+ | `acorn.mjs` | `acorn` | `node_modules/acorn/dist/acorn.mjs` |
41
+
42
+ To update: change the version in the root `package.json`, `npm install`, copy
43
+ the file across, and run `npm run verify`.