@hyperframes/parsers 0.7.22 → 0.7.23
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.
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "is this sub-composition file usable?" check.
|
|
3
|
+
*
|
|
4
|
+
* `data-composition-src` files are authored by AI agents far more often than
|
|
5
|
+
* by humans clicking a UI. The dominant real-world failure is a scene worker
|
|
6
|
+
* that dies mid-write (or a step that references a scene before writing it),
|
|
7
|
+
* leaving an empty or partial `compositions/scene-*.html` on disk. Historically
|
|
8
|
+
* this surfaced in three different ways depending on which code path touched
|
|
9
|
+
* the file first:
|
|
10
|
+
*
|
|
11
|
+
* 1. A raw crash inside linkedom's `Document.head` getter — destructuring
|
|
12
|
+
* `firstElementChild` off a `null` `documentElement` — when the file is
|
|
13
|
+
* empty or contains no parseable markup.
|
|
14
|
+
* 2. An actionable-but-late `Error` thrown deep inside the render compiler
|
|
15
|
+
* (see git history: #1364), which aborted the whole render.
|
|
16
|
+
* 3. A silent skip (see git history: #1678) that drops the scene from the
|
|
17
|
+
* output with only a `console.warn`, producing a materially broken
|
|
18
|
+
* video (missing scene, no error surfaced anywhere) with no clear
|
|
19
|
+
* signal to the caller.
|
|
20
|
+
*
|
|
21
|
+
* This module gives every consumer (lint, render pre-flight, the tolerant
|
|
22
|
+
* inliner) a single, shared definition of "usable" so they can never
|
|
23
|
+
* disagree about whether a given file would render something. It lives in
|
|
24
|
+
* `@hyperframes/parsers` (rather than `@hyperframes/core`, where it
|
|
25
|
+
* originated) because `@hyperframes/lint` needs it too, and `lint` cannot
|
|
26
|
+
* depend on `core` — `core` already depends on `lint` — so this shared,
|
|
27
|
+
* dependency-free check lives in the common ancestor package both `core`
|
|
28
|
+
* and `lint` already depend on.
|
|
29
|
+
*
|
|
30
|
+
* `inlineSubCompositions.ts` (in `@hyperframes/core`) intentionally stays
|
|
31
|
+
* tolerant (skip + continue) for the preview/studio bundling path, where
|
|
32
|
+
* partial content while iterating is expected. `lint` and the render
|
|
33
|
+
* pre-flight check (`packages/producer/src/services/htmlCompiler.ts`) use
|
|
34
|
+
* this helper to fail loudly and name the exact offending file, because a
|
|
35
|
+
* render that silently drops a scene is strictly worse than a render that
|
|
36
|
+
* refuses to start.
|
|
37
|
+
*/
|
|
38
|
+
type SubCompositionValidityReason = "empty" | "unparsable" | "no-content" | "no-composition-root";
|
|
39
|
+
interface SubCompositionValidity {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
/** Present when `ok` is false. */
|
|
42
|
+
reason?: SubCompositionValidityReason;
|
|
43
|
+
/** Human-readable detail suitable for direct inclusion in an error message. */
|
|
44
|
+
detail?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Minimal shape both linkedom's `Document` and `happy-dom`'s satisfy. */
|
|
47
|
+
interface ParsableDocumentLike {
|
|
48
|
+
documentElement: {
|
|
49
|
+
outerHTML?: string;
|
|
50
|
+
} | null;
|
|
51
|
+
body?: {
|
|
52
|
+
innerHTML?: string | null;
|
|
53
|
+
} | null;
|
|
54
|
+
querySelector(selector: string): {
|
|
55
|
+
innerHTML?: string | null;
|
|
56
|
+
} | null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check whether `html` (the raw file contents resolved for a
|
|
60
|
+
* `data-composition-src` reference) is non-empty and parses to a document
|
|
61
|
+
* that actually contains renderable content.
|
|
62
|
+
*
|
|
63
|
+
* Mirrors the content-detection steps in `inlineSubCompositions` exactly
|
|
64
|
+
* (resolve → parse → find `<template>` or `<body>` content → parse that →
|
|
65
|
+
* confirm a `[data-composition-id]` root exists in it), so a file that
|
|
66
|
+
* passes this check is guaranteed to produce non-empty output from the
|
|
67
|
+
* inliner, and a file that fails it is guaranteed to hit one of the
|
|
68
|
+
* inliner's `onMissingComposition` branches.
|
|
69
|
+
*
|
|
70
|
+
* @param html Raw file contents, or `null`/`undefined` if the file could not
|
|
71
|
+
* be read (e.g. missing from disk). Callers should distinguish "missing"
|
|
72
|
+
* from "empty" in their own error message using a separate existence
|
|
73
|
+
* check — this function only inspects content.
|
|
74
|
+
* @param parseHtml Parse an HTML string into a document. Pass linkedom's
|
|
75
|
+
* `parseHTML(html).document` or the core bundler's `parseHTMLContent`.
|
|
76
|
+
*/
|
|
77
|
+
declare function checkSubCompositionUsability(html: string | null | undefined, parseHtml: (html: string) => ParsableDocumentLike): SubCompositionValidity;
|
|
78
|
+
|
|
79
|
+
export { type ParsableDocumentLike, type SubCompositionValidity, type SubCompositionValidityReason, checkSubCompositionUsability };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/subCompositionValidity.ts
|
|
2
|
+
function checkSubCompositionUsability(html, parseHtml) {
|
|
3
|
+
if (html == null || !html.trim()) {
|
|
4
|
+
return {
|
|
5
|
+
ok: false,
|
|
6
|
+
reason: "empty",
|
|
7
|
+
detail: "the file is empty (0 bytes or whitespace-only)"
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
const compDoc = parseHtml(html);
|
|
11
|
+
if (!compDoc.documentElement) {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
reason: "unparsable",
|
|
15
|
+
detail: "the file's contents could not be parsed as HTML"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const contentRoot = compDoc.querySelector("template");
|
|
19
|
+
const contentHtml = contentRoot ? contentRoot.innerHTML || "" : compDoc.body?.innerHTML || "";
|
|
20
|
+
if (!contentHtml.trim()) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: "no-content",
|
|
24
|
+
detail: "the file has no <template> or <body> content to render"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const contentDoc = parseHtml(contentHtml);
|
|
28
|
+
if (!contentDoc.documentElement) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
reason: "unparsable",
|
|
32
|
+
detail: "the file's <template>/<body> contents could not be parsed as HTML"
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (!contentDoc.querySelector("[data-composition-id]")) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
reason: "no-composition-root",
|
|
39
|
+
detail: "the file's <template>/<body> content has no element with a data-composition-id attribute"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { ok: true };
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
checkSubCompositionUsability
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=subCompositionValidity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/subCompositionValidity.ts"],"sourcesContent":["/**\n * Shared \"is this sub-composition file usable?\" check.\n *\n * `data-composition-src` files are authored by AI agents far more often than\n * by humans clicking a UI. The dominant real-world failure is a scene worker\n * that dies mid-write (or a step that references a scene before writing it),\n * leaving an empty or partial `compositions/scene-*.html` on disk. Historically\n * this surfaced in three different ways depending on which code path touched\n * the file first:\n *\n * 1. A raw crash inside linkedom's `Document.head` getter — destructuring\n * `firstElementChild` off a `null` `documentElement` — when the file is\n * empty or contains no parseable markup.\n * 2. An actionable-but-late `Error` thrown deep inside the render compiler\n * (see git history: #1364), which aborted the whole render.\n * 3. A silent skip (see git history: #1678) that drops the scene from the\n * output with only a `console.warn`, producing a materially broken\n * video (missing scene, no error surfaced anywhere) with no clear\n * signal to the caller.\n *\n * This module gives every consumer (lint, render pre-flight, the tolerant\n * inliner) a single, shared definition of \"usable\" so they can never\n * disagree about whether a given file would render something. It lives in\n * `@hyperframes/parsers` (rather than `@hyperframes/core`, where it\n * originated) because `@hyperframes/lint` needs it too, and `lint` cannot\n * depend on `core` — `core` already depends on `lint` — so this shared,\n * dependency-free check lives in the common ancestor package both `core`\n * and `lint` already depend on.\n *\n * `inlineSubCompositions.ts` (in `@hyperframes/core`) intentionally stays\n * tolerant (skip + continue) for the preview/studio bundling path, where\n * partial content while iterating is expected. `lint` and the render\n * pre-flight check (`packages/producer/src/services/htmlCompiler.ts`) use\n * this helper to fail loudly and name the exact offending file, because a\n * render that silently drops a scene is strictly worse than a render that\n * refuses to start.\n */\n\nexport type SubCompositionValidityReason =\n | \"empty\"\n | \"unparsable\"\n | \"no-content\"\n | \"no-composition-root\";\n\nexport interface SubCompositionValidity {\n ok: boolean;\n /** Present when `ok` is false. */\n reason?: SubCompositionValidityReason;\n /** Human-readable detail suitable for direct inclusion in an error message. */\n detail?: string;\n}\n\n/** Minimal shape both linkedom's `Document` and `happy-dom`'s satisfy. */\nexport interface ParsableDocumentLike {\n documentElement: { outerHTML?: string } | null;\n body?: { innerHTML?: string | null } | null;\n querySelector(selector: string): { innerHTML?: string | null } | null;\n}\n\n/**\n * Check whether `html` (the raw file contents resolved for a\n * `data-composition-src` reference) is non-empty and parses to a document\n * that actually contains renderable content.\n *\n * Mirrors the content-detection steps in `inlineSubCompositions` exactly\n * (resolve → parse → find `<template>` or `<body>` content → parse that →\n * confirm a `[data-composition-id]` root exists in it), so a file that\n * passes this check is guaranteed to produce non-empty output from the\n * inliner, and a file that fails it is guaranteed to hit one of the\n * inliner's `onMissingComposition` branches.\n *\n * @param html Raw file contents, or `null`/`undefined` if the file could not\n * be read (e.g. missing from disk). Callers should distinguish \"missing\"\n * from \"empty\" in their own error message using a separate existence\n * check — this function only inspects content.\n * @param parseHtml Parse an HTML string into a document. Pass linkedom's\n * `parseHTML(html).document` or the core bundler's `parseHTMLContent`.\n */\nexport function checkSubCompositionUsability(\n html: string | null | undefined,\n parseHtml: (html: string) => ParsableDocumentLike,\n): SubCompositionValidity {\n if (html == null || !html.trim()) {\n return {\n ok: false,\n reason: \"empty\",\n detail: \"the file is empty (0 bytes or whitespace-only)\",\n };\n }\n\n const compDoc = parseHtml(html);\n if (!compDoc.documentElement) {\n return {\n ok: false,\n reason: \"unparsable\",\n detail: \"the file's contents could not be parsed as HTML\",\n };\n }\n\n // Find content: prefer <template>, fall back to <body> — same precedence\n // inlineSubCompositions uses when extracting the sub-composition's markup.\n const contentRoot = compDoc.querySelector(\"template\");\n const contentHtml = contentRoot ? contentRoot.innerHTML || \"\" : compDoc.body?.innerHTML || \"\";\n if (!contentHtml.trim()) {\n return {\n ok: false,\n reason: \"no-content\",\n detail: \"the file has no <template> or <body> content to render\",\n };\n }\n\n const contentDoc = parseHtml(contentHtml);\n if (!contentDoc.documentElement) {\n return {\n ok: false,\n reason: \"unparsable\",\n detail: \"the file's <template>/<body> contents could not be parsed as HTML\",\n };\n }\n\n // The content must contain an actual composition root — the element the\n // inliner looks for (`contentDoc.querySelector(\"[data-composition-id]\")`)\n // to know what to inject into the host. Well-formed but marker-free HTML\n // (e.g. an AI-authored placeholder like `<body><p>TODO</p></body>`) parses\n // fine and has non-empty content, but has nothing for the inliner to find.\n if (!contentDoc.querySelector(\"[data-composition-id]\")) {\n return {\n ok: false,\n reason: \"no-composition-root\",\n detail:\n \"the file's <template>/<body> content has no element with a data-composition-id attribute\",\n };\n }\n\n return { ok: true };\n}\n"],"mappings":";AA8EO,SAAS,6BACd,MACA,WACwB;AACxB,MAAI,QAAQ,QAAQ,CAAC,KAAK,KAAK,GAAG;AAChC,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,QAAM,UAAU,UAAU,IAAI;AAC9B,MAAI,CAAC,QAAQ,iBAAiB;AAC5B,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAIA,QAAM,cAAc,QAAQ,cAAc,UAAU;AACpD,QAAM,cAAc,cAAc,YAAY,aAAa,KAAK,QAAQ,MAAM,aAAa;AAC3F,MAAI,CAAC,YAAY,KAAK,GAAG;AACvB,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,QAAM,aAAa,UAAU,WAAW;AACxC,MAAI,CAAC,WAAW,iBAAiB;AAC/B,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAOA,MAAI,CAAC,WAAW,cAAc,uBAAuB,GAAG;AACtD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QACE;AAAA,IACJ;AAAA,EACF;AAEA,SAAO,EAAE,IAAI,KAAK;AACpB;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/parsers",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.23",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/heygen-com/hyperframes",
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
"./composition": {
|
|
60
60
|
"import": "./dist/composition.js",
|
|
61
61
|
"types": "./dist/composition.d.ts"
|
|
62
|
+
},
|
|
63
|
+
"./sub-composition-validity": {
|
|
64
|
+
"import": "./dist/subCompositionValidity.js",
|
|
65
|
+
"types": "./dist/subCompositionValidity.d.ts"
|
|
62
66
|
}
|
|
63
67
|
},
|
|
64
68
|
"publishConfig": {
|
|
@@ -78,7 +82,7 @@
|
|
|
78
82
|
"tsx": "^4.21.0",
|
|
79
83
|
"typescript": "^5.0.0",
|
|
80
84
|
"vitest": "^3.2.4",
|
|
81
|
-
"@hyperframes/core": "0.7.
|
|
85
|
+
"@hyperframes/core": "0.7.23"
|
|
82
86
|
},
|
|
83
87
|
"scripts": {
|
|
84
88
|
"build": "tsup",
|