diff --git a/.claude/settings.json b/.claude/settings.json index e9ba04395..fa4bc827c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -8,7 +8,7 @@ "type": "command", "timeout": 180, "statusMessage": "Running build + lint + typecheck before commit…", - "command": "node -e \"\nconst chunks = [];\nprocess.stdin.on('data', d => chunks.push(d));\nprocess.stdin.on('end', () => {\n const input = JSON.parse(Buffer.concat(chunks).toString());\n const cmd = input.tool_input?.command || '';\n if (!/git\\\\s+commit\\\\b/.test(cmd)) process.exit(0);\n const { execSync } = require('child_process');\n const cwd = process.env.PWD || process.cwd();\n const steps = [\n ['bun run build', 'Build'],\n ['bun run lint', 'Lint'],\n ['bun run --filter \\'*\\' typecheck 2>&1 | grep -v \\'vitest\\\\|test\\\\.ts\\' || true', 'Typecheck'],\n ];\n const failures = [];\n for (const [script, label] of steps) {\n try { execSync(script, { cwd, stdio: 'pipe' }); }\n catch (e) {\n failures.push(label + ':\\\\n' + (e.stdout?.toString() || e.message).slice(0, 400));\n }\n }\n if (failures.length > 0) {\n process.stdout.write(JSON.stringify({\n continue: false,\n stopReason: '\\u274c Pre-commit checks failed:\\\\n\\\\n' + failures.join('\\\\n\\\\n') + '\\\\n\\\\nFix the issues above before committing.',\n }));\n }\n});\"" + "command": "node -e \"\nconst chunks = [];\nprocess.stdin.on('data', d => chunks.push(d));\nprocess.stdin.on('end', () => {\n const input = JSON.parse(Buffer.concat(chunks).toString());\n const cmd = input.tool_input?.command || '';\n if (!/git\\\\s+commit\\\\b/.test(cmd)) process.exit(0);\n const { execSync } = require('child_process');\n const cwd = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n const steps = [\n ['bun run build', 'Build'],\n ['bun run lint', 'Lint'],\n ['bun run --filter \\'*\\' typecheck 2>&1 | grep -v \\'vitest\\\\|test\\\\.ts\\' || true', 'Typecheck'],\n ];\n const failures = [];\n for (const [script, label] of steps) {\n try { execSync(script, { cwd, stdio: 'pipe' }); }\n catch (e) {\n failures.push(label + ':\\\\n' + (e.stdout?.toString() || e.message).slice(0, 400));\n }\n }\n if (failures.length > 0) {\n process.stdout.write(JSON.stringify({\n continue: false,\n stopReason: '\\u274c Pre-commit checks failed:\\\\n\\\\n' + failures.join('\\\\n\\\\n') + '\\\\n\\\\nFix the issues above before committing.',\n }));\n }\n});\"" } ] } diff --git a/bun.lock b/bun.lock index 2ae53f19f..668387d0f 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,6 @@ "@hono/node-server": "^1.8.0", "@puppeteer/browsers": "^2.13.0", "adm-zip": "^0.5.16", - "cheerio": "^1.2.0", "citty": "^0.2.1", "compare-versions": "^6.1.1", "esbuild": "^0.25.0", @@ -48,7 +47,6 @@ "@types/mime-types": "^3.0.1", "@types/node": "^22.0.0", "adm-zip": "^0.5.16", - "cheerio": "^1.2.0", "hono": "^4.0.0", "linkedom": "^0.18.12", "mime-types": "^3.0.2", @@ -75,8 +73,8 @@ "vitest": "^3.2.4", }, "optionalDependencies": { - "cheerio": "^1.2.0", "esbuild": "^0.25.12", + "linkedom": "^0.18.12", }, "peerDependencies": { "hono": "^4.0.0", @@ -808,10 +806,6 @@ "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], - "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "2.1.0", "dom-serializer": "2.0.0", "domhandler": "5.0.3", "domutils": "3.2.2", "encoding-sniffer": "0.2.1", "htmlparser2": "10.1.0", "parse5": "7.3.0", "parse5-htmlparser2-tree-adapter": "7.1.0", "parse5-parser-stream": "7.1.2", "undici": "7.24.5", "whatwg-mimetype": "4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], - - "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "1.0.0", "css-select": "5.2.2", "css-what": "6.2.2", "domelementtype": "2.3.0", "domhandler": "5.0.3", "domutils": "3.2.2" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "4.1.2" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chromium-bidi": ["chromium-bidi@14.0.0", "", { "dependencies": { "mitt": "3.0.1", "zod": "3.25.76" }, "peerDependencies": { "devtools-protocol": "0.0.1581282" } }, "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw=="], @@ -908,11 +902,9 @@ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "0.6.3", "whatwg-encoding": "3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], @@ -1008,8 +1000,6 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "7.1.4", "debug": "4.4.3" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "1.0.1", "resolve-from": "4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], @@ -1202,10 +1192,6 @@ "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "6.0.1" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "5.0.3", "parse5": "7.3.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], - - "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "7.3.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], @@ -1288,8 +1274,6 @@ "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "1.4.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], @@ -1432,9 +1416,7 @@ "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], - "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], - - "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "1.15.0", "tr46": "6.0.0", "webidl-conversions": "8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], @@ -1512,7 +1494,7 @@ "chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "data-urls/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -1520,8 +1502,6 @@ "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -1530,8 +1510,6 @@ "jsdom/parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "6.0.1" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], - "jsdom/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], - "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], diff --git a/packages/cli/package.json b/packages/cli/package.json index 050b04017..3ee9a0c07 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -28,7 +28,6 @@ "@hono/node-server": "^1.8.0", "@puppeteer/browsers": "^2.13.0", "adm-zip": "^0.5.16", - "cheerio": "^1.2.0", "citty": "^0.2.1", "compare-versions": "^6.1.1", "esbuild": "^0.25.0", @@ -49,7 +48,6 @@ "@types/mime-types": "^3.0.1", "@types/node": "^22.0.0", "adm-zip": "^0.5.16", - "cheerio": "^1.2.0", "hono": "^4.0.0", "linkedom": "^0.18.12", "mime-types": "^3.0.2", diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts index 747ae7136..3eab4ccba 100644 --- a/packages/cli/tsup.config.ts +++ b/packages/cli/tsup.config.ts @@ -32,7 +32,6 @@ var __dirname = __hf_dirname(__filename);`, "hono", "hono/*", "@hono/node-server", - "cheerio", "mime-types", "adm-zip", "esbuild", diff --git a/packages/core/package.json b/packages/core/package.json index 1fa3bb6d3..72dc1cf2e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -109,7 +109,7 @@ } }, "optionalDependencies": { - "cheerio": "^1.2.0", - "esbuild": "^0.25.12" + "esbuild": "^0.25.12", + "linkedom": "^0.18.12" } } diff --git a/packages/core/src/compiler/htmlBundler.ts b/packages/core/src/compiler/htmlBundler.ts index e2fb81c6d..7933350cb 100644 --- a/packages/core/src/compiler/htmlBundler.ts +++ b/packages/core/src/compiler/htmlBundler.ts @@ -1,11 +1,24 @@ import { readFileSync, existsSync } from "fs"; import { join, resolve, isAbsolute, sep } from "path"; -import * as cheerio from "cheerio"; +import { parseHTML } from "linkedom"; import { transformSync } from "esbuild"; import { compileHtml, type MediaDurationProber } from "./htmlCompiler"; import { rewriteAssetPaths, rewriteCssAssetUrls } from "./rewriteSubCompPaths"; import { validateHyperframeHtmlContract } from "./staticGuard"; +/** + * Parse an HTML string into a document. Fragments (without a full document + * structure) are wrapped in `…` + * so that linkedom places the content inside `document.body`. + */ +function parseHTMLContent(html: string): Document { + const trimmed = html.trimStart().toLowerCase(); + if (trimmed.startsWith("${html}`).document; +} + /** Resolve a relative path within projectDir, rejecting traversal outside it. */ function safePath(projectDir: string, relativePath: string): string | null { const resolved = resolve(projectDir, relativePath); @@ -179,21 +192,23 @@ function rewriteCssUrlsWithInlinedAssets(cssText: string, projectDir: string): s ); } -function enforceCompositionPixelSizing($: cheerio.CheerioAPI): void { - const compositionEls = $("[data-composition-id][data-width][data-height]").toArray(); +function enforceCompositionPixelSizing(document: Document): void { + const compositionEls = [ + ...document.querySelectorAll("[data-composition-id][data-width][data-height]"), + ]; if (compositionEls.length === 0) return; const sizeMap = new Map(); for (const el of compositionEls) { - const compId = $(el).attr("data-composition-id"); - const w = Number($(el).attr("data-width")); - const h = Number($(el).attr("data-height")); + const compId = el.getAttribute("data-composition-id"); + const w = Number(el.getAttribute("data-width")); + const h = Number(el.getAttribute("data-height")); if (compId && Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) { sizeMap.set(compId, { w, h }); } } if (sizeMap.size === 0) return; - $("style").each((_, styleEl) => { - let css = $(styleEl).html() || ""; + for (const styleEl of document.querySelectorAll("style")) { + let css = styleEl.textContent || ""; let modified = false; for (const [compId, { w, h }] of sizeMap) { const escaped = compId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -209,52 +224,52 @@ function enforceCompositionPixelSizing($: cheerio.CheerioAPI): void { return open + newBody + close; }); } - if (modified) $(styleEl).text(css); - }); + if (modified) styleEl.textContent = css; + } } -function autoHealMissingCompositionIds($: cheerio.CheerioAPI): void { +function autoHealMissingCompositionIds(document: Document): void { const compositionIdRe = /data-composition-id=["']([^"']+)["']/gi; const referencedIds = new Set(); - $("style, script").each((_, el) => { - const text = ($(el).html() || "").trim(); - if (!text) return; + for (const el of document.querySelectorAll("style, script")) { + const text = (el.textContent || "").trim(); + if (!text) continue; let match: RegExpExecArray | null; while ((match = compositionIdRe.exec(text)) !== null) { const compId = (match[1] || "").trim(); if (compId) referencedIds.add(compId); } - }); + } if (referencedIds.size === 0) return; const existingIds = new Set(); - $("[data-composition-id]").each((_, el) => { - const id = ($(el).attr("data-composition-id") || "").trim(); + for (const el of document.querySelectorAll("[data-composition-id]")) { + const id = (el.getAttribute("data-composition-id") || "").trim(); if (id) existingIds.add(id); - }); + } for (const compId of referencedIds) { if (compId === "root" || existingIds.has(compId)) continue; const candidates = [`${compId}-layer`, `${compId}-comp`, compId]; for (const targetId of candidates) { - const match = $(`#${targetId}`).first(); - if (match.length > 0 && !match.attr("data-composition-id")) { - match.attr("data-composition-id", compId); + const found = document.getElementById(targetId); + if (found && !found.getAttribute("data-composition-id")) { + found.setAttribute("data-composition-id", compId); break; } } } } -function coalesceHeadStylesAndBodyScripts($: cheerio.CheerioAPI): void { - const headStyleEls = $("head style").toArray(); +function coalesceHeadStylesAndBodyScripts(document: Document): void { + const headStyleEls = [...document.querySelectorAll("head style")]; if (headStyleEls.length > 1) { const importRe = /@import\s+url\([^)]*\)\s*;|@import\s+["'][^"']+["']\s*;/gi; const imports: string[] = []; const cssParts: string[] = []; const seenImports = new Set(); for (const el of headStyleEls) { - const raw = ($(el).html() || "").trim(); + const raw = (el.textContent || "").trim(); if (!raw) continue; const nonImportCss = raw.replace(importRe, (match) => { const cleaned = match.trim(); @@ -269,29 +284,29 @@ function coalesceHeadStylesAndBodyScripts($: cheerio.CheerioAPI): void { } const merged = [...imports, ...cssParts].join("\n\n").trim(); if (merged) { - $(headStyleEls[0]).text(merged); - for (let i = 1; i < headStyleEls.length; i++) $(headStyleEls[i]).remove(); + headStyleEls[0]!.textContent = merged; + for (let i = 1; i < headStyleEls.length; i++) headStyleEls[i]!.remove(); } } - const bodyInlineScripts = $("body script") - .toArray() - .filter((el) => { - const src = ($(el).attr("src") || "").trim(); - if (src) return false; - const type = ($(el).attr("type") || "").trim().toLowerCase(); - return !type || type === "text/javascript" || type === "application/javascript"; - }); + const bodyInlineScripts = [...document.querySelectorAll("body script")].filter((el) => { + const src = (el.getAttribute("src") || "").trim(); + if (src) return false; + const type = (el.getAttribute("type") || "").trim().toLowerCase(); + return !type || type === "text/javascript" || type === "application/javascript"; + }); if (bodyInlineScripts.length > 0) { const mergedJs = bodyInlineScripts - .map((el) => ($(el).html() || "").trim()) + .map((el) => (el.textContent || "").trim()) .filter(Boolean) .join("\n;\n") .trim(); - for (const el of bodyInlineScripts) $(el).remove(); + for (const el of bodyInlineScripts) el.remove(); if (mergedJs) { const stripped = stripJsCommentsParserSafe(mergedJs); - $("body").append(``); + const inlineScript = document.createElement("script"); + inlineScript.textContent = stripped; + document.body.appendChild(inlineScript); } } } @@ -338,87 +353,99 @@ export async function bundleToSingleHtml( } const withInterceptor = injectInterceptor(compiled); - const $ = cheerio.load(withInterceptor); + const { document } = parseHTML(withInterceptor); // Inline local CSS const localCssChunks: string[] = []; let cssAnchorPlaced = false; - $('link[rel="stylesheet"]').each((_, el) => { - const href = $(el).attr("href"); - if (!href || !isRelativeUrl(href)) return; + for (const el of [...document.querySelectorAll('link[rel="stylesheet"]')]) { + const href = el.getAttribute("href"); + if (!href || !isRelativeUrl(href)) continue; const cssPath = safePath(projectDir, href); const css = cssPath ? safeReadFile(cssPath) : null; - if (css == null) return; + if (css == null) continue; localCssChunks.push(css); if (!cssAnchorPlaced) { - $(el).replaceWith(''); + const anchor = document.createElement("style"); + anchor.setAttribute("data-hf-bundled-local-css", "1"); + el.replaceWith(anchor); cssAnchorPlaced = true; } else { - $(el).remove(); + el.remove(); } - }); + } if (localCssChunks.length > 0) { - const $anchor = $('style[data-hf-bundled-local-css="1"]').first(); - if ($anchor.length) - $anchor.removeAttr("data-hf-bundled-local-css").text(localCssChunks.join("\n\n")); - else $("head").append(``); + const anchor = document.querySelector('style[data-hf-bundled-local-css="1"]'); + if (anchor) { + anchor.removeAttribute("data-hf-bundled-local-css"); + anchor.textContent = localCssChunks.join("\n\n"); + } else { + const style = document.createElement("style"); + style.textContent = localCssChunks.join("\n\n"); + document.head.appendChild(style); + } } // Inline local JS const localJsChunks: string[] = []; let jsAnchorPlaced = false; - $("script[src]").each((_, el) => { - const src = $(el).attr("src"); - if (!src || !isRelativeUrl(src)) return; + for (const el of [...document.querySelectorAll("script[src]")]) { + const src = el.getAttribute("src"); + if (!src || !isRelativeUrl(src)) continue; const jsPath = safePath(projectDir, src); const js = jsPath ? safeReadFile(jsPath) : null; - if (js == null) return; + if (js == null) continue; localJsChunks.push(js); if (!jsAnchorPlaced) { - $(el).replaceWith(''); + const anchor = document.createElement("script"); + anchor.setAttribute("data-hf-bundled-local-js", "1"); + el.replaceWith(anchor); jsAnchorPlaced = true; } else { - $(el).remove(); + el.remove(); } - }); + } if (localJsChunks.length > 0) { - const $anchor = $('script[data-hf-bundled-local-js="1"]').first(); - if ($anchor.length) - $anchor.removeAttr("data-hf-bundled-local-js").text(localJsChunks.join("\n;\n")); - else $("body").append(``); + const anchor = document.querySelector('script[data-hf-bundled-local-js="1"]'); + if (anchor) { + anchor.removeAttribute("data-hf-bundled-local-js"); + anchor.textContent = localJsChunks.join("\n;\n"); + } else { + const script = document.createElement("script"); + script.textContent = localJsChunks.join("\n;\n"); + document.body.appendChild(script); + } } // Inline sub-compositions const compStyleChunks: string[] = []; const compScriptChunks: string[] = []; const compExternalScriptSrcs: string[] = []; - $("[data-composition-src]").each((_, hostEl) => { - const src = $(hostEl).attr("data-composition-src"); - if (!src || !isRelativeUrl(src)) return; + for (const hostEl of [...document.querySelectorAll("[data-composition-src]")]) { + const src = hostEl.getAttribute("data-composition-src"); + if (!src || !isRelativeUrl(src)) continue; const compPath = safePath(projectDir, src); const compHtml = compPath ? safeReadFile(compPath) : null; if (compHtml == null) { console.warn(`[Bundler] Composition file not found: ${src}`); - return; + continue; } - const $comp = cheerio.load(compHtml); - const compId = $(hostEl).attr("data-composition-id"); - const $contentRoot = $comp("template").first(); - const contentHtml = $contentRoot.length - ? $contentRoot.html() || "" - : $comp("body").html() || ""; - const $content = cheerio.load(contentHtml); - const $innerRoot = compId - ? $content(`[data-composition-id="${compId}"]`).first() - : $content("[data-composition-id]").first(); + const compDoc = parseHTMLContent(compHtml); + const compId = hostEl.getAttribute("data-composition-id"); + const contentRoot = compDoc.querySelector("template"); + const contentHtml = contentRoot ? contentRoot.innerHTML || "" : compDoc.body.innerHTML || ""; + const contentDoc = parseHTMLContent(contentHtml); + const innerRoot = compId + ? contentDoc.querySelector(`[data-composition-id="${compId}"]`) + : contentDoc.querySelector("[data-composition-id]"); - $content("style").each((_, s) => { - compStyleChunks.push(rewriteCssAssetUrls($content(s).html() || "", src)); - $content(s).remove(); - }); - $content("script").each((_, s) => { - const externalSrc = ($content(s).attr("src") || "").trim(); + for (const s of [...contentDoc.querySelectorAll("style")]) { + compStyleChunks.push(rewriteCssAssetUrls(s.textContent || "", src)); + s.remove(); + } + for (const s of [...contentDoc.querySelectorAll("script")]) { + const externalSrc = (s.getAttribute("src") || "").trim(); if (externalSrc) { // External CDN/remote script — collect for deduped injection into the document. // Do NOT try to inline the content (external scripts have no innerHTML). @@ -427,155 +454,167 @@ export async function bundleToSingleHtml( } } else { compScriptChunks.push( - `(function(){ try { ${$content(s).html() || ""} } catch (_err) { console.error('[HyperFrames] composition script error:', _err); } })();`, + `(function(){ try { ${s.textContent || ""} } catch (_err) { console.error('[HyperFrames] composition script error:', _err); } })();`, ); } - $content(s).remove(); - }); + s.remove(); + } // Rewrite relative asset paths before inlining so ../foo.svg from // compositions/ resolves correctly when the content moves to root. - const $assetEls = $innerRoot.length - ? $innerRoot.find("[src], [href]") - : $content("[src], [href]"); + const assetEls = innerRoot + ? innerRoot.querySelectorAll("[src], [href]") + : contentDoc.querySelectorAll("[src], [href]"); rewriteAssetPaths( - $assetEls.toArray(), + assetEls, src, - (el, attr) => $content(el).attr(attr), - (el, attr, val) => { - $content(el).attr(attr, val); + (el: Element, attr: string) => el.getAttribute(attr), + (el: Element, attr: string, val: string) => { + el.setAttribute(attr, val); }, ); - if ($innerRoot.length) { - const innerCompId = $innerRoot.attr("data-composition-id"); - const innerW = $innerRoot.attr("data-width"); - const innerH = $innerRoot.attr("data-height"); - if (innerCompId && !$(hostEl).attr("data-composition-id")) - $(hostEl).attr("data-composition-id", innerCompId); - if (innerW && !$(hostEl).attr("data-width")) $(hostEl).attr("data-width", innerW); - if (innerH && !$(hostEl).attr("data-height")) $(hostEl).attr("data-height", innerH); - $innerRoot.find("style, script").remove(); - $(hostEl).html($innerRoot.html() || ""); + if (innerRoot) { + const innerCompId = innerRoot.getAttribute("data-composition-id"); + const innerW = innerRoot.getAttribute("data-width"); + const innerH = innerRoot.getAttribute("data-height"); + if (innerCompId && !hostEl.getAttribute("data-composition-id")) + hostEl.setAttribute("data-composition-id", innerCompId); + if (innerW && !hostEl.getAttribute("data-width")) hostEl.setAttribute("data-width", innerW); + if (innerH && !hostEl.getAttribute("data-height")) hostEl.setAttribute("data-height", innerH); + for (const child of [...innerRoot.querySelectorAll("style, script")]) child.remove(); + hostEl.innerHTML = innerRoot.innerHTML || ""; } else { - $content("style, script").remove(); - $(hostEl).html($content.html() || ""); + for (const child of [...contentDoc.querySelectorAll("style, script")]) child.remove(); + hostEl.innerHTML = contentDoc.body.innerHTML || ""; } - $(hostEl).removeAttr("data-composition-src"); - }); + hostEl.removeAttribute("data-composition-src"); + } // Inline template compositions: inject