@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/README.md +82 -0
- package/cli.mjs +66 -1
- package/floor.mjs +479 -0
- package/htmlscan.mjs +149 -0
- package/index.mjs +1 -0
- package/package.json +5 -1
- package/theme-entry.mjs +5 -1
- package/themehost.mjs +4 -38
- package/vendor/README.md +43 -0
- package/vendor/acorn.mjs +6313 -0
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 {
|
|
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"
|
|
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"
|
|
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" &&
|
|
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)) {
|
package/vendor/README.md
ADDED
|
@@ -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`.
|