From a883487c02f64871ca6256a815bb90101c100a0a Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Tue, 28 Oct 2025 22:30:00 +0800 Subject: [PATCH 01/36] ci(parser): bump to 1.0.0 --- packages/@devtoolcss-parser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@devtoolcss-parser/package.json b/packages/@devtoolcss-parser/package.json index 2281f71..acbe04f 100644 --- a/packages/@devtoolcss-parser/package.json +++ b/packages/@devtoolcss-parser/package.json @@ -1,6 +1,6 @@ { "name": "@devtoolcss/parser", - "version": "0.0.8", + "version": "1.0.0", "description": "", "license": "MIT", "files": [ From 8a7f235c42a38988f1847dcda9b064cf103121fa Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Sat, 8 Nov 2025 02:26:19 +0800 Subject: [PATCH 02/36] refactor!(inliner,cleanclone,uiexport): use chrome-inspector --- packages/@devtoolcss-inliner/package.json | 2 +- packages/@devtoolcss-inliner/src/inliner.ts | 261 ++- .../src/optimizers/AriaExpanded.ts | 67 +- .../src/optimizers/PrunePseudoElement.ts | 24 +- .../src/optimizers/index.ts | 34 - .../src/optimizers/optimizer.ts | 35 +- packages/@devtoolcss-inliner/src/types.ts | 10 +- packages/cleanclone/package.json | 2 +- packages/cleanclone/src/crawler.ts | 63 +- packages/cleanclone/src/rewrite.ts | 4 +- packages/uiexport/package.json | 2 +- packages/uiexport/sidebar.js | 15 +- pnpm-lock.yaml | 1839 ++++++++++++++++- pnpm-workspace.yaml | 2 +- 14 files changed, 2092 insertions(+), 268 deletions(-) diff --git a/packages/@devtoolcss-inliner/package.json b/packages/@devtoolcss-inliner/package.json index 184d780..62ef778 100644 --- a/packages/@devtoolcss-inliner/package.json +++ b/packages/@devtoolcss-inliner/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@devtoolcss/parser": "workspace:*", - "@devtoolcss/inspector": "workspace:*", + "chrome-inspector": "^1.0.1", "css-what": "^7.0.0", "postcss": "^8.5.6", "postcss-var-replace": "^1.0.0" diff --git a/packages/@devtoolcss-inliner/src/inliner.ts b/packages/@devtoolcss-inliner/src/inliner.ts index ebbbf21..6696cb9 100644 --- a/packages/@devtoolcss-inliner/src/inliner.ts +++ b/packages/@devtoolcss-inliner/src/inliner.ts @@ -6,17 +6,21 @@ import type { ParsedCSSPropertyObject, ParsedCSSPropertyValue, } from "@devtoolcss/parser"; -import { Inspector, CDPNodeType } from "@devtoolcss/inspector"; +import { + Inspector, + CDPNodeType, + CDPNode, + InspectorElement, +} from "chrome-inspector"; import { iterateParsedCSS, traverse } from "@devtoolcss/parser"; import { forciblePseudoClasses } from "./constants.js"; import { AriaExpandedOptimizer, PrunePsuedoElementOptimizer, - invokeOptimizers, } from "./optimizers/index.js"; import type { - NodeWithId, + CDPNodeWithId, ParsedCSSRulesObjValue, ParsedStyleSheetObjValue, ParsedCSSRules, @@ -51,6 +55,7 @@ function getRewrittenSelectors( return [...rewrittenSelectors]; } +// TODO: check why outer not replaced function replaceVariables( rules: ParsedCSSRulesObjValue, ): ParsedCSSRulesObjValue { @@ -135,8 +140,16 @@ function rewriteSelectors(parsed: ParsedCSS, id: string): ParsedCSSRules { return rules; } +function removeOrigins(parsed: ParsedCSS, originsToRemove: string[]): void { + for (let i = parsed.matched.length - 1; i >= 0; --i) { + if (originsToRemove.includes(parsed.matched[i].origin)) { + parsed.matched.splice(i, 1); + } + } +} + function getInlineText( - node: NodeWithId, + node: CDPNodeWithId, parsedCSSs: ParsedCSS[], mediaConditions: string[], optimizers: Optimizer[], @@ -144,8 +157,11 @@ function getInlineText( const mediaRules: ParsedStyleSheetObjValue = {}; for (let i = 0; i < parsedCSSs.length; i++) { const parsed = parsedCSSs[i]; + removeOrigins(parsed, ["user-agent"]); const rules = rewriteSelectors(parsed, node.id!); - invokeOptimizers(optimizers, "afterRewriteSelectors", node, rules); + for (const optimizer of optimizers) { + optimizer.afterRewriteSelectors(node, rules); + } const inlineRules = cascade(rules); mediaRules[mediaConditions[i]] = replaceVariables(inlineRules); @@ -209,37 +225,33 @@ function getInlineText( return style; } -function setIdAttrs(node): NodeWithId { +function initIdCSS(node: CDPNode): CDPNodeWithId { let id = `node-${node.nodeId}`; let hasId = false; - /* - if (!node.attributes) { - const { attributes } = await DOM.getAttributes({ - nodeId: node.nodeId, - }); - node.attributes = attributes; - } - */ - for (let i = 0; i < node.attributes.length; i += 2) { - if (node.attributes[i] === "id") { - id = node.attributes[i + 1]; - if (id.includes(":")) { - // can break selector - id = id.replace(/:/g, "-"); - node.attributes[i + 1] = id; + + if (node.attributes) { + for (let i = 0; i < node.attributes.length; i += 2) { + if (node.attributes[i] === "id") { + id = node.attributes[i + 1]; + if (id.includes(":")) { + // can break selector + id = id.replace(/:/g, "-"); + node.attributes[i + 1] = id; + } + hasId = true; + break; } - hasId = true; - break; } - } - if (!hasId) { - node.attributes.push("id", id); + if (!hasId) { + node.attributes.push("id", id); + } } node.id = id; - return node; + node.css = []; + return node as CDPNodeWithId; } -function mergeTrees(roots: NodeWithId[], nScreens: number): NodeWithId { +function mergeTrees(roots: CDPNodeWithId[], nScreens: number): CDPNodeWithId { const mergedRoot = roots[0]; // merge css, filling missing with display: none if (mergedRoot.nodeType === CDPNodeType.ELEMENT_NODE) { @@ -275,7 +287,7 @@ function mergeTrees(roots: NodeWithId[], nScreens: number): NodeWithId { return mergedRoot; } - const nodeMap = new Map(); + const nodeMap = new Map(); for (const root of roots) { if (root.children) { @@ -297,14 +309,15 @@ function mergeTrees(roots: NodeWithId[], nScreens: number): NodeWithId { } function inlineStyle( - document: Document, // JSDOM or browser DOM + element: Element, // JSDOM or browser DOM cssAttr = "data-css", removeAttr = true, ) { // clean scripts/styles/links - document.querySelectorAll("script, link, style").forEach((el) => el.remove()); + element.querySelectorAll("script, link, style").forEach((el) => el.remove()); - const elements = document.querySelectorAll(`[${cssAttr}]`); + const elements = Array.from(element.querySelectorAll(`[${cssAttr}]`)); + elements.push(element); // include root element for (const el of elements) { const cssText = el.getAttribute(cssAttr); if (cssText) { @@ -313,7 +326,7 @@ function inlineStyle( // inline style can contain variables and override resolved stylesheet (el as HTMLElement).removeAttribute("style"); - const styleEl = document.createElement("style"); + const styleEl = element.ownerDocument.createElement("style"); styleEl.textContent = cssText; // keep here for stringify and eval const noChildTags = [ @@ -343,6 +356,8 @@ function inlineStyle( if (noChildTags.includes(el.tagName) || el.children.length === 0) { // prevent break :empty for those with no children // still may break :nth-child() + + // TODO: fix for root element el.parentNode.insertBefore(styleEl, el); } else { el.insertBefore(styleEl, el.children[0]); @@ -365,12 +380,66 @@ function inlineStyle( } } +function buildNodeTree( + cdpNode: CDPNodeWithId, + document: Document, +): Node | null { + let docNode: Node; + + switch (cdpNode.nodeType) { + case CDPNodeType.ELEMENT_NODE: + // iframe is safe because no children (not setting pierce) + docNode = document.createElement(cdpNode.localName); + + if (Array.isArray(cdpNode.attributes)) { + for (let i = 0; i < cdpNode.attributes.length; i += 2) { + (docNode as HTMLElement).setAttribute( + cdpNode.attributes[i], + cdpNode.attributes[i + 1], + ); + } + } + break; + + case CDPNodeType.TEXT_NODE: + docNode = document.createTextNode(cdpNode.nodeValue || ""); + break; + + case CDPNodeType.COMMENT_NODE: + docNode = document.createComment(cdpNode.nodeValue || ""); + break; + + default: + return null; + } + + // Recursively add children + if (cdpNode.children) { + for (const child of cdpNode.children) { + const childNode = buildNodeTree(child, document); + if (childNode) docNode.appendChild(childNode); + } + } + return docNode; +} + +function getFreezedCdpTree(cdpNode: CDPNode): CDPNodeWithId { + const nodeWithId = initIdCSS({ ...cdpNode }); + nodeWithId.css = []; + nodeWithId.children = []; + for (const child of cdpNode.children || []) { + nodeWithId.children.push(getFreezedCdpTree(child)); + } + return nodeWithId; +} + async function getInlinedComponent( selector: string, inspector: Inspector, + onProgress: (completed: number, total: number) => void = () => {}, onError: (e: any) => void = () => {}, options: InlineOptions = {}, -): Promise { +): Promise { const { highlightNode = false } = options; let { customScreens, mediaConditions } = options; if (!customScreens) { @@ -386,86 +455,69 @@ async function getInlinedComponent( new AriaExpandedOptimizer(), new PrunePsuedoElementOptimizer(), ]; - const roots = []; + const freezedRoots = []; for (let i = 0; i < customScreens.length; ++i) { - const node = await inspector.inspect(selector, { - depth: -1, - computed: false, - parseOptions: { excludeOrigin: ["user-agent"], removeUnusedVar: true }, - customScreen: customScreens[i], - beforeTraverse: async (rootNode, inspector, rootElement) => { - await invokeOptimizers( - optimizers, - "beforeTraverse", - rootNode, - inspector, - rootElement, - ); + if (customScreens[i]) { + await inspector.setDevice(customScreens[i]); + } + const root = inspector.querySelector(selector); + for (const optimizer of optimizers) { + await optimizer.beforeTraverse(root); + } + const freezedCdpRoot = getFreezedCdpTree(root._cdpNode); + + let total = 0; + await traverse( + freezedCdpRoot, + async (freezedNode: CDPNodeWithId) => { + total += 1; }, - beforeGetMatchedStyle: async (node, inspector, rootElement) => { + onError, + -1, + false, + ); + + let completed = 0; + await traverse( + freezedCdpRoot, + async (freezedNode: CDPNodeWithId) => { + const inspectorElement = inspector.getNodeByNodeId( + freezedNode.nodeId, + ) as InspectorElement; if (highlightNode) { - const objectId = await inspector.getNodeObjectId(node); - await inspector.scrollToNode(objectId); - await inspector.highlightNode(objectId); + await inspectorElement.scrollIntoView(); + await inspector.highlightNode(inspectorElement); } - await invokeOptimizers( - optimizers, - "beforeForcePseudo", - node, - inspector, - rootElement, - ); + for (const optimizer of optimizers) { + await optimizer.beforeForcePseudo(inspectorElement); + } - // Force all pseudo classes - await inspector.sendCommand("CSS.forcePseudoState", { - nodeId: node.nodeId, - forcedPseudoClasses: forciblePseudoClasses, - }); - }, - afterGetMatchedStyle: async (node, inspector, rootElement) => { - await invokeOptimizers( - optimizers, - "afterForcePseudo", - node, - inspector, - rootElement, + await inspector.forcePseudoState( + inspectorElement, + forciblePseudoClasses, ); - // Cleanup forced pseudo classes - await inspector.sendCommand("CSS.forcePseudoState", { - nodeId: node.nodeId, - forcedPseudoClasses: [], - }); - - if (highlightNode) { - await inspector.hideHighlight(); + for (const optimizer of optimizers) { + await optimizer.afterForcePseudo(inspectorElement); } - }, - }); - // label css by screen idx - await traverse( - node, - (n) => { - n.css = []; - n.css[i] = n.styles; + + freezedNode.css[i] = await inspectorElement.getMatchedStyles({ + parseOptions: { removeUnusedVar: true }, + }); + await inspector.forcePseudoState(inspectorElement, []); // reset forced pseudo states + onProgress(++completed, total); }, onError, -1, - true, + false, ); - roots.push(node); + if (highlightNode) { + await inspector.hideHighlight(); + } + freezedRoots.push(freezedCdpRoot); } - const root = mergeTrees(roots, customScreens.length); - await traverse( - root, - (node) => { - setIdAttrs(node); - }, - onError, - -1, - true, - ); + const root = mergeTrees(freezedRoots, customScreens.length); // after all node.id are set await traverse( root, @@ -482,9 +534,12 @@ async function getInlinedComponent( -1, true, ); - const doc = Inspector.nodeToDOM(root); - inlineStyle(doc); - return doc; + const docRoot = buildNodeTree( + root, + inspector.documentImpl.createHTMLDocument(), + ) as Element; + inlineStyle(docRoot); + return docRoot; } export { getInlinedComponent }; diff --git a/packages/@devtoolcss-inliner/src/optimizers/AriaExpanded.ts b/packages/@devtoolcss-inliner/src/optimizers/AriaExpanded.ts index 741c9cf..df51b9c 100644 --- a/packages/@devtoolcss-inliner/src/optimizers/AriaExpanded.ts +++ b/packages/@devtoolcss-inliner/src/optimizers/AriaExpanded.ts @@ -1,7 +1,7 @@ -import type { Inspector, Node } from "@devtoolcss/inspector"; import type { Optimizer } from "./optimizer.js"; import { parseCSSProperties } from "@devtoolcss/parser"; -import type { ParsedCSSRules, NodeWithId } from "../types.js"; +import type { ParsedCSSRules, CDPNodeWithId } from "../types.js"; +import type { InspectorElement } from "chrome-inspector"; /** handling li:has([aria-expanded]) nodes. @@ -21,16 +21,10 @@ export class AriaExpandedOptimizer implements Optimizer { * To be run in beforeTraverse. * Collects li:has([aria-expanded]) nodeIds from the rootElement. */ - async beforeTraverse( - rootNode: Node, - inspector: Inspector, - rootElement: Element, - ): Promise { + async beforeTraverse(root: InspectorElement): Promise { try { - rootElement.querySelectorAll("li:has([aria-expanded])").forEach((el) => { - this.checkChildrenNodeIds.add( - Number(el.attributes["data-nodeId"].value), - ); + root.querySelectorAll("li:has([aria-expanded])").forEach((el) => { + this.checkChildrenNodeIds.add(Number(el._cdpNode.nodeId)); }); } catch {} } @@ -38,57 +32,46 @@ export class AriaExpandedOptimizer implements Optimizer { /** * To be run before forcePseudoState. */ - async beforeForcePseudo( - node: Node, - inspector: Inspector, - rootElement: Element, - ): Promise { + async beforeForcePseudo(element: InspectorElement): Promise { // Collect children styles before forcing pseudo state - if (this.checkChildrenNodeIds.has(node.nodeId) && node.children) { + if ( + this.checkChildrenNodeIds.has(element._cdpNode.nodeId) && + element.children + ) { const childrenStyleBefore = []; - for (let i = 0; i < node.children.length; ++i) { - const child = node.children[i]; - const childrenStyle = await inspector.sendCommand( - "CSS.getMatchedStylesForNode", - { - nodeId: child.nodeId, - }, - ); + for (const child of element.children) { + const childrenStyle = await child.getMatchedStyles(); childrenStyleBefore.push(childrenStyle); } - this.childrenStyleBefore.set(node.nodeId, childrenStyleBefore); + this.childrenStyleBefore.set( + element._cdpNode.nodeId, + childrenStyleBefore, + ); } } /** * To be run before cleanup forcePseudo. */ - async afterForcePseudo( - node: Node, - inspector: Inspector, - rootElement: Element, - ): Promise { + async afterForcePseudo(element: InspectorElement): Promise { // Collect children styles after forcing pseudo state - if (this.checkChildrenNodeIds.has(node.nodeId) && node.children) { + if ( + this.checkChildrenNodeIds.has(element._cdpNode.nodeId) && + element.children + ) { const childrenStyleAfter = []; - for (let i = 0; i < node.children.length; ++i) { - const child = node.children[i]; - const childrenStyle = await inspector.sendCommand( - "CSS.getMatchedStylesForNode", - { - nodeId: child.nodeId, - }, - ); + for (const child of element.children) { + const childrenStyle = await child.getMatchedStyles(); childrenStyleAfter.push(childrenStyle); } - this.childrenStyleAfter.set(node.nodeId, childrenStyleAfter); + this.childrenStyleAfter.set(element._cdpNode.nodeId, childrenStyleAfter); } } /* * To be run after after rewriteSelectors before cascade. */ - afterRewriteSelectors(node: NodeWithId, rules: ParsedCSSRules): void { + afterRewriteSelectors(node: CDPNodeWithId, rules: ParsedCSSRules): void { const childrenStyleBefore = this.childrenStyleBefore.get(node.nodeId) || []; const childrenStyleAfter = this.childrenStyleAfter.get(node.nodeId) || []; diff --git a/packages/@devtoolcss-inliner/src/optimizers/PrunePseudoElement.ts b/packages/@devtoolcss-inliner/src/optimizers/PrunePseudoElement.ts index bf76439..bfb5450 100644 --- a/packages/@devtoolcss-inliner/src/optimizers/PrunePseudoElement.ts +++ b/packages/@devtoolcss-inliner/src/optimizers/PrunePseudoElement.ts @@ -1,6 +1,6 @@ -import type { Inspector, Node } from "@devtoolcss/inspector"; +import type { InspectorElement } from "chrome-inspector"; import type { Optimizer } from "./optimizer.js"; -import type { ParsedCSSRules, NodeWithId } from "../types.js"; +import type { ParsedCSSRules, CDPNodeWithId } from "../types.js"; import { getNormalizedSuffix } from "../utils.js"; import * as CSSwhat from "css-what"; @@ -10,28 +10,16 @@ import * as CSSwhat from "css-what"; export class PrunePsuedoElementOptimizer implements Optimizer { constructor() {} - async beforeTraverse( - rootNode: Node, - inspector: Inspector, - rootElement: Element, - ): Promise {} + async beforeTraverse(element: InspectorElement): Promise {} - async beforeForcePseudo( - node: Node, - inspector: Inspector, - rootElement: Element, - ): Promise {} + async beforeForcePseudo(element: InspectorElement): Promise {} - async afterForcePseudo( - node: Node, - inspector: Inspector, - rootElement: Element, - ): Promise {} + async afterForcePseudo(element: InspectorElement): Promise {} /** * clean up after rewriteSelectors. */ - afterRewriteSelectors(node: NodeWithId, rules: ParsedCSSRules): void { + afterRewriteSelectors(node: CDPNodeWithId, rules: ParsedCSSRules): void { for (const [selector, properties] of Object.entries(rules)) { const suffix = getNormalizedSuffix(CSSwhat.parse(selector)[0]); if (suffix.endsWith("before") || suffix.endsWith("after")) { diff --git a/packages/@devtoolcss-inliner/src/optimizers/index.ts b/packages/@devtoolcss-inliner/src/optimizers/index.ts index b214ee5..51138d9 100644 --- a/packages/@devtoolcss-inliner/src/optimizers/index.ts +++ b/packages/@devtoolcss-inliner/src/optimizers/index.ts @@ -1,36 +1,2 @@ -import { Optimizer, OptimizerMethodArgs } from "./optimizer.js"; export { AriaExpandedOptimizer } from "./AriaExpanded.js"; export { PrunePsuedoElementOptimizer } from "./PrunePseudoElement.js"; - -type SyncMethodNames = "afterRewriteSelectors"; -type AsyncMethodNames = - | "beforeTraverse" - | "beforeForcePseudo" - | "afterForcePseudo"; - -// Overload for async methods -export function invokeOptimizers( - optimizers: Optimizer[], - methodName: K, - ...args: OptimizerMethodArgs[K] -): Promise; - -// Overload for sync methods -export function invokeOptimizers( - optimizers: Optimizer[], - methodName: K, - ...args: OptimizerMethodArgs[K] -): void; - -// Implementation -export function invokeOptimizers( - optimizers: Optimizer[], - methodName: K, - ...args: OptimizerMethodArgs[K] -): void | Promise { - const results = optimizers.map((opt) => opt[methodName].apply(opt, args)); - // Check if any result is a Promise (async) - if (results.some((r) => r instanceof Promise)) { - return Promise.all(results).then(() => {}); // .then ensure Promise - } -} diff --git a/packages/@devtoolcss-inliner/src/optimizers/optimizer.ts b/packages/@devtoolcss-inliner/src/optimizers/optimizer.ts index fedfd59..2ca961d 100644 --- a/packages/@devtoolcss-inliner/src/optimizers/optimizer.ts +++ b/packages/@devtoolcss-inliner/src/optimizers/optimizer.ts @@ -1,5 +1,9 @@ -import type { Inspector, Node } from "@devtoolcss/inspector"; -import type { ParsedCSSRules, NodeWithId } from "../types.js"; +import type { ParsedCSSRules, CDPNodeWithId } from "../types.js"; +import type { + Inspector, + InspectorNode, + InspectorElement, +} from "chrome-inspector"; /** * Optimizer interface for CSS optimizations. @@ -12,42 +16,23 @@ export interface Optimizer { * To be run in beforeTraverse. * Collects nodeIds or performs setup before traversal. */ - beforeTraverse( - rootNode: Node, - inspector: Inspector, - rootElement: Element, - ): Promise; + beforeTraverse(root: InspectorElement): Promise; /** * To be run before forcePseudoState. * Collects styles or performs setup before forcing pseudo state. */ - beforeForcePseudo( - node: Node, - inspector: Inspector, - rootElement: Element, - ): Promise; + beforeForcePseudo(element: InspectorElement): Promise; /** * To be run after forcePseudoState cleanup. * Collects styles or performs cleanup after forcing pseudo state. */ - afterForcePseudo( - node: Node, - inspector: Inspector, - rootElement: Element, - ): Promise; + afterForcePseudo(element: InspectorElement): Promise; /** * To be run after rewriteSelectors before cascade. * Performs actions after selectors are rewritten. */ - afterRewriteSelectors(node: NodeWithId, rules: ParsedCSSRules): void; + afterRewriteSelectors(node: CDPNodeWithId, rules: ParsedCSSRules): void; } - -export type OptimizerMethodArgs = { - beforeTraverse: [rootNode: Node, inspector: Inspector, rootElement: Element]; - beforeForcePseudo: [node: Node, inspector: Inspector, rootElement: Element]; - afterForcePseudo: [node: Node, inspector: Inspector, rootElement: Element]; - afterRewriteSelectors: [node: NodeWithId, rules: ParsedCSSRules]; -}; diff --git a/packages/@devtoolcss-inliner/src/types.ts b/packages/@devtoolcss-inliner/src/types.ts index f8452ef..c5840a4 100644 --- a/packages/@devtoolcss-inliner/src/types.ts +++ b/packages/@devtoolcss-inliner/src/types.ts @@ -1,10 +1,14 @@ -import type { Node, Screen } from "@devtoolcss/inspector"; +import type { Device, CDPNode } from "chrome-inspector"; import type { ParsedCSSPropertyObject, ParsedCSSPropertyValue, } from "@devtoolcss/parser"; -export type NodeWithId = Node & { id: string; children?: NodeWithId[] }; +export type CDPNodeWithId = CDPNode & { + id: string; + children: CDPNodeWithId[]; + css: any[]; +}; export type ParsedCSSRules = { [selector: string]: ParsedCSSPropertyValue[]; @@ -20,6 +24,6 @@ export type ParsedStyleSheetObjValue = { export type InlineOptions = { highlightNode?: boolean; - customScreens?: Screen[]; + customScreens?: Device[]; mediaConditions?: string[]; }; diff --git a/packages/cleanclone/package.json b/packages/cleanclone/package.json index ec2d3e9..c0b4e4e 100644 --- a/packages/cleanclone/package.json +++ b/packages/cleanclone/package.json @@ -26,8 +26,8 @@ "type": "module", "dependencies": { "@devtoolcss/inliner": "workspace:*", - "@devtoolcss/inspector": "workspace:*", "chalk": "^5.6.2", + "chrome-inspector": "^1.0.1", "chrome-launcher": "^1.2.1", "chrome-remote-interface": "^0.33.3", "escape-html": "^1.0.3", diff --git a/packages/cleanclone/src/crawler.ts b/packages/cleanclone/src/crawler.ts index ac82ce9..bd51ded 100644 --- a/packages/cleanclone/src/crawler.ts +++ b/packages/cleanclone/src/crawler.ts @@ -3,7 +3,7 @@ import path from "path"; import { EventEmitter } from "events"; import CDP from "chrome-remote-interface"; import * as ChromeLauncher from "chrome-launcher"; -import { Inspector, type Screen } from "@devtoolcss/inspector"; +import { Inspector, type Device } from "chrome-inspector"; import { getInlinedComponent } from "@devtoolcss/inliner"; import { getAvailableFilename, @@ -88,7 +88,7 @@ export class Crawler extends EventEmitter { private downloadedURLs = new Set(); private assetDir = ""; private fontCSSPath = ""; - private screens: Screen[] = []; + private screens: Device[] = []; private mediaConditions: string[]; private toHighlight = false; @@ -484,21 +484,26 @@ export class Crawler extends EventEmitter { }); // @ts-ignore - const inspector = Inspector.fromCDPClient(client); - inspector.on("progress", ({ completed, total }) => { - this.emitProgress({ - crawlProgress: { - processedElements: completed, - totalElements: total, - }, - }); - }); + const inspector = await Inspector.fromCDPClient(client); inspector.on("error", this.onError); - const doc = await getInlinedComponent("body", inspector, this.onError, { - customScreens: this.screens, - mediaConditions: this.mediaConditions, - highlightNode: this.toHighlight, - }); + const body = await getInlinedComponent( + "body", + inspector, + (completed, total) => { + this.emitProgress({ + crawlProgress: { + processedElements: completed, + totalElements: total, + }, + }); + }, + this.onError, + { + customScreens: this.screens, + mediaConditions: this.mediaConditions, + highlightNode: this.toHighlight, + }, + ); // stop recording requests // @ts-ignore TODO: fix typing upstream @@ -544,17 +549,19 @@ export class Crawler extends EventEmitter { pageBase = path.dirname(path.join(pagePath, "dummy")); } const origin = getOrigin(pageURL); - normalizeSameSiteHref(doc, origin); - // Insert the font link tag as the first child of - const fontLink = doc.createElement("link"); - fontLink.rel = "stylesheet"; - fontLink.href = "/fonts.css"; - const head = doc.querySelector("head"); - head.insertBefore(fontLink, head.firstChild); - const rawHtml = doc.documentElement.outerHTML; - let outerHTML = - "\n" + - rewriteResourceLinks(origin, pageBase, resources, rawHtml); + normalizeSameSiteHref(body, origin); + const bodyHTML = rewriteResourceLinks( + origin, + pageBase, + resources, + body.outerHTML, + ); + let outerHTML = ` + + +${bodyHTML} + +`; outerHTML = beautify.html(outerHTML, { indent_size: 2, wrap_line_length: 0, // disable line wrapping @@ -567,7 +574,7 @@ export class Crawler extends EventEmitter { await client.close(); } - private buildScreens(): Screen[] { + private buildScreens(): Device[] { return this.cfg.deviceWidths.map((width) => ({ width, height: this.cfg.screenHeight, diff --git a/packages/cleanclone/src/rewrite.ts b/packages/cleanclone/src/rewrite.ts index 3c9bd86..3e07d15 100644 --- a/packages/cleanclone/src/rewrite.ts +++ b/packages/cleanclone/src/rewrite.ts @@ -54,8 +54,8 @@ export function rewriteResourceLinks( return outerHTML; } -export function normalizeSameSiteHref(doc: Document, origin: string) { - doc.querySelectorAll("a").forEach((el) => { +export function normalizeSameSiteHref(body: Element, origin: string) { + body.querySelectorAll("a").forEach((el) => { if (el.href) { try { const url = new URL(el.href, origin); diff --git a/packages/uiexport/package.json b/packages/uiexport/package.json index 02bbe26..16d8ab4 100644 --- a/packages/uiexport/package.json +++ b/packages/uiexport/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@devtoolcss/inliner": "workspace:*", - "@devtoolcss/inspector": "workspace:*", + "chrome-inspector": "^1.0.1", "highlight.js": "^11.11.1", "js-beautify": "^1.15.4" }, diff --git a/packages/uiexport/sidebar.js b/packages/uiexport/sidebar.js index cc58ba2..a6794bb 100644 --- a/packages/uiexport/sidebar.js +++ b/packages/uiexport/sidebar.js @@ -1,6 +1,6 @@ /// -import { Inspector } from "@devtoolcss/inspector"; +import { Inspector } from "chrome-inspector"; import { getInlinedComponent } from "@devtoolcss/inliner"; import { getUniqueSelector } from "./selector.js"; @@ -52,16 +52,17 @@ const exportBtn = document.getElementById("exportBtn"); try { await chrome.debugger.attach(target, "1.3"); const selector = await inspectedWindowEval(getUniqueSelector, "$0"); - const inspector = Inspector.fromChromeDebugger( + const inspector = await Inspector.fromChromeDebugger( chrome.debugger, target.tabId, ); - inspector.on("progress", (progress) => { - updateProgress(progress.completed, progress.total); - }); - const doc = await getInlinedComponent(selector, inspector); + const element = await getInlinedComponent( + selector, + inspector, + updateProgress, + ); - iframe.contentDocument.body.innerHTML = doc.body.innerHTML; + iframe.contentDocument.body.innerHTML = element.outerHTML; await chrome.debugger.detach(target); } catch (e) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 819b644..f370938 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,24 +12,1859 @@ importers: specifier: ^5.9.2 version: 5.9.3 + packages/@devtoolcss-inliner: + dependencies: + '@devtoolcss/parser': + specifier: workspace:* + version: link:../@devtoolcss-parser + chrome-inspector: + specifier: ^1.0.1 + version: 1.0.1 + css-what: + specifier: ^7.0.0 + version: 7.0.0 + postcss: + specifier: ^8.5.6 + version: 8.5.6 + postcss-var-replace: + specifier: ^1.0.0 + version: 1.0.0(postcss@8.5.6) + devDependencies: + devtools-protocol: + specifier: ^0.0.1528500 + version: 0.0.1528500 + packages/@devtoolcss-parser: devDependencies: devtools-protocol: specifier: ^0.0.1528500 version: 0.0.1528500 + packages/cleanclone: + dependencies: + '@devtoolcss/inliner': + specifier: workspace:* + version: link:../@devtoolcss-inliner + chalk: + specifier: ^5.6.2 + version: 5.6.2 + chrome-inspector: + specifier: ^1.0.1 + version: 1.0.1 + chrome-launcher: + specifier: ^1.2.1 + version: 1.2.1 + chrome-remote-interface: + specifier: ^0.33.3 + version: 0.33.3 + escape-html: + specifier: ^1.0.3 + version: 1.0.3 + escape-string-regexp: + specifier: ^5.0.0 + version: 5.0.0 + ink: + specifier: ^6.3.0 + version: 6.4.0(@types/react@19.2.2)(react@19.2.0) + js-beautify: + specifier: ^1.15.4 + version: 1.15.4 + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + react: + specifier: ^19.1.1 + version: 19.2.0 + yargs: + specifier: ^18.0.0 + version: 18.0.0 + devDependencies: + '@types/chrome-remote-interface': + specifier: ^0.31.14 + version: 0.31.14 + '@types/js-beautify': + specifier: ^1.14.3 + version: 1.14.3 + '@types/jsdom': + specifier: ^21.1.7 + version: 21.1.7 + '@types/node': + specifier: ^24.3.1 + version: 24.10.0 + '@types/react': + specifier: ^19.1.13 + version: 19.2.2 + '@types/yargs': + specifier: ^17.0.33 + version: 17.0.34 + devtools-protocol: + specifier: 0.0.927104 + version: 0.0.927104 + + packages/uiexport: + dependencies: + '@devtoolcss/inliner': + specifier: workspace:* + version: link:../@devtoolcss-inliner + chrome-inspector: + specifier: ^1.0.1 + version: 1.0.1 + highlight.js: + specifier: ^11.11.1 + version: 11.11.1 + js-beautify: + specifier: ^1.15.4 + version: 1.15.4 + devDependencies: + '@types/chrome': + specifier: ^0.1.12 + version: 0.1.27 + chokidar: + specifier: ^4.0.3 + version: 4.0.3 + esbuild: + specifier: 0.25.10 + version: 0.25.10 + packages: + '@acemir/cssom@0.9.22': + resolution: {integrity: sha512-QviHW7uL3M3oQ5b5z+6AqDe+ZzJ3XeLLKNaD+XbuRIMkeAZ/FsL7zIle0V+YR5bllZDL4s1i+NYx8wNGNpnQTg==} + + '@alcalzone/ansi-tokenize@0.2.2': + resolution: {integrity: sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA==} + engines: {node: '>=18'} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@asamuzakjp/css-color@4.0.5': + resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} + + '@asamuzakjp/dom-selector@6.7.4': + resolution: {integrity: sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.15': + resolution: {integrity: sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@devtoolcss/parser@1.0.0': + resolution: {integrity: sha512-kvJhIT7fUjJ/pY0//hESS3LOEin4FI2gc51aD102c7PjbwsWQdboAhU55+QCc8HxEunwEP/zS9M/TdiG3r7yFA==} + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@types/chrome-remote-interface@0.31.14': + resolution: {integrity: sha512-H9hTcLu1y+Ms6GDPXXeGhgxaOSD69yEo674vjJw5EeW1tTwYo8fEkf7A9nWlnO6ArJsS7c41iZeX6mRDQ1LhEw==} + + '@types/chrome@0.1.27': + resolution: {integrity: sha512-pkkCb0Ft8X+Igi751POzT+YqchSxUCtB6s4Gs6ttgSj8qzJga/qlJMgSW1mKxuQTW4i0sTqQbqVtzXDS5AU+4A==} + + '@types/filesystem@0.0.36': + resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} + + '@types/filewriter@0.0.33': + resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} + + '@types/har-format@1.2.16': + resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==} + + '@types/js-beautify@1.14.3': + resolution: {integrity: sha512-FMbQHz+qd9DoGvgLHxeqqVPaNRffpIu5ZjozwV8hf9JAGpIOzuAf4wGbRSo8LNITHqGjmmVjaMggTT5P4v4IHg==} + + '@types/jsdom@21.1.7': + resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} + + '@types/node@24.10.0': + resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} + + '@types/react@19.2.2': + resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.34': + resolution: {integrity: sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-escapes@7.2.0: + resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + auto-bind@5.0.1: + resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chrome-inspector@1.0.1: + resolution: {integrity: sha512-m1yk8jSNtfSdY6Jf0ChPjctUgMLMJPxBcPK7EM5Lk+6fMKeenBLc6z1e2RQgwrehghKXSRNdXAhxu/t+tdIrCg==} + + chrome-launcher@1.2.1: + resolution: {integrity: sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==} + engines: {node: '>=12.13.0'} + hasBin: true + + chrome-remote-interface@0.33.3: + resolution: {integrity: sha512-zNnn0prUL86Teru6UCAZ1yU1XeXljHl3gj7OrfPcarEfU62OUU4IujDPdTDW3dAWwRqN3ZMG/Chhkh2gPL/wiw==} + hasBin: true + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + code-excerpt@4.0.0: + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@2.11.0: + resolution: {integrity: sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + convert-to-spaces@2.0.1: + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@7.0.0: + resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==} + engines: {node: '>= 6'} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + cssstyle@5.3.2: + resolution: {integrity: sha512-zDMqXh8Vs1CdRYZQ2M633m/SFgcjlu8RB8b/1h82i+6vpArF507NSYIWJHGlJaTWoS+imcnctmEz43txhbVkOw==} + engines: {node: '>=20'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + devtools-protocol@0.0.1528500: resolution: {integrity: sha512-zWbI0sZQngmekg5M5t585E03Ih/OaRyH58QR5lVgYZ5bkFyZP9EjcF+41lvl/OAwvo5JYzKOCqf4bwvIyKDOoQ==} + devtools-protocol@0.0.927104: + resolution: {integrity: sha512-5jfffjSuTOv0Lz53wTNNTcCUV8rv7d82AhYcapj28bC2B5tDxEZzVb7k51cNxZP2KHw24QE+sW7ZuSeD9NfMpA==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + es-toolkit@1.41.0: + resolution: {integrity: sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==} + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ink@6.4.0: + resolution: {integrity: sha512-v43isNGrHeFfipbQbwz7/Eg0+aWz3ASEdT/s1Ty2JtyBzR3maE0P77FwkMET+Nzh5KbRL3efLgkT/ZzPFzW3BA==} + engines: {node: '>=20'} + peerDependencies: + '@types/react': '>=19.0.0' + react: '>=19.0.0' + react-devtools-core: ^6.1.2 + peerDependenciesMeta: + '@types/react': + optional: true + react-devtools-core: + optional: true + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-in-ci@2.0.0: + resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} + engines: {node: '>=20'} + hasBin: true + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsdom@27.1.0: + resolution: {integrity: sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + lighthouse-logger@2.0.2: + resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + nwsapi@2.2.22: + resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + + patch-console@2.0.0: + resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss-var-replace@1.0.0: + resolution: {integrity: sha512-Aw8t/L0wmuJMNUbYHl7AfJmQ7pUgLrS0zXz+AR+380QxJ85HA8Gxkg3+HvkWK0RoRKpoErpVhakd0k/aHOlNzw==} + engines: {node: '>=18.0.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-reconciler@0.32.0: + resolution: {integrity: sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.1.0 + + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts-core@7.0.17: + resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tldts@7.0.17: + resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + snapshots: - devtools-protocol@0.0.1528500: {} + '@acemir/cssom@0.9.22': {} - typescript@5.9.3: {} + '@alcalzone/ansi-tokenize@0.2.2': + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@asamuzakjp/css-color@4.0.5': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.2 + + '@asamuzakjp/dom-selector@6.7.4': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.2 + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.15': {} + + '@csstools/css-tokenizer@3.0.4': {} + + '@devtoolcss/parser@1.0.0': {} + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@one-ini/wasm@0.1.1': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@types/chrome-remote-interface@0.31.14': + dependencies: + devtools-protocol: 0.0.927104 + + '@types/chrome@0.1.27': + dependencies: + '@types/filesystem': 0.0.36 + '@types/har-format': 1.2.16 + + '@types/filesystem@0.0.36': + dependencies: + '@types/filewriter': 0.0.33 + + '@types/filewriter@0.0.33': {} + + '@types/har-format@1.2.16': {} + + '@types/js-beautify@1.14.3': {} + + '@types/jsdom@21.1.7': + dependencies: + '@types/node': 24.10.0 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + + '@types/node@24.10.0': + dependencies: + undici-types: 7.16.0 + + '@types/react@19.2.2': + dependencies: + csstype: 3.1.3 + + '@types/tough-cookie@4.0.5': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.34': + dependencies: + '@types/yargs-parser': 21.0.3 + + abbrev@2.0.0: {} + + agent-base@7.1.4: {} + + ansi-escapes@7.2.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + auto-bind@5.0.1: {} + + balanced-match@1.0.2: {} + + balanced-match@2.0.0: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + chalk@5.6.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chrome-inspector@1.0.1: + dependencies: + '@devtoolcss/parser': 1.0.0 + jsdom: 27.1.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + chrome-launcher@1.2.1: + dependencies: + '@types/node': 24.10.0 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 2.0.2 + transitivePeerDependencies: + - supports-color + + chrome-remote-interface@0.33.3: + dependencies: + commander: 2.11.0 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + cli-boxes@3.0.0: {} + + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + + code-excerpt@4.0.0: + dependencies: + convert-to-spaces: 2.0.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@10.0.1: {} + + commander@2.11.0: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + convert-to-spaces@2.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + css-what@7.0.0: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + cssstyle@5.3.2: + dependencies: + '@asamuzakjp/css-color': 4.0.5 + '@csstools/css-syntax-patches-for-csstree': 1.0.15 + css-tree: 3.1.0 + + csstype@3.1.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + data-urls@6.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + devtools-protocol@0.0.1528500: {} + + devtools-protocol@0.0.927104: {} + + eastasianwidth@0.2.0: {} + + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.7.3 + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@6.0.1: {} + + environment@1.1.0: {} + + es-toolkit@1.41.0: {} + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.4.0: {} + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + highlight.js@11.11.1: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + indent-string@5.0.0: {} + + ini@1.3.8: {} + + ink@6.4.0(@types/react@19.2.2)(react@19.2.0): + dependencies: + '@alcalzone/ansi-tokenize': 0.2.2 + ansi-escapes: 7.2.0 + ansi-styles: 6.2.3 + auto-bind: 5.0.1 + chalk: 5.6.2 + cli-boxes: 3.0.0 + cli-cursor: 4.0.0 + cli-truncate: 4.0.0 + code-excerpt: 4.0.0 + es-toolkit: 1.41.0 + indent-string: 5.0.0 + is-in-ci: 2.0.0 + patch-console: 2.0.0 + react: 19.2.0 + react-reconciler: 0.32.0(react@19.2.0) + signal-exit: 3.0.7 + slice-ansi: 7.1.2 + stack-utils: 2.0.6 + string-width: 7.2.0 + type-fest: 4.41.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.2 + ws: 8.18.3 + yoga-layout: 3.2.1 + optionalDependencies: + '@types/react': 19.2.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + is-docker@2.2.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.4.0 + + is-in-ci@2.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.22 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsdom@27.1.0: + dependencies: + '@acemir/cssom': 0.9.22 + '@asamuzakjp/dom-selector': 6.7.4 + cssstyle: 5.3.2 + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + lighthouse-logger@2.0.2: + dependencies: + debug: 4.4.3 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + + lru-cache@10.4.3: {} + + lru-cache@11.2.2: {} + + marky@1.3.0: {} + + mdn-data@2.12.2: {} + + mimic-fn@2.1.0: {} + + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.2: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + nwsapi@2.2.22: {} + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + package-json-from-dist@1.0.1: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parse5@8.0.0: + dependencies: + entities: 6.0.1 + + patch-console@2.0.0: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + postcss-var-replace@1.0.0(postcss@8.5.6): + dependencies: + balanced-match: 2.0.0 + escape-string-regexp: 4.0.0 + postcss: 8.5.6 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proto-list@1.2.4: {} + + punycode@2.3.1: {} + + react-reconciler@0.32.0(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.26.0 + + react@19.2.0: {} + + readdirp@4.1.2: {} + + require-from-string@2.0.2: {} + + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + rrweb-cssom@0.8.0: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.26.0: {} + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + source-map-js@1.2.1: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + symbol-tree@3.2.4: {} + + tldts-core@6.1.86: {} + + tldts-core@7.0.17: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tldts@7.0.17: + dependencies: + tldts-core: 7.0.17 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.17 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + type-fest@4.41.0: {} + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + webidl-conversions@8.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + ws@7.5.10: {} + + ws@8.18.3: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yoga-layout@3.2.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d440abb..abc3af1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,5 @@ packages: - - packages/@devtoolcss-parser + - packages/* onlyBuiltDependencies: - esbuild From da73d9b14d1860fb25629b4894cf043f92761b46 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Sat, 8 Nov 2025 03:00:34 +0800 Subject: [PATCH 03/36] doc(inliner): update README --- packages/@devtoolcss-inliner/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/@devtoolcss-inliner/README.md b/packages/@devtoolcss-inliner/README.md index 9669d5a..fa8f266 100644 --- a/packages/@devtoolcss-inliner/README.md +++ b/packages/@devtoolcss-inliner/README.md @@ -1,5 +1,3 @@ -Shared code about CSS/DOM/Node processign, and basic types & constants. No CDP call, only CDP data. +Provide function `getInlinedComponent` that returns an element tree with inlined styles. The core inlining logic for [cleanclone](../cleanclone/) and [uiexport](../uiexport/). -## TODO - -split to parser and inliner +TODO: explanation on implementation and optimizer. From 5a10af8dd0bd7c4506392c9db9724f6b5cdabb01 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 10 Nov 2025 10:30:38 +0800 Subject: [PATCH 04/36] fix(inliner)!: return root element's style element --- packages/@devtoolcss-inliner/src/inliner.ts | 19 ++++++++++--------- packages/cleanclone/src/crawler.ts | 2 +- packages/uiexport/sidebar.js | 6 ++++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/@devtoolcss-inliner/src/inliner.ts b/packages/@devtoolcss-inliner/src/inliner.ts index 6696cb9..3d05104 100644 --- a/packages/@devtoolcss-inliner/src/inliner.ts +++ b/packages/@devtoolcss-inliner/src/inliner.ts @@ -312,10 +312,12 @@ function inlineStyle( element: Element, // JSDOM or browser DOM cssAttr = "data-css", removeAttr = true, -) { +): { element: Element; rootStyle?: Element } { // clean scripts/styles/links element.querySelectorAll("script, link, style").forEach((el) => el.remove()); + let rootStyle: Element | undefined; + const elements = Array.from(element.querySelectorAll(`[${cssAttr}]`)); elements.push(element); // include root element for (const el of elements) { @@ -356,9 +358,8 @@ function inlineStyle( if (noChildTags.includes(el.tagName) || el.children.length === 0) { // prevent break :empty for those with no children // still may break :nth-child() - - // TODO: fix for root element - el.parentNode.insertBefore(styleEl, el); + if (el === element) rootStyle = styleEl; + else el.parentNode.insertBefore(styleEl, el); } else { el.insertBefore(styleEl, el.children[0]); } @@ -378,6 +379,7 @@ function inlineStyle( } } } + return { element, rootStyle }; } function buildNodeTree( @@ -439,7 +441,7 @@ async function getInlinedComponent( onProgress: (completed: number, total: number) => void = () => {}, onError: (e: any) => void = () => {}, options: InlineOptions = {}, -): Promise { +): Promise<{ element: Element; rootStyle?: Element }> { const { highlightNode = false } = options; let { customScreens, mediaConditions } = options; if (!customScreens) { @@ -486,7 +488,7 @@ async function getInlinedComponent( ) as InspectorElement; if (highlightNode) { await inspectorElement.scrollIntoView(); - await inspector.highlightNode(inspectorElement); + await inspectorElement.highlight(); } for (const optimizer of optimizers) { @@ -534,12 +536,11 @@ async function getInlinedComponent( -1, true, ); - const docRoot = buildNodeTree( + const elementRoot = buildNodeTree( root, inspector.documentImpl.createHTMLDocument(), ) as Element; - inlineStyle(docRoot); - return docRoot; + return inlineStyle(elementRoot); } export { getInlinedComponent }; diff --git a/packages/cleanclone/src/crawler.ts b/packages/cleanclone/src/crawler.ts index bd51ded..ae3793d 100644 --- a/packages/cleanclone/src/crawler.ts +++ b/packages/cleanclone/src/crawler.ts @@ -486,7 +486,7 @@ export class Crawler extends EventEmitter { // @ts-ignore const inspector = await Inspector.fromCDPClient(client); inspector.on("error", this.onError); - const body = await getInlinedComponent( + const { element: body } = await getInlinedComponent( "body", inspector, (completed, total) => { diff --git a/packages/uiexport/sidebar.js b/packages/uiexport/sidebar.js index a6794bb..41bbe26 100644 --- a/packages/uiexport/sidebar.js +++ b/packages/uiexport/sidebar.js @@ -56,13 +56,15 @@ const exportBtn = document.getElementById("exportBtn"); chrome.debugger, target.tabId, ); - const element = await getInlinedComponent( + const { element, rootStyle } = await getInlinedComponent( selector, inspector, updateProgress, ); - iframe.contentDocument.body.innerHTML = element.outerHTML; + iframe.contentDocument.body.innerHTML = rootStyle + ? rootStyle.outerHTML + "\n" + element.outerHTML + : element.outerHTML; await chrome.debugger.detach(target); } catch (e) { From b4ddadd3b9b4d3583a853804b4cc628c1b4cd823 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 10 Nov 2025 11:48:29 +0800 Subject: [PATCH 05/36] fix(inliner): await to let inspector update before freezing tree --- packages/@devtoolcss-inliner/src/inliner.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/@devtoolcss-inliner/src/inliner.ts b/packages/@devtoolcss-inliner/src/inliner.ts index 3d05104..66c2b04 100644 --- a/packages/@devtoolcss-inliner/src/inliner.ts +++ b/packages/@devtoolcss-inliner/src/inliner.ts @@ -427,7 +427,6 @@ function buildNodeTree( function getFreezedCdpTree(cdpNode: CDPNode): CDPNodeWithId { const nodeWithId = initIdCSS({ ...cdpNode }); - nodeWithId.css = []; nodeWithId.children = []; for (const child of cdpNode.children || []) { nodeWithId.children.push(getFreezedCdpTree(child)); @@ -457,6 +456,15 @@ async function getInlinedComponent( new AriaExpandedOptimizer(), new PrunePsuedoElementOptimizer(), ]; + + const warnChildNodeRemoved = (params) => { + onError( + `Warning: DOM.childNodeRemoved triggered during tree freezed: ${JSON.stringify( + params, + )}`, + ); + }; + const freezedRoots = []; for (let i = 0; i < customScreens.length; ++i) { if (customScreens[i]) { @@ -466,6 +474,12 @@ async function getInlinedComponent( for (const optimizer of optimizers) { await optimizer.beforeTraverse(root); } + + // need to await to transfer execution to inspector to update the node info + // and wait DOM change after changing device metrics + await new Promise((resolve) => setTimeout(resolve, 500)); + + inspector.on("DOM.childNodeRemoved", warnChildNodeRemoved); const freezedCdpRoot = getFreezedCdpTree(root._cdpNode); let total = 0; @@ -514,11 +528,13 @@ async function getInlinedComponent( -1, false, ); + inspector.off("DOM.childNodeRemoved", warnChildNodeRemoved); if (highlightNode) { await inspector.hideHighlight(); } freezedRoots.push(freezedCdpRoot); } + const root = mergeTrees(freezedRoots, customScreens.length); // after all node.id are set await traverse( From f2b09ad890c573c4b25f80e641dc1d8d6dba884a Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 10 Nov 2025 14:57:44 +0800 Subject: [PATCH 06/36] fix(parser): pseudoElement array --- packages/@devtoolcss-parser/src/css_parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@devtoolcss-parser/src/css_parser.ts b/packages/@devtoolcss-parser/src/css_parser.ts index 33517b3..9fe8bfb 100644 --- a/packages/@devtoolcss-parser/src/css_parser.ts +++ b/packages/@devtoolcss-parser/src/css_parser.ts @@ -282,7 +282,7 @@ export function parseGetMatchedStylesForNodeResponse( appliedProperties, false, ); - parsed.pseudoElements[match.pseudoType] = parsedRules; + parsed.pseudoElements.push(...parsedRules); } } From 5380f082f2e2d0b9c59e06942468ee0f31255d77 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 10 Nov 2025 14:58:58 +0800 Subject: [PATCH 07/36] fix(inliner) prune ::before/::after only when content is normal --- .../@devtoolcss-inliner/src/optimizers/PrunePseudoElement.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/@devtoolcss-inliner/src/optimizers/PrunePseudoElement.ts b/packages/@devtoolcss-inliner/src/optimizers/PrunePseudoElement.ts index bfb5450..981b74c 100644 --- a/packages/@devtoolcss-inliner/src/optimizers/PrunePseudoElement.ts +++ b/packages/@devtoolcss-inliner/src/optimizers/PrunePseudoElement.ts @@ -25,10 +25,7 @@ export class PrunePsuedoElementOptimizer implements Optimizer { if (suffix.endsWith("before") || suffix.endsWith("after")) { let hasIneffectiveContent = true; for (const prop of properties) { - if ( - prop.name === "content" && - !["normal", '""', "''"].includes(prop.value) - ) { + if (prop.name === "content" && prop.value !== "normal") { hasIneffectiveContent = false; } } From 1ddde7dc0bef4bb9710193c7b2c39b9bb9fe87a4 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 10 Nov 2025 15:01:20 +0800 Subject: [PATCH 08/36] refactor: use workspace's parser --- packages/@devtoolcss-inliner/src/inliner.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/@devtoolcss-inliner/src/inliner.ts b/packages/@devtoolcss-inliner/src/inliner.ts index 66c2b04..48daf4f 100644 --- a/packages/@devtoolcss-inliner/src/inliner.ts +++ b/packages/@devtoolcss-inliner/src/inliner.ts @@ -11,8 +11,13 @@ import { CDPNodeType, CDPNode, InspectorElement, + GetMatchedStylesForNodeResponse, } from "chrome-inspector"; -import { iterateParsedCSS, traverse } from "@devtoolcss/parser"; +import { + iterateParsedCSS, + parseGetMatchedStylesForNodeResponse, + traverse, +} from "@devtoolcss/parser"; import { forciblePseudoClasses } from "./constants.js"; import { AriaExpandedOptimizer, @@ -518,8 +523,12 @@ async function getInlinedComponent( await optimizer.afterForcePseudo(inspectorElement); } - freezedNode.css[i] = await inspectorElement.getMatchedStyles({ - parseOptions: { removeUnusedVar: true }, + const resp = (await inspectorElement.getMatchedStyles({ + raw: true, + })) as GetMatchedStylesForNodeResponse; + // use parser in workspace for easier debugging + freezedNode.css[i] = parseGetMatchedStylesForNodeResponse(resp, { + removeUnusedVar: true, }); await inspector.forcePseudoState(inspectorElement, []); // reset forced pseudo states onProgress(++completed, total); From 5a9b7baad4d2586139517d5519495754cd4ef8e2 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 10 Nov 2025 15:03:45 +0800 Subject: [PATCH 09/36] fix(inliner): eliminate multiple pseudo-elements in replaced selectors postcssVarReplace may have invalid multiple pseudoElements, preserve the first one, which is the element's --- packages/@devtoolcss-inliner/src/inliner.ts | 41 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/@devtoolcss-inliner/src/inliner.ts b/packages/@devtoolcss-inliner/src/inliner.ts index 48daf4f..ece0e66 100644 --- a/packages/@devtoolcss-inliner/src/inliner.ts +++ b/packages/@devtoolcss-inliner/src/inliner.ts @@ -60,7 +60,44 @@ function getRewrittenSelectors( return [...rewrittenSelectors]; } -// TODO: check why outer not replaced +/* + * Fix selectors with multiple pseudo-elements by keeping only the first one + * Example: + * #ab::before { + * --v: ''; + * } + * + * #ab::after{ + * content: var(--v); + * } + * + * becomes + * #ab::after{ + * content: undefined; + * } + * + * #ab::after::before{ + * content: ''; + * } + */ +function fixSelector(selector: string): string { + const parsedSelector = CSSwhat.parse(selector)[0]; + let pseudoElements: number[] = []; + for (let i = 0; i < parsedSelector.length; i++) { + if (parsedSelector[i].type === "pseudo-element") { + pseudoElements.push(i); + } + } + if (pseudoElements.length > 1) { + for (let j = pseudoElements.length - 1; j > 0; j--) { + parsedSelector.splice(pseudoElements[j], 1); + } + return CSSwhat.stringify([parsedSelector]); + } else { + return selector; + } +} + function replaceVariables( rules: ParsedCSSRulesObjValue, ): ParsedCSSRulesObjValue { @@ -69,7 +106,7 @@ function replaceVariables( const { root } = postcss([postcssVarReplace()]).process(styleSheet); const replaced: ParsedCSSRulesObjValue = {}; root.walkRules((rule) => { - const selector = rule.selector; + const selector = fixSelector(rule.selector); if (!replaced[selector]) { replaced[selector] = {}; } From d1598256edd032cac8a3f4af92e4a3b7428646a0 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 10 Nov 2025 19:31:03 +0800 Subject: [PATCH 10/36] chore(parser): bump to 1.0.1 --- packages/@devtoolcss-parser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@devtoolcss-parser/package.json b/packages/@devtoolcss-parser/package.json index acbe04f..3989c57 100644 --- a/packages/@devtoolcss-parser/package.json +++ b/packages/@devtoolcss-parser/package.json @@ -1,6 +1,6 @@ { "name": "@devtoolcss/parser", - "version": "1.0.0", + "version": "1.0.1", "description": "", "license": "MIT", "files": [ From 24e1f24299b4cdba6c04d01f5778d170fc2e498d Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 10 Nov 2025 19:42:12 +0800 Subject: [PATCH 11/36] chore: bump chrome-inspector to 1.0.2 --- packages/@devtoolcss-inliner/package.json | 2 +- packages/cleanclone/package.json | 2 +- packages/uiexport/package.json | 2 +- pnpm-lock.yaml | 26 +++++++++++------------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/@devtoolcss-inliner/package.json b/packages/@devtoolcss-inliner/package.json index 62ef778..fe1b0f9 100644 --- a/packages/@devtoolcss-inliner/package.json +++ b/packages/@devtoolcss-inliner/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@devtoolcss/parser": "workspace:*", - "chrome-inspector": "^1.0.1", + "chrome-inspector": "^1.0.2", "css-what": "^7.0.0", "postcss": "^8.5.6", "postcss-var-replace": "^1.0.0" diff --git a/packages/cleanclone/package.json b/packages/cleanclone/package.json index c0b4e4e..a04a534 100644 --- a/packages/cleanclone/package.json +++ b/packages/cleanclone/package.json @@ -27,7 +27,7 @@ "dependencies": { "@devtoolcss/inliner": "workspace:*", "chalk": "^5.6.2", - "chrome-inspector": "^1.0.1", + "chrome-inspector": "^1.0.2", "chrome-launcher": "^1.2.1", "chrome-remote-interface": "^0.33.3", "escape-html": "^1.0.3", diff --git a/packages/uiexport/package.json b/packages/uiexport/package.json index 16d8ab4..66a7ac5 100644 --- a/packages/uiexport/package.json +++ b/packages/uiexport/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@devtoolcss/inliner": "workspace:*", - "chrome-inspector": "^1.0.1", + "chrome-inspector": "^1.0.2", "highlight.js": "^11.11.1", "js-beautify": "^1.15.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f370938..58a19eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: workspace:* version: link:../@devtoolcss-parser chrome-inspector: - specifier: ^1.0.1 - version: 1.0.1 + specifier: ^1.0.2 + version: 1.0.2 css-what: specifier: ^7.0.0 version: 7.0.0 @@ -49,8 +49,8 @@ importers: specifier: ^5.6.2 version: 5.6.2 chrome-inspector: - specifier: ^1.0.1 - version: 1.0.1 + specifier: ^1.0.2 + version: 1.0.2 chrome-launcher: specifier: ^1.2.1 version: 1.2.1 @@ -107,8 +107,8 @@ importers: specifier: workspace:* version: link:../@devtoolcss-inliner chrome-inspector: - specifier: ^1.0.1 - version: 1.0.1 + specifier: ^1.0.2 + version: 1.0.2 highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -179,8 +179,8 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@devtoolcss/parser@1.0.0': - resolution: {integrity: sha512-kvJhIT7fUjJ/pY0//hESS3LOEin4FI2gc51aD102c7PjbwsWQdboAhU55+QCc8HxEunwEP/zS9M/TdiG3r7yFA==} + '@devtoolcss/parser@1.0.1': + resolution: {integrity: sha512-XtSIgT9OVJLL4jpJ2bk+d2TXk87P8oDo7IdV4BuyQIBa+xvj73BUzOn6sl9VPCk1d146indN3pcjYjg9SSBIbQ==} '@esbuild/aix-ppc64@0.25.10': resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} @@ -437,8 +437,8 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chrome-inspector@1.0.1: - resolution: {integrity: sha512-m1yk8jSNtfSdY6Jf0ChPjctUgMLMJPxBcPK7EM5Lk+6fMKeenBLc6z1e2RQgwrehghKXSRNdXAhxu/t+tdIrCg==} + chrome-inspector@1.0.2: + resolution: {integrity: sha512-OGPtxtQseM8rcBUXePR9cKtC2UdfJlTG7krZ/UbBWGBrPedGf8x21qZeKIqJAWNzGpspM+zzBVktfEFpUFv5aA==} chrome-launcher@1.2.1: resolution: {integrity: sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==} @@ -1090,7 +1090,7 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@devtoolcss/parser@1.0.0': {} + '@devtoolcss/parser@1.0.1': {} '@esbuild/aix-ppc64@0.25.10': optional: true @@ -1263,9 +1263,9 @@ snapshots: dependencies: readdirp: 4.1.2 - chrome-inspector@1.0.1: + chrome-inspector@1.0.2: dependencies: - '@devtoolcss/parser': 1.0.0 + '@devtoolcss/parser': 1.0.1 jsdom: 27.1.0 transitivePeerDependencies: - bufferutil From 657123f1512f9ea359020945c0523b7cfe81d612 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 10 Nov 2025 19:47:34 +0800 Subject: [PATCH 12/36] chore: bump to 1.0.1 --- packages/@devtoolcss-inliner/package.json | 2 +- packages/cleanclone/package.json | 2 +- packages/uiexport/manifest.json | 2 +- packages/uiexport/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@devtoolcss-inliner/package.json b/packages/@devtoolcss-inliner/package.json index fe1b0f9..efb7b90 100644 --- a/packages/@devtoolcss-inliner/package.json +++ b/packages/@devtoolcss-inliner/package.json @@ -1,6 +1,6 @@ { "name": "@devtoolcss/inliner", - "version": "0.0.0", + "version": "1.0.1", "description": "A CSS inliner library powered by Chrome DevTools Protocol.", "license": "MIT", "files": [ diff --git a/packages/cleanclone/package.json b/packages/cleanclone/package.json index a04a534..eb0db35 100644 --- a/packages/cleanclone/package.json +++ b/packages/cleanclone/package.json @@ -1,6 +1,6 @@ { "name": "cleanclone", - "version": "0.0.8", + "version": "1.0.1", "description": "Inline DevTool-parsed CSS for any website and crawl it on the fly.", "license": "MIT", "files": [ diff --git a/packages/uiexport/manifest.json b/packages/uiexport/manifest.json index caf609b..b597fa3 100644 --- a/packages/uiexport/manifest.json +++ b/packages/uiexport/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "UI Export", - "version": "0.0.8", + "version": "1.0.1", "description": "Export any components with CSS inlined.", "permissions": ["debugger"], "devtools_page": "devtools.html", diff --git a/packages/uiexport/package.json b/packages/uiexport/package.json index 66a7ac5..39979f8 100644 --- a/packages/uiexport/package.json +++ b/packages/uiexport/package.json @@ -1,6 +1,6 @@ { "name": "uiexport", - "version": "0.0.8", + "version": "1.0.1", "private": true, "description": "", "author": "", From eb68b6b948088ea58c7576c443da6c7c25065ff5 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 10 Nov 2025 22:36:55 +0800 Subject: [PATCH 13/36] ci: set NODE_ENV to produciton --- .github/workflows/publish-all.yml | 2 ++ .github/workflows/publish-single.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/publish-all.yml b/.github/workflows/publish-all.yml index 9ff54a3..4945120 100644 --- a/.github/workflows/publish-all.yml +++ b/.github/workflows/publish-all.yml @@ -8,6 +8,8 @@ jobs: permissions: contents: write id-token: write + env: + NODE_ENV: production steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-single.yml b/.github/workflows/publish-single.yml index a6570f7..e5df99b 100644 --- a/.github/workflows/publish-single.yml +++ b/.github/workflows/publish-single.yml @@ -13,6 +13,8 @@ jobs: permissions: contents: write id-token: write + env: + NODE_ENV: production steps: - uses: actions/checkout@v5 From 3cf49bbe9e29ab3d95561dcdb03ee9ee4765517c Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Tue, 11 Nov 2025 00:15:20 +0800 Subject: [PATCH 14/36] chore: bump chrome-inspector to 1.0.3 --- packages/@devtoolcss-inliner/package.json | 2 +- packages/cleanclone/package.json | 2 +- packages/uiexport/package.json | 2 +- pnpm-lock.yaml | 18 +++++++++--------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/@devtoolcss-inliner/package.json b/packages/@devtoolcss-inliner/package.json index efb7b90..f4fd96d 100644 --- a/packages/@devtoolcss-inliner/package.json +++ b/packages/@devtoolcss-inliner/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@devtoolcss/parser": "workspace:*", - "chrome-inspector": "^1.0.2", + "chrome-inspector": "^1.0.3", "css-what": "^7.0.0", "postcss": "^8.5.6", "postcss-var-replace": "^1.0.0" diff --git a/packages/cleanclone/package.json b/packages/cleanclone/package.json index eb0db35..8b55376 100644 --- a/packages/cleanclone/package.json +++ b/packages/cleanclone/package.json @@ -27,7 +27,7 @@ "dependencies": { "@devtoolcss/inliner": "workspace:*", "chalk": "^5.6.2", - "chrome-inspector": "^1.0.2", + "chrome-inspector": "^1.0.3", "chrome-launcher": "^1.2.1", "chrome-remote-interface": "^0.33.3", "escape-html": "^1.0.3", diff --git a/packages/uiexport/package.json b/packages/uiexport/package.json index 39979f8..636188e 100644 --- a/packages/uiexport/package.json +++ b/packages/uiexport/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@devtoolcss/inliner": "workspace:*", - "chrome-inspector": "^1.0.2", + "chrome-inspector": "^1.0.3", "highlight.js": "^11.11.1", "js-beautify": "^1.15.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58a19eb..4a90450 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: workspace:* version: link:../@devtoolcss-parser chrome-inspector: - specifier: ^1.0.2 - version: 1.0.2 + specifier: ^1.0.3 + version: 1.0.3 css-what: specifier: ^7.0.0 version: 7.0.0 @@ -49,8 +49,8 @@ importers: specifier: ^5.6.2 version: 5.6.2 chrome-inspector: - specifier: ^1.0.2 - version: 1.0.2 + specifier: ^1.0.3 + version: 1.0.3 chrome-launcher: specifier: ^1.2.1 version: 1.2.1 @@ -107,8 +107,8 @@ importers: specifier: workspace:* version: link:../@devtoolcss-inliner chrome-inspector: - specifier: ^1.0.2 - version: 1.0.2 + specifier: ^1.0.3 + version: 1.0.3 highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -437,8 +437,8 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chrome-inspector@1.0.2: - resolution: {integrity: sha512-OGPtxtQseM8rcBUXePR9cKtC2UdfJlTG7krZ/UbBWGBrPedGf8x21qZeKIqJAWNzGpspM+zzBVktfEFpUFv5aA==} + chrome-inspector@1.0.3: + resolution: {integrity: sha512-rF2YrgaCzZNOimM91ESna+4+dTHBMJlDViDUvkwVHsEOxZzrPoeZ4mldP5zOZYFhWpgDrDnIMUlycEKsbq56Ag==} chrome-launcher@1.2.1: resolution: {integrity: sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==} @@ -1263,7 +1263,7 @@ snapshots: dependencies: readdirp: 4.1.2 - chrome-inspector@1.0.2: + chrome-inspector@1.0.3: dependencies: '@devtoolcss/parser': 1.0.1 jsdom: 27.1.0 From 21bef47ca6bbd389ae42cd1b3c4173e965fde1d7 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Wed, 12 Nov 2025 14:29:45 +0800 Subject: [PATCH 15/36] feat(mcp): basic extension to node ws connection --- packages/@devtoolcss-mcp/README.md | 19 ++++ packages/@devtoolcss-mcp/background.js | 141 +++++++++++++++++++++++++ packages/@devtoolcss-mcp/manifest.json | 15 +++ packages/@devtoolcss-mcp/package.json | 17 +++ packages/@devtoolcss-mcp/popup.html | 105 ++++++++++++++++++ packages/@devtoolcss-mcp/popup.js | 42 ++++++++ packages/@devtoolcss-mcp/ws-server.js | 108 +++++++++++++++++++ pnpm-lock.yaml | 10 ++ pnpm-workspace.yaml | 4 +- 9 files changed, 459 insertions(+), 2 deletions(-) create mode 100644 packages/@devtoolcss-mcp/README.md create mode 100644 packages/@devtoolcss-mcp/background.js create mode 100644 packages/@devtoolcss-mcp/manifest.json create mode 100644 packages/@devtoolcss-mcp/package.json create mode 100644 packages/@devtoolcss-mcp/popup.html create mode 100644 packages/@devtoolcss-mcp/popup.js create mode 100644 packages/@devtoolcss-mcp/ws-server.js diff --git a/packages/@devtoolcss-mcp/README.md b/packages/@devtoolcss-mcp/README.md new file mode 100644 index 0000000..70e25bc --- /dev/null +++ b/packages/@devtoolcss-mcp/README.md @@ -0,0 +1,19 @@ +## mcp + +chrome extension (ws polling) + stateful inspector + lazy init + detach handle (exception & tab toggling dashboard) + +### feature + +get handle: + +- querySelectorAll + +handle methods: + +- getMatchedStyles +- getComputedStyle (by attr array) +- querySelectorAll +- parent +- children +- attributes +- outerHTML (with depth/line lenght control) diff --git a/packages/@devtoolcss-mcp/background.js b/packages/@devtoolcss-mcp/background.js new file mode 100644 index 0000000..1bd5d00 --- /dev/null +++ b/packages/@devtoolcss-mcp/background.js @@ -0,0 +1,141 @@ +/// + +// Keep service worker alive +// https://stackoverflow.com/a/66618269 +const KEEPALIVE_INTERVAL = 20000; // 20 seconds +const keepAlive = () => + setInterval(chrome.runtime.getPlatformInfo, KEEPALIVE_INTERVAL); +chrome.runtime.onStartup.addListener(keepAlive); +keepAlive(); + +// WebSocket connection state +let ws = null; +let settings = { + host: "127.0.0.1", + port: 9333, + pollingEnabled: true, + pollingInterval: 2000, // 2 seconds +}; + +// Handle messages from server +function handleMessage(data) { + console.log("[Handler] Processing message:", data); + // TODO: Implement your message handling logic here +} + +// Listen for settings changes +chrome.storage.onChanged.addListener(async (changes, areaName) => { + await loadSettings(); + if (changes.pollingEnabled.newValue && !changes.pollingEnabled.oldValue) { + pollAndConnect(); + } +}); + +// Load settings from storage +async function loadSettings() { + const stored = await chrome.storage.sync.get([ + "host", + "port", + "pollingEnabled", + "pollingInterval", + ]); + settings = { + host: stored.host || "127.0.0.1", + port: stored.port || 9333, + pollingEnabled: stored.pollingEnabled !== false, // default true + pollingInterval: stored.pollingInterval || 2000, + }; + console.log("[Settings] Loaded:", settings); +} + +// Check if server is available using HTTP polling (silent on failure) +async function checkServerAvailability() { + if (!settings.pollingEnabled) { + return false; + } + + const healthUrl = `http://${settings.host}:${settings.port}/health`; + + try { + const response = await fetch(healthUrl, { + method: "GET", + signal: AbortSignal.timeout(5000), + }); + + return response.ok; + } catch (error) { + // Silent failure - this is expected when server is not available + return false; + } +} + +// Connect to WebSocket server +function connectWebSocket() { + const wsUrl = `ws://${settings.host}:${settings.port}`; + console.log(`[WS] Connecting to ${wsUrl}...`); + + ws = new WebSocket(wsUrl); + + const cleanUp = () => { + ws.close(); + ws = null; // remove the only reference, effectively cleanup + }; + + ws.onopen = () => { + console.log("[WS] Connected successfully"); + // Send a test message + ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() })); + }; + + ws.onmessage = (event) => { + console.log("[WS] Message received:", event.data); + try { + const data = JSON.parse(event.data); + handleMessage(data); + } catch (e) { + console.error("[WS] Failed to parse message:", e); + } + }; + + ws.onerror = (event) => { + if (ws.readyState === WebSocket.CLOSED) { + // Connection lost, will be handled by onclose + return; + } + console.error("[WS] Error occurred:", event); + }; + + ws.onclose = () => { + console.log("[WS] Connection closed"); + cleanUp(); + pollAndConnect(); + }; +} + +// Poll and connect +async function pollAndConnect() { + while (settings.pollingEnabled) { + const available = await checkServerAvailability(); + + if (available) { + console.log("[Poll] Server is available, connecting WebSocket..."); + connectWebSocket(); + return; + } + // Server not available, wait until next poll + await new Promise((r) => setTimeout(r, settings.pollingInterval)); + } +} + +// Handle extension lifecycle +chrome.runtime.onStartup.addListener(() => { + console.log("[Lifecycle] Extension started"); + keepAlive(); + loadSettings().then(() => pollAndConnect()); +}); + +chrome.runtime.onInstalled.addListener(() => { + console.log("[Lifecycle] Extension installed/updated"); + keepAlive(); + loadSettings().then(() => pollAndConnect()); +}); diff --git a/packages/@devtoolcss-mcp/manifest.json b/packages/@devtoolcss-mcp/manifest.json new file mode 100644 index 0000000..b917c89 --- /dev/null +++ b/packages/@devtoolcss-mcp/manifest.json @@ -0,0 +1,15 @@ +{ + "name": "DevtoolCSS MCP", + "version": "0.0.0", + "manifest_version": 3, + "description": "Chrome extension with WebSocket polling to MCP server", + "background": { + "service_worker": "background.js" + }, + "permissions": ["storage"], + "action": { + "default_popup": "popup.html", + "default_title": "DevtoolCSS MCP Settings" + }, + "host_permissions": ["http://127.0.0.1/*", "ws://127.0.0.1/*"] +} diff --git a/packages/@devtoolcss-mcp/package.json b/packages/@devtoolcss-mcp/package.json new file mode 100644 index 0000000..7401c4e --- /dev/null +++ b/packages/@devtoolcss-mcp/package.json @@ -0,0 +1,17 @@ +{ + "name": "@devtoolcss/mcp", + "version": "0.0.0", + "private": true, + "description": "Chrome extension with WebSocket polling to MCP server", + "main": "ws-server.js", + "scripts": { + "server": "node ws-server.js", + "dev": "node ws-server.js" + }, + "dependencies": { + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/chrome": "^0.1.27" + } +} diff --git a/packages/@devtoolcss-mcp/popup.html b/packages/@devtoolcss-mcp/popup.html new file mode 100644 index 0000000..cba8594 --- /dev/null +++ b/packages/@devtoolcss-mcp/popup.html @@ -0,0 +1,105 @@ + + + + + DevtoolCSS MCP Settings + + + +

MCP Server Settings

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
Settings saved!
+ + + + diff --git a/packages/@devtoolcss-mcp/popup.js b/packages/@devtoolcss-mcp/popup.js new file mode 100644 index 0000000..8504beb --- /dev/null +++ b/packages/@devtoolcss-mcp/popup.js @@ -0,0 +1,42 @@ +// Load current settings +async function loadSettings() { + const settings = await chrome.storage.sync.get([ + "host", + "port", + "pollingEnabled", + "pollingInterval", + ]); + + document.getElementById("host").value = settings.host || "127.0.0.1"; + document.getElementById("port").value = settings.port || 9333; + document.getElementById("pollingInterval").value = + settings.pollingInterval || 2000; + document.getElementById("pollingEnabled").checked = + settings.pollingEnabled !== false; +} + +// Save settings +async function saveSettings() { + const settings = { + host: document.getElementById("host").value.trim() || "127.0.0.1", + port: parseInt(document.getElementById("port").value) || 9333, + pollingInterval: + parseInt(document.getElementById("pollingInterval").value) || 2000, + pollingEnabled: document.getElementById("pollingEnabled").checked, + }; + + await chrome.storage.sync.set(settings); + + // Show success message + const status = document.getElementById("status"); + status.classList.add("success"); + setTimeout(() => { + status.classList.remove("success"); + }, 2000); +} + +// Event listeners +document.getElementById("save").addEventListener("click", saveSettings); + +// Load settings on popup open +loadSettings(); diff --git a/packages/@devtoolcss-mcp/ws-server.js b/packages/@devtoolcss-mcp/ws-server.js new file mode 100644 index 0000000..471dd02 --- /dev/null +++ b/packages/@devtoolcss-mcp/ws-server.js @@ -0,0 +1,108 @@ +#!/usr/bin/env node +import http from "http"; +import WebSocket from "ws"; + +const PORT = process.env.PORT || 9333; + +// Create HTTP server +const server = http.createServer((req, res) => { + // Health check endpoint + if (req.url === "/health" && req.method === "GET") { + res.writeHead(200, { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }); + res.end( + JSON.stringify({ + status: "ok", + timestamp: Date.now(), + service: "DevtoolCSS MCP Server", + }), + ); + return; + } + + // Handle CORS preflight + if (req.method === "OPTIONS") { + res.writeHead(204, { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }); + res.end(); + return; + } + + res.writeHead(404); + res.end("Not Found"); +}); + +// Create WebSocket server +const wss = new WebSocket.Server({ server }); + +console.log(`[Server] HTTP + WebSocket server listening on:`); +console.log(` - HTTP: http://127.0.0.1:${PORT}/health`); +console.log(` - WebSocket: ws://127.0.0.1:${PORT}`); + +const handleMessage = (message) => { + try { + const data = JSON.parse(message.toString()); + + // Handle different message types + if (data.type === "ping") { + ws.send( + JSON.stringify({ + type: "pong", + timestamp: Date.now(), + original: data, + }), + ); + } else { + // Echo back with a response + ws.send( + JSON.stringify({ + type: "response", + original: data, + timestamp: Date.now(), + }), + ); + } + } catch (e) { + console.error("[WS] Failed to parse message:", e); + } +}; + +wss.on("connection", (ws) => { + console.log("[WS] Client connected"); + + ws.on("message", (message) => { + console.log("[WS] Received:", message.toString()); + handleMessage(message); + }); + + ws.on("close", () => { + console.log("[WS] Client disconnected"); + }); + + ws.on("error", (error) => { + console.error("[WS] Error:", error); + }); + + // Send welcome message + ws.send( + JSON.stringify({ + type: "welcome", + message: "Connected to DevtoolCSS MCP Server", + timestamp: Date.now(), + }), + ); +}); + +wss.on("error", (error) => { + console.error("[WS] Server error:", error); +}); + +// Start server +server.listen(PORT, "127.0.0.1", () => { + console.log("[Server] Ready to accept connections"); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a90450..6e18ef6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,16 @@ importers: specifier: ^0.0.1528500 version: 0.0.1528500 + packages/@devtoolcss-mcp: + dependencies: + ws: + specifier: ^8.18.3 + version: 8.18.3 + devDependencies: + '@types/chrome': + specifier: ^0.1.27 + version: 0.1.27 + packages/@devtoolcss-parser: devDependencies: devtools-protocol: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index abc3af1..8a4444d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,5 @@ packages: - - packages/* - + - packages/@devtoolcss-mcp + onlyBuiltDependencies: - esbuild From ec1b4d0f22e5ee2e53258733ae756d46026438ca Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Thu, 13 Nov 2025 22:04:26 +0800 Subject: [PATCH 16/36] ci(uiexport): fix rebuild command in watch --- packages/uiexport/scripts/watch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uiexport/scripts/watch.js b/packages/uiexport/scripts/watch.js index 129ab15..1011ce9 100644 --- a/packages/uiexport/scripts/watch.js +++ b/packages/uiexport/scripts/watch.js @@ -25,7 +25,7 @@ chokidar.watch(".").on("change", (path) => { // any js change could affect the bundle console.log(`Rebuilding due to change in ${path}`); exec( - "NODE_ENV=development node esbuild.config.js", + "NODE_ENV=development node scripts/esbuild.config.js", (error, stdout, stderr) => { if (error) { console.error(`Error during build: ${stderr}`); From d23df9d36251c5a630f3a40b9110f522eaa557b8 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Thu, 13 Nov 2025 22:05:38 +0800 Subject: [PATCH 17/36] feat(mcp): setup basic archtecture TODO: handle errors, ex: cannot access chrome:// --- packages/@devtoolcss-mcp/BiWeakNodeMap.js | 37 +++ packages/@devtoolcss-mcp/background.js | 115 ++++++-- packages/@devtoolcss-mcp/manifest.json | 5 +- .../@devtoolcss-mcp/offscreen_inspectors.html | 6 + .../@devtoolcss-mcp/offscreen_inspectors.js | 126 ++++++++ packages/@devtoolcss-mcp/package.json | 13 +- packages/@devtoolcss-mcp/scripts/copy.js | 30 ++ .../@devtoolcss-mcp/scripts/esbuild.config.js | 17 ++ .../@devtoolcss-mcp/scripts/sync-manifest.js | 33 +++ packages/@devtoolcss-mcp/scripts/watch.js | 51 ++++ packages/@devtoolcss-mcp/ws-server.js | 65 ++-- pnpm-lock.yaml | 277 ++++++++++++++++++ 12 files changed, 719 insertions(+), 56 deletions(-) create mode 100644 packages/@devtoolcss-mcp/BiWeakNodeMap.js create mode 100644 packages/@devtoolcss-mcp/offscreen_inspectors.html create mode 100644 packages/@devtoolcss-mcp/offscreen_inspectors.js create mode 100644 packages/@devtoolcss-mcp/scripts/copy.js create mode 100644 packages/@devtoolcss-mcp/scripts/esbuild.config.js create mode 100644 packages/@devtoolcss-mcp/scripts/sync-manifest.js create mode 100644 packages/@devtoolcss-mcp/scripts/watch.js diff --git a/packages/@devtoolcss-mcp/BiWeakNodeMap.js b/packages/@devtoolcss-mcp/BiWeakNodeMap.js new file mode 100644 index 0000000..e9f44c4 --- /dev/null +++ b/packages/@devtoolcss-mcp/BiWeakNodeMap.js @@ -0,0 +1,37 @@ +export class BiWeakNodeMap { + constructor() { + this.idCnt = 0; + this._idToRef = new Map(); // id -> WeakRef(node) + this._nodeToId = new WeakMap(); // node -> id + } + + set(node) { + const id = `${node.nodeName.toLowerCase()}-${++this.idCnt}`; + this._idToRef.set(id, new WeakRef(node)); + this._nodeToId.set(node, id); + return id; + } + + getNode(id) { + const ref = this._idToRef.get(id); + if (!ref) return undefined; + const node = ref.deref(); + if (!node) { + // Node GC'd, clean up stale entry + this._idToRef.delete(id); + } + return node; + } + + getId(node) { + return this._nodeToId.get(node); + } + + cleanUp() { + for (const [id, ref] of this._idToRef.entries()) { + if (ref.deref() === undefined) { + this._idToRef.delete(id); + } + } + } +} diff --git a/packages/@devtoolcss-mcp/background.js b/packages/@devtoolcss-mcp/background.js index 1bd5d00..6b8e11b 100644 --- a/packages/@devtoolcss-mcp/background.js +++ b/packages/@devtoolcss-mcp/background.js @@ -17,18 +17,87 @@ let settings = { pollingInterval: 2000, // 2 seconds }; -// Handle messages from server -function handleMessage(data) { - console.log("[Handler] Processing message:", data); - // TODO: Implement your message handling logic here +async function getActiveTabId() { + const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); + const activeTab = tabs[0]; + return activeTab.id; } -// Listen for settings changes -chrome.storage.onChanged.addListener(async (changes, areaName) => { - await loadSettings(); - if (changes.pollingEnabled.newValue && !changes.pollingEnabled.oldValue) { - pollAndConnect(); +chrome.tabs.onRemoved.addListener((tabId, removeInfo) => { + console.log(`Tab ${tabId} closed`); + // Clean up any resources related to this tab + chrome.runtime.sendMessage({ + event: "TAB_CLOSED", + tabId: tabId, + }); +}); + +chrome.debugger.onEvent.addListener((source, method, params) => { + // Forward debugger events to offscreen inspector + chrome.runtime.sendMessage({ + event: "DEBUGGER_EVENT", + source, + method, + params, + }); +}); + +// Handle debugger commands from offscreen +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + switch (msg.type) { + case "DEBUGGER_SEND_COMMAND": + try { + chrome.debugger.sendCommand( + msg.target, + msg.method, + msg.params, + (result) => { + sendResponse({ result }); + }, + ); + } catch (error) { + sendResponse({ error: error.message }); + } + break; + case "DEBUGGER_ATTACH": + try { + chrome.debugger.attach(msg.target, "1.3", () => { + sendResponse({ success: true }); + }); + } catch (error) { + sendResponse({ error: error.message }); + } + break; + case "DEBUGGER_DETACH": + try { + chrome.debugger.detach(msg.target, () => { + sendResponse({ success: true }); + }); + } catch (error) { + sendResponse({ error: error.message }); + } + break; } + return true; // Keep the message channel open for async response +}); + +// Main serving logic +async function handleRequest(request) { + const activeTabId = await getActiveTabId(); + const response = await chrome.runtime.sendMessage({ + ...request, + tabId: activeTabId, + }); + return response; +} + +// Listen for settings changes +chrome.storage.onChanged.addListener((changes, areaName) => { + loadSettings().then(() => { + if (changes.pollingEnabled.newValue && !changes.pollingEnabled.oldValue) { + pollAndConnect(); + } + }); }); // Load settings from storage @@ -83,15 +152,15 @@ function connectWebSocket() { ws.onopen = () => { console.log("[WS] Connected successfully"); - // Send a test message - ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() })); }; - ws.onmessage = (event) => { - console.log("[WS] Message received:", event.data); + ws.onmessage = async (event) => { + console.log("[WS] Request received:", event.data); try { - const data = JSON.parse(event.data); - handleMessage(data); + const req = JSON.parse(event.data); + const response = await handleRequest(req); + console.log("response:", response); + ws.send(JSON.stringify({ id: req.id, ...response })); } catch (e) { console.error("[WS] Failed to parse message:", e); } @@ -127,15 +196,23 @@ async function pollAndConnect() { } } +async function main() { + await loadSettings(); + await chrome.offscreen.createDocument({ + url: "offscreen_inspectors.html", + reasons: ["DOM_PARSER"], + justification: "Providing DOM implementation for inspector.", + }); + pollAndConnect(); +} + // Handle extension lifecycle chrome.runtime.onStartup.addListener(() => { console.log("[Lifecycle] Extension started"); - keepAlive(); - loadSettings().then(() => pollAndConnect()); + main(); }); chrome.runtime.onInstalled.addListener(() => { console.log("[Lifecycle] Extension installed/updated"); - keepAlive(); - loadSettings().then(() => pollAndConnect()); + main(); }); diff --git a/packages/@devtoolcss-mcp/manifest.json b/packages/@devtoolcss-mcp/manifest.json index b917c89..3d7d166 100644 --- a/packages/@devtoolcss-mcp/manifest.json +++ b/packages/@devtoolcss-mcp/manifest.json @@ -4,9 +4,10 @@ "manifest_version": 3, "description": "Chrome extension with WebSocket polling to MCP server", "background": { - "service_worker": "background.js" + "service_worker": "background.js", + "type": "module" }, - "permissions": ["storage"], + "permissions": ["storage", "debugger", "offscreen"], "action": { "default_popup": "popup.html", "default_title": "DevtoolCSS MCP Settings" diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.html b/packages/@devtoolcss-mcp/offscreen_inspectors.html new file mode 100644 index 0000000..16804b4 --- /dev/null +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js new file mode 100644 index 0000000..252fd95 --- /dev/null +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -0,0 +1,126 @@ +import { Inspector } from "chrome-inspector"; +import { BiWeakNodeMap } from "./BiWeakNodeMap"; + +// Create a chromeDebugger wrapper that works in offscreen context +const chromeDebugger = { + // Event listeners storage + _listeners: new Set(), + + async attach(target, version, callback = () => {}) { + chrome.runtime.sendMessage( + { + type: "DEBUGGER_ATTACH", + target, + }, + (response) => { + if (response?.error) { + throw new Error(response.error); + } + callback(); + }, + ); + }, + + async detach(target, callback = () => {}) { + chrome.runtime.sendMessage( + { + type: "DEBUGGER_DETACH", + target, + }, + (response) => { + if (response?.error) { + throw new Error(response.error); + } + callback(); + }, + ); + }, + + onEvent: { + addListener(callback) { + chromeDebugger._listeners.add(callback); + }, + removeListener(callback) { + chromeDebugger._listeners.delete(callback); + }, + }, + + // Send command to the actual chrome.debugger in background.js + async sendCommand(target, method, params) { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + { + type: "DEBUGGER_SEND_COMMAND", + target, + method, + params, + }, + (response) => { + if (response?.error) { + reject(new Error(response.error)); + } else { + resolve(response?.result); + } + }, + ); + }); + }, + + // Internal method to dispatch events to listeners + _dispatchEvent(source, method, params) { + for (const listener of chromeDebugger._listeners) { + try { + listener(source, method, params); + } catch (e) { + console.error("Error in debugger event listener:", e); + } + } + }, +}; + +const biMap = new BiWeakNodeMap(); + +// inspector management per tab +const inspectors = {}; + +async function getInspector(tabId) { + if (!inspectors[tabId]) { + await chromeDebugger.attach({ tabId }, "1.3"); + inspectors[tabId] = await Inspector.fromChromeDebugger( + chromeDebugger, + tabId, + ); + } + return inspectors[tabId]; +} + +async function serveRequest(request) { + const inspector = await getInspector(request.tabId); + switch (request.tool) { + case "querySelectorAll": + const nodes = await inspector.querySelectorAll(request.selector); + const uids = nodes.map((node) => biMap.set(node)); + console.log("serveRequest - querySelectorAll uids:", uids); + return { uids: uids }; + } +} + +// listener must be sync, return true to indicate async response +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + switch (msg.event) { + case "TAB_CLOSED": + chromeDebugger.detach({ tabId: msg.tabId }); + delete inspectors[msg.tabId]; + biMap.cleanUp(); + break; + + case "DEBUGGER_EVENT": + const { source, method, params } = msg; + chromeDebugger._dispatchEvent(source, method, params); + break; + + default: // request + serveRequest(msg).then(sendResponse); + return true; // Keep the message channel open for async response + } +}); diff --git a/packages/@devtoolcss-mcp/package.json b/packages/@devtoolcss-mcp/package.json index 7401c4e..7a482db 100644 --- a/packages/@devtoolcss-mcp/package.json +++ b/packages/@devtoolcss-mcp/package.json @@ -3,15 +3,20 @@ "version": "0.0.0", "private": true, "description": "Chrome extension with WebSocket polling to MCP server", - "main": "ws-server.js", + "type": "module", "scripts": { - "server": "node ws-server.js", - "dev": "node ws-server.js" + "build": "node scripts/copy.js && node scripts/esbuild.config.js", + "prepare": "pnpm build && pnpm sync:manifest", + "watch": "node scripts/watch.js", + "sync:manifest": "node scripts/sync-manifest.js" }, "dependencies": { + "chrome-inspector": "link:/home/bill/chrome-inspector/", "ws": "^8.18.3" }, "devDependencies": { - "@types/chrome": "^0.1.27" + "@types/chrome": "^0.1.27", + "chokidar": "^4.0.3", + "esbuild": "^0.27.0" } } diff --git a/packages/@devtoolcss-mcp/scripts/copy.js b/packages/@devtoolcss-mcp/scripts/copy.js new file mode 100644 index 0000000..133ecf8 --- /dev/null +++ b/packages/@devtoolcss-mcp/scripts/copy.js @@ -0,0 +1,30 @@ +import fs from "fs"; +import path from "path"; + +// Ensure dist/ directory exists +if (!fs.existsSync("dist")) { + fs.mkdirSync("dist"); +} + +// Recursive directory copy function +function copyDir(srcDir, destDir) { + if (!fs.existsSync(destDir)) fs.mkdirSync(destDir); + fs.readdirSync(srcDir).forEach((item) => { + const srcPath = path.join(srcDir, item); + const destPath = path.join(destDir, item); + if (fs.lstatSync(srcPath).isDirectory()) { + copyDir(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + }); +} + +// Copy all .css and .html files, and manifest.json +fs.readdirSync(".") + .filter( + (f) => f.endsWith(".css") || f.endsWith(".html") || f === "manifest.json", + ) + .forEach((f) => fs.copyFileSync(f, path.join("dist", f))); + +//copyDir("icons", path.join("dist", "icons")); diff --git a/packages/@devtoolcss-mcp/scripts/esbuild.config.js b/packages/@devtoolcss-mcp/scripts/esbuild.config.js new file mode 100644 index 0000000..884d532 --- /dev/null +++ b/packages/@devtoolcss-mcp/scripts/esbuild.config.js @@ -0,0 +1,17 @@ +import { build } from "esbuild"; + +const isProd = process.env.NODE_ENV === "production"; + +const commonOptions = { + entryPoints: ["popup.js", "background.js", "offscreen_inspectors.js"], + bundle: true, + outdir: "dist", + format: "esm", + external: ["chrome"], +}; + +build({ + ...commonOptions, + sourcemap: !isProd, + minify: isProd, +}).catch(() => process.exit(1)); diff --git a/packages/@devtoolcss-mcp/scripts/sync-manifest.js b/packages/@devtoolcss-mcp/scripts/sync-manifest.js new file mode 100644 index 0000000..9728cba --- /dev/null +++ b/packages/@devtoolcss-mcp/scripts/sync-manifest.js @@ -0,0 +1,33 @@ +import fs from "fs"; +import path from "path"; + +const cwd = process.cwd(); +const packageJsonPath = path.join(cwd, "package.json"); +const manifestJsonPath = path.join(cwd, "manifest.json"); + +function syncManifestVersion() { + if (!fs.existsSync(packageJsonPath) || !fs.existsSync(manifestJsonPath)) { + console.error( + "package.json or manifest.json not found in current directory.", + ); + process.exit(1); + } + + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + if (!packageJson.version) { + console.error("No version field found in package.json."); + process.exit(1); + } + + const manifestText = fs.readFileSync(manifestJsonPath, "utf8"); + // use regex to not disturb formatting + const updatedManifestText = manifestText.replace( + /("version"\s*:\s*)"(.*?)"/, + `$1"${packageJson.version}"`, + ); + + fs.writeFileSync(manifestJsonPath, updatedManifestText, "utf8"); + console.log(`manifest.json version synced to ${packageJson.version}`); +} + +syncManifestVersion(); diff --git a/packages/@devtoolcss-mcp/scripts/watch.js b/packages/@devtoolcss-mcp/scripts/watch.js new file mode 100644 index 0000000..7c67c7f --- /dev/null +++ b/packages/@devtoolcss-mcp/scripts/watch.js @@ -0,0 +1,51 @@ +import chokidar from "chokidar"; +import { exec } from "child_process"; + +const ignored = [ + "package.json", + "package-lock.json", + "README.md", + "tsconfig.json", + "ws-server.js", +]; + +const copyIgnoreMatcher = (path) => { + if ( + ignored.includes(path) || + path.startsWith("node_modules/") || + path.startsWith("dist/") || + path.startsWith("scripts/") || + path.startsWith(".") + ) + return true; + + return false; +}; + +chokidar.watch(".").on("change", (path) => { + if (copyIgnoreMatcher(path)) { + return; + } + + if (path.endsWith(".js")) { + // any js change could affect the bundle + console.log(`Rebuilding due to change in ${path}`); + exec( + "NODE_ENV=development node scripts/esbuild.config.js", + (error, stdout, stderr) => { + if (error) { + console.error(`Error during build: ${stderr}`); + } + }, + ); + } else { + console.log(`Copying due to change in ${path}`); + exec(`cp ${path} dist/`, (error, stdout, stderr) => { + if (error) { + console.error(`Error during copy: ${stderr}`); + } + }); + } +}); + +console.log("Watching for changes..."); diff --git a/packages/@devtoolcss-mcp/ws-server.js b/packages/@devtoolcss-mcp/ws-server.js index 471dd02..6d1afa5 100644 --- a/packages/@devtoolcss-mcp/ws-server.js +++ b/packages/@devtoolcss-mcp/ws-server.js @@ -1,6 +1,7 @@ #!/usr/bin/env node import http from "http"; -import WebSocket from "ws"; +import WebSocket, { WebSocketServer } from "ws"; +import readline from "readline"; const PORT = process.env.PORT || 9333; @@ -38,7 +39,7 @@ const server = http.createServer((req, res) => { }); // Create WebSocket server -const wss = new WebSocket.Server({ server }); +const wss = new WebSocketServer({ server }); console.log(`[Server] HTTP + WebSocket server listening on:`); console.log(` - HTTP: http://127.0.0.1:${PORT}/health`); @@ -47,32 +48,21 @@ console.log(` - WebSocket: ws://127.0.0.1:${PORT}`); const handleMessage = (message) => { try { const data = JSON.parse(message.toString()); - - // Handle different message types - if (data.type === "ping") { - ws.send( - JSON.stringify({ - type: "pong", - timestamp: Date.now(), - original: data, - }), - ); - } else { - // Echo back with a response - ws.send( - JSON.stringify({ - type: "response", - original: data, - timestamp: Date.now(), - }), - ); - } + console.log("[WS] Message received:", data); } catch (e) { console.error("[WS] Failed to parse message:", e); } }; +let activeWs = null; // Track the active WebSocket connection + wss.on("connection", (ws) => { + if (activeWs && activeWs.readyState === WebSocket.OPEN) { + ws.close(1000, "Only one connection allowed"); + console.log("[WS] Refused new connection: already connected"); + return; + } + activeWs = ws; console.log("[WS] Client connected"); ws.on("message", (message) => { @@ -82,20 +72,14 @@ wss.on("connection", (ws) => { ws.on("close", () => { console.log("[WS] Client disconnected"); + if (activeWs === ws) { + activeWs = null; + } }); ws.on("error", (error) => { console.error("[WS] Error:", error); }); - - // Send welcome message - ws.send( - JSON.stringify({ - type: "welcome", - message: "Connected to DevtoolCSS MCP Server", - timestamp: Date.now(), - }), - ); }); wss.on("error", (error) => { @@ -106,3 +90,22 @@ wss.on("error", (error) => { server.listen(PORT, "127.0.0.1", () => { console.log("[Server] Ready to accept connections"); }); +// Setup readline interface for stdin +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false, +}); + +rl.on("line", (line) => { + try { + const json = JSON.parse(line); + if (activeWs && activeWs.readyState === WebSocket.OPEN) { + activeWs.send(JSON.stringify(json)); + } + // Optionally, log if no active connection + } catch (e) { + console.error("Failed to parse stdin line as JSON:", e); + // Ignore lines that are not valid JSON + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e18ef6..c6ecad1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ importers: packages/@devtoolcss-mcp: dependencies: + chrome-inspector: + specifier: link:/home/bill/chrome-inspector/ + version: link:../../../chrome-inspector ws: specifier: ^8.18.3 version: 8.18.3 @@ -43,6 +46,12 @@ importers: '@types/chrome': specifier: ^0.1.27 version: 0.1.27 + chokidar: + specifier: ^4.0.3 + version: 4.0.3 + esbuild: + specifier: ^0.27.0 + version: 0.27.0 packages/@devtoolcss-parser: devDependencies: @@ -198,156 +207,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.10': resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.10': resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.10': resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.10': resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.10': resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.10': resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.10': resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.10': resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.10': resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.10': resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.10': resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.10': resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.10': resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.10': resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.10': resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.10': resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.10': resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.10': resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.10': resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.10': resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.10': resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.10': resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.10': resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.10': resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.10': resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -582,6 +747,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1105,81 +1275,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.10': optional: true + '@esbuild/aix-ppc64@0.27.0': + optional: true + '@esbuild/android-arm64@0.25.10': optional: true + '@esbuild/android-arm64@0.27.0': + optional: true + '@esbuild/android-arm@0.25.10': optional: true + '@esbuild/android-arm@0.27.0': + optional: true + '@esbuild/android-x64@0.25.10': optional: true + '@esbuild/android-x64@0.27.0': + optional: true + '@esbuild/darwin-arm64@0.25.10': optional: true + '@esbuild/darwin-arm64@0.27.0': + optional: true + '@esbuild/darwin-x64@0.25.10': optional: true + '@esbuild/darwin-x64@0.27.0': + optional: true + '@esbuild/freebsd-arm64@0.25.10': optional: true + '@esbuild/freebsd-arm64@0.27.0': + optional: true + '@esbuild/freebsd-x64@0.25.10': optional: true + '@esbuild/freebsd-x64@0.27.0': + optional: true + '@esbuild/linux-arm64@0.25.10': optional: true + '@esbuild/linux-arm64@0.27.0': + optional: true + '@esbuild/linux-arm@0.25.10': optional: true + '@esbuild/linux-arm@0.27.0': + optional: true + '@esbuild/linux-ia32@0.25.10': optional: true + '@esbuild/linux-ia32@0.27.0': + optional: true + '@esbuild/linux-loong64@0.25.10': optional: true + '@esbuild/linux-loong64@0.27.0': + optional: true + '@esbuild/linux-mips64el@0.25.10': optional: true + '@esbuild/linux-mips64el@0.27.0': + optional: true + '@esbuild/linux-ppc64@0.25.10': optional: true + '@esbuild/linux-ppc64@0.27.0': + optional: true + '@esbuild/linux-riscv64@0.25.10': optional: true + '@esbuild/linux-riscv64@0.27.0': + optional: true + '@esbuild/linux-s390x@0.25.10': optional: true + '@esbuild/linux-s390x@0.27.0': + optional: true + '@esbuild/linux-x64@0.25.10': optional: true + '@esbuild/linux-x64@0.27.0': + optional: true + '@esbuild/netbsd-arm64@0.25.10': optional: true + '@esbuild/netbsd-arm64@0.27.0': + optional: true + '@esbuild/netbsd-x64@0.25.10': optional: true + '@esbuild/netbsd-x64@0.27.0': + optional: true + '@esbuild/openbsd-arm64@0.25.10': optional: true + '@esbuild/openbsd-arm64@0.27.0': + optional: true + '@esbuild/openbsd-x64@0.25.10': optional: true + '@esbuild/openbsd-x64@0.27.0': + optional: true + '@esbuild/openharmony-arm64@0.25.10': optional: true + '@esbuild/openharmony-arm64@0.27.0': + optional: true + '@esbuild/sunos-x64@0.25.10': optional: true + '@esbuild/sunos-x64@0.27.0': + optional: true + '@esbuild/win32-arm64@0.25.10': optional: true + '@esbuild/win32-arm64@0.27.0': + optional: true + '@esbuild/win32-ia32@0.25.10': optional: true + '@esbuild/win32-ia32@0.27.0': + optional: true + '@esbuild/win32-x64@0.25.10': optional: true + '@esbuild/win32-x64@0.27.0': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -1434,6 +1682,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.10 '@esbuild/win32-x64': 0.25.10 + esbuild@0.27.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 + escalade@3.2.0: {} escape-html@1.0.3: {} From 1c48c66e15ed9ca6fa58837fd51ab72d52cbd9c3 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Thu, 13 Nov 2025 22:56:24 +0800 Subject: [PATCH 18/36] feat(mcp): support all listed features --- packages/@devtoolcss-mcp/background.js | 1 + packages/@devtoolcss-mcp/htmlUtils.js | 125 ++++++++++++++ .../@devtoolcss-mcp/offscreen_inspectors.js | 124 +++++++++++++- packages/@devtoolcss-mcp/styleUtils.js | 157 ++++++++++++++++++ 4 files changed, 405 insertions(+), 2 deletions(-) create mode 100644 packages/@devtoolcss-mcp/htmlUtils.js create mode 100644 packages/@devtoolcss-mcp/styleUtils.js diff --git a/packages/@devtoolcss-mcp/background.js b/packages/@devtoolcss-mcp/background.js index 6b8e11b..9035dad 100644 --- a/packages/@devtoolcss-mcp/background.js +++ b/packages/@devtoolcss-mcp/background.js @@ -88,6 +88,7 @@ async function handleRequest(request) { ...request, tabId: activeTabId, }); + console.log("response received:", response); return response; } diff --git a/packages/@devtoolcss-mcp/htmlUtils.js b/packages/@devtoolcss-mcp/htmlUtils.js new file mode 100644 index 0000000..adfa936 --- /dev/null +++ b/packages/@devtoolcss-mcp/htmlUtils.js @@ -0,0 +1,125 @@ +import beautify from "js-beautify"; +/** + * Truncates HTML based on depth and line length controls + * @param {Node} node - The DOM node to truncate + * @param {number} [maxDepth] - Maximum nesting depth of tags to include + * @param {number} [maxLineLength] - Maximum length of each line + * @returns {string} Truncated HTML + */ +export function truncateHTML(node, maxDepth, maxLineLength, maxChars) { + let result = node.cloneNode(true); + + // Truncate by depth + if (maxDepth > 0) { + for (let depth = maxDepth; depth > 0; depth--) { + // has to use original node each time to ensure summary is correct + let truncated = truncateByDepth(node, depth); + let html = truncated.outerHTML || truncated.textContent; + if (maxChars === undefined || (html && html.length < maxChars)) { + result = truncated; + break; + } + } + } + + // Get HTML string + let html = result.outerHTML || result.textContent; + html = beautify.html(html, { + indent_size: 2, + wrap_line_length: 0, // Don't wrap - let truncateByLineLength handle it + preserve_newlines: false, + }); + + // Truncate by line length + if (maxLineLength !== undefined && maxLineLength > 0) { + html = truncateByLineLength(html, maxLineLength); + } + + return html; +} + +/** + * Counts the structure of remaining nodes + * @param {Node} node - The DOM node to analyze + * @returns {Object} Summary of node counts + */ +function summarizeRemainingStructure(node) { + const summary = { + elements: 0, + textNodes: 0, + totalDepth: 0, + }; + + function traverse(n, depth) { + summary.totalDepth = Math.max(summary.totalDepth, depth); + + for (let child of n.childNodes) { + if (child.nodeType === Node.ELEMENT_NODE) { + summary.elements++; + traverse(child, depth + 1); + } else if ( + child.nodeType === Node.TEXT_NODE && + child.textContent.trim() + ) { + summary.textNodes++; + } + } + } + + traverse(node, 0); + return summary; +} + +/** + * Truncates a DOM node by maximum nesting depth + * @param {Node} node - The DOM node to truncate + * @param {number} maxDepth - Maximum depth to traverse + * @returns {Node} Cloned and truncated node + */ +function truncateByDepth(node, maxDepth) { + // Clone the node without children + const clone = node.cloneNode(false); + + // If we're at max depth, add summary comment and return + if (maxDepth <= 0) { + if (clone.nodeType === Node.ELEMENT_NODE) { + const summary = summarizeRemainingStructure(node); + const summaryText = `... ${summary.elements} more element(s), ${summary.textNodes} text node(s), max depth +${summary.totalDepth}`; + clone.appendChild(document.createComment(summaryText)); + } + return clone; + } + + // Process children + for (let child of node.childNodes) { + if (child.nodeType === Node.ELEMENT_NODE) { + // Recursively truncate element children + clone.appendChild(truncateByDepth(child, maxDepth - 1)); + } else if ( + child.nodeType === Node.TEXT_NODE || + child.nodeType === Node.COMMENT_NODE + ) { + // Copy text and comment nodes as-is + clone.appendChild(child.cloneNode(true)); + } + } + + return clone; +} + +/** + * Truncates each line of text to a maximum length + * @param {string} text - The text to truncate + * @param {number} maxLineLength - Maximum length per line + * @returns {string} Text with truncated lines + */ +function truncateByLineLength(text, maxLineLength) { + const lines = text.split("\n"); + return lines + .map((line) => + line.length > maxLineLength + ? line.substring(0, maxLineLength - 3) + "..." + : line, + ) + .join("\n"); +} diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js index 252fd95..84d293e 100644 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -1,5 +1,11 @@ import { Inspector } from "chrome-inspector"; import { BiWeakNodeMap } from "./BiWeakNodeMap"; +import { truncateHTML } from "./htmlUtils"; +import { + filterComputedStyle, + filterMatchedStyles, + simplifyMatchedStyles, +} from "./styleUtils"; // Create a chromeDebugger wrapper that works in offscreen context const chromeDebugger = { @@ -97,11 +103,125 @@ async function getInspector(tabId) { async function serveRequest(request) { const inspector = await getInspector(request.tabId); switch (request.tool) { - case "querySelectorAll": + case "querySelectorAll": { const nodes = await inspector.querySelectorAll(request.selector); const uids = nodes.map((node) => biMap.set(node)); - console.log("serveRequest - querySelectorAll uids:", uids); return { uids: uids }; + } + + case "getMatchedStyles": { + const node = biMap.getNode(request.uid); + if (!node) { + return { error: "Node not found for uid: " + request.uid }; + } + let styles = await node.getMatchedStyles(request.options || {}); + console.log("serveRequest - getMatchedStyles styles:", styles); + + // Apply filters to reduce response size + if (request.filter) { + styles = filterMatchedStyles(styles, request.filter); + } + + // Optionally simplify the response + if (request.simplify) { + styles = simplifyMatchedStyles(styles); + } + + return { styles }; + } + + case "getComputedStyle": { + const node = biMap.getNode(request.uid); + if (!node) { + return { error: "Node not found for uid: " + request.uid }; + } + + let styles = await node.getComputedStyle(request.options || {}); + console.log("serveRequest - getComputedStyle styles:", styles); + + // Apply filters to reduce response size + if (request.filter) { + styles = filterComputedStyle(styles, request.filter); + } else if (request.properties) { + // Backward compatibility: support direct properties array + styles = filterComputedStyle(styles, { + properties: request.properties, + }); + } + + return { styles }; + } + + case "querySelectorAll_handle": { + const node = biMap.getNode(request.uid); + if (!node) { + return { error: "Node not found for uid: " + request.uid }; + } + const nodes = await node.querySelectorAll(request.selector); + const uids = nodes.map((n) => biMap.set(n)); + return { uids }; + } + + case "parent": { + const node = biMap.getNode(request.uid); + if (!node) { + return { error: "Node not found for uid: " + request.uid }; + } + const parent = node.parentNode; + if (!parent) { + return { uid: null }; + } + const uid = biMap.set(parent); + return { uid }; + } + + case "children": { + const node = biMap.getNode(request.uid); + if (!node) { + return { error: "Node not found for uid: " + request.uid }; + } + const children = node.children || node.childNodes; + const uids = children.map((child) => biMap.set(child)); + return { uids }; + } + + case "attributes": { + const node = biMap.getNode(request.uid); + if (!node) { + return { error: "Node not found for uid: " + request.uid }; + } + const attrs = {}; + if (node.attributes) { + for (let i = 0; i < node.attributes.length; i++) { + const attr = node.attributes[i]; + attrs[attr.name] = attr.value; + } + } + return { attributes: attrs }; + } + + case "outerHTML": { + const node = biMap.getNode(request.uid); + if (!node) { + return { error: "Node not found for uid: " + request.uid }; + } else if (!node.tracked) { + return { + error: "Node is no longer existed for uid: " + request.uid, + }; + } + // Apply depth and line length controls if provided + html = truncateHTML( + node._docNode, + request.maxDepth, + request.maxLineLength, + request.maxChars, + ); + + return { outerHTML: html }; + } + + default: + return { error: "Unknown tool: " + request.tool }; } } diff --git a/packages/@devtoolcss-mcp/styleUtils.js b/packages/@devtoolcss-mcp/styleUtils.js new file mode 100644 index 0000000..75dd22f --- /dev/null +++ b/packages/@devtoolcss-mcp/styleUtils.js @@ -0,0 +1,157 @@ +/** + * Filters computed styles based on provided property names or patterns + * @param {Object} styles - The computed styles object + * @param {Object} filter - Filter options + * @param {string[]} [filter.properties] - Specific property names to include + * @param {string[]} [filter.patterns] - Regex patterns to match property names + * @returns {Object} Filtered styles + */ +export function filterComputedStyle(styles, filter = {}) { + if (!filter || (!filter.properties && !filter.patterns && !filter.exclude)) { + return styles; + } + + const result = {}; + + // If specific properties are requested, only include those + if (filter.properties && Array.isArray(filter.properties)) { + for (const prop of filter.properties) { + if (prop in styles) { + result[prop] = styles[prop]; + } + } + return result; + } + + // If patterns are provided, match property names + let matchedProps = new Set(); + if (filter.patterns && Array.isArray(filter.patterns)) { + const regexes = filter.patterns.map((pattern) => new RegExp(pattern)); + for (const prop in styles) { + for (const regex of regexes) { + if (regex.test(prop)) { + matchedProps.add(prop); + break; + } + } + } + } else { + // No patterns, include all + matchedProps = new Set(Object.keys(styles)); + } + + return result; +} + +/** + * Filters matched styles response to reduce size + * @param {Object} matchedStyles - The matched styles object from chrome-inspector + * @param {Object} filter - Filter options + * @param {string[]} [filter.field] - Array of field types to include (e.g., ['inlineStyle', 'matchedCSSRules', 'inherited', 'pseudoElements']) + * @param {string[]} [filter.selectors] - Regex pattern for selector matching + * @param {string[]} [filter.properties] - Properties to include rules with matching properties + * @returns {Object} Filtered matched styles + */ +export function filterMatchedStyles(matchedStyles, filter = {}) { + if (!filter || Object.keys(filter).length === 0) { + return matchedStyles; + } + + // Compile regex patterns if provided + const selectorRegexes = filter.selectors + ? filter.selectors.map((pattern) => new RegExp(pattern)) + : null; + + if (filter.field) { + const fieldsToInclude = new Set(filter.field); + for (const key of Object.keys(matchedStyles)) { + if (!fieldsToInclude.has(key)) { + delete matchedStyles[key]; + } + } + } + + const filterRuleBySelectors = (rules) => { + return rules.filter((rule) => { + return selectorRegexes.some((regex) => + regex.test(rule.matchedSelectors.join(", ")), + ); + }); + }; + + if (filter.selectors) { + if (matchedStyles.inherited) { + for (const inheritedItem of matchedStyles.inherited) { + inherited; + } + } + } + + // TODO: fix logic + + return result; +} + +/** + * Simplifies matched styles to a more readable format + * @param {Object} matchedStyles - The matched styles object + * @returns {Object} Simplified styles object + */ +export function simplifyMatchedStyles(matchedStyles) { + const simplified = { + inline: {}, + matched: [], + inherited: [], + }; + + // Extract inline styles + if (matchedStyles.inlineStyle?.cssProperties) { + for (const prop of matchedStyles.inlineStyle.cssProperties) { + simplified.inline[prop.name] = prop.value; + } + } + + // Extract matched rules + if (matchedStyles.matchedCSSRules) { + for (const rule of matchedStyles.matchedCSSRules) { + const ruleObj = { + selector: rule.rule?.selectorList?.text || rule.rule?.selectorText, + properties: {}, + }; + + if (rule.rule?.style?.cssProperties) { + for (const prop of rule.rule.style.cssProperties) { + ruleObj.properties[prop.name] = prop.value; + } + } + + simplified.matched.push(ruleObj); + } + } + + // Extract inherited styles + if (matchedStyles.inherited) { + for (const inheritedItem of matchedStyles.inherited) { + const inheritedObj = { + from: inheritedItem.inlineStyle ? "inline" : "rules", + properties: {}, + }; + + if (inheritedItem.matchedCSSRules) { + for (const rule of inheritedItem.matchedCSSRules) { + if (rule.rule?.style?.cssProperties) { + for (const prop of rule.rule.style.cssProperties) { + inheritedObj.properties[prop.name] = prop.value; + } + } + } + } + + if (Object.keys(inheritedObj.properties).length > 0) { + simplified.inherited.push(inheritedObj); + } + } + } + + return simplified; +} From 25bb5f7064bd3f5cae7ebff1ead8d731d644e493 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Sat, 15 Nov 2025 13:17:31 +0800 Subject: [PATCH 19/36] feat(mcp): getNodes by eval DOM expression --- packages/@devtoolcss-mcp/BiWeakNodeMap.js | 2 +- .../@devtoolcss-mcp/offscreen_inspectors.js | 120 +++++++++--------- 2 files changed, 64 insertions(+), 58 deletions(-) diff --git a/packages/@devtoolcss-mcp/BiWeakNodeMap.js b/packages/@devtoolcss-mcp/BiWeakNodeMap.js index e9f44c4..9118468 100644 --- a/packages/@devtoolcss-mcp/BiWeakNodeMap.js +++ b/packages/@devtoolcss-mcp/BiWeakNodeMap.js @@ -6,7 +6,7 @@ export class BiWeakNodeMap { } set(node) { - const id = `${node.nodeName.toLowerCase()}-${++this.idCnt}`; + const id = `${node.nodeName.toLowerCase()}_${++this.idCnt}`; this._idToRef.set(id, new WeakRef(node)); this._nodeToId.set(node, id); return id; diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js index 84d293e..055b390 100644 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -84,8 +84,6 @@ const chromeDebugger = { }, }; -const biMap = new BiWeakNodeMap(); - // inspector management per tab const inspectors = {}; @@ -100,17 +98,73 @@ async function getInspector(tabId) { return inspectors[tabId]; } +const biMap = new BiWeakNodeMap(); + +// handling predefined nodes +function getNode(uid, inspector) { + if (uid === "html") return inspector.querySelector("html"); + return biMap.getNode(uid); +} + +/** + * Evaluates a DOM expression by replacing UID variables with actual nodes + * Examples: + * "html" -> predefined html element (querySelector('html')) + * "html.querySelectorAll('div.container')[0]" -> query from html + * "uid_1.querySelectorAll('span')[0]" -> query from node + * "uid_1.parentNode" -> get parent node + * "uid_1.children[1]" -> get second child + * + * @param {Inspector} inspector - The inspector instance + * @param {string} expression - DOM expression to evaluate + * @returns {Promise<{uids: string[]} | {error: string}>} + */ +async function evaluateNodeExpression(inspector, expression) { + expression = expression.trim(); + const targetNodeName = expression.split(".")[0]; + const targetNode = getNode(targetNodeName, inspector); + if (!targetNode) { + return { error: `Target node '${targetNodeName}' not found` }; + } + + const remainingExpression = expression.slice(targetNodeName.length); + // TODO: validate remainingExpression to ensure safety + + try { + let result = eval(` + targetNode${remainingExpression}; + `); + + // Normalize result to array + let nodes; + if (result === null || result === undefined) { + nodes = []; + } else if (Array.isArray(result)) { + nodes = result; + } else { + nodes = [result]; + } + + const uids = nodes.map((node) => biMap.set(node)); + return { uids }; + } catch (error) { + return { error: `Failed to evaluate expression: ${error.message}` }; + } +} + async function serveRequest(request) { const inspector = await getInspector(request.tabId); switch (request.tool) { - case "querySelectorAll": { - const nodes = await inspector.querySelectorAll(request.selector); - const uids = nodes.map((node) => biMap.set(node)); - return { uids: uids }; + case "getNodes": { + // Unified node retrieval using DOM expression syntax + if (!request.expression) { + return { error: "Missing 'expression' parameter" }; + } + return await evaluateNodeExpression(inspector, request.expression); } case "getMatchedStyles": { - const node = biMap.getNode(request.uid); + const node = getNode(request.uid, inspector); if (!node) { return { error: "Node not found for uid: " + request.uid }; } @@ -131,7 +185,7 @@ async function serveRequest(request) { } case "getComputedStyle": { - const node = biMap.getNode(request.uid); + const node = getNode(request.uid, inspector); if (!node) { return { error: "Node not found for uid: " + request.uid }; } @@ -152,56 +206,8 @@ async function serveRequest(request) { return { styles }; } - case "querySelectorAll_handle": { - const node = biMap.getNode(request.uid); - if (!node) { - return { error: "Node not found for uid: " + request.uid }; - } - const nodes = await node.querySelectorAll(request.selector); - const uids = nodes.map((n) => biMap.set(n)); - return { uids }; - } - - case "parent": { - const node = biMap.getNode(request.uid); - if (!node) { - return { error: "Node not found for uid: " + request.uid }; - } - const parent = node.parentNode; - if (!parent) { - return { uid: null }; - } - const uid = biMap.set(parent); - return { uid }; - } - - case "children": { - const node = biMap.getNode(request.uid); - if (!node) { - return { error: "Node not found for uid: " + request.uid }; - } - const children = node.children || node.childNodes; - const uids = children.map((child) => biMap.set(child)); - return { uids }; - } - - case "attributes": { - const node = biMap.getNode(request.uid); - if (!node) { - return { error: "Node not found for uid: " + request.uid }; - } - const attrs = {}; - if (node.attributes) { - for (let i = 0; i < node.attributes.length; i++) { - const attr = node.attributes[i]; - attrs[attr.name] = attr.value; - } - } - return { attributes: attrs }; - } - case "outerHTML": { - const node = biMap.getNode(request.uid); + const node = getNode(request.uid, inspector); if (!node) { return { error: "Node not found for uid: " + request.uid }; } else if (!node.tracked) { From 7e322a8fab89636aab2d06ec6f4a98ad0eb1c3d7 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Sat, 15 Nov 2025 13:20:57 +0800 Subject: [PATCH 20/36] feat(mcp): id count for each nodeName --- packages/@devtoolcss-mcp/BiWeakNodeMap.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/@devtoolcss-mcp/BiWeakNodeMap.js b/packages/@devtoolcss-mcp/BiWeakNodeMap.js index 9118468..ad311bc 100644 --- a/packages/@devtoolcss-mcp/BiWeakNodeMap.js +++ b/packages/@devtoolcss-mcp/BiWeakNodeMap.js @@ -1,12 +1,22 @@ export class BiWeakNodeMap { constructor() { - this.idCnt = 0; + this._nodeCounters = new Map(); // nodeName -> counter this._idToRef = new Map(); // id -> WeakRef(node) this._nodeToId = new WeakMap(); // node -> id } + generateId(node) { + const nodeName = node.nodeName.toLowerCase(); + const counter = this._nodeCounters.get(nodeName) || 0; + this._nodeCounters.set(nodeName, counter + 1); + return `${nodeName}_${counter}`; + } + set(node) { - const id = `${node.nodeName.toLowerCase()}_${++this.idCnt}`; + if (this._nodeToId.has(node)) { + return this._nodeToId.get(node); + } + const id = this.generateId(node); this._idToRef.set(id, new WeakRef(node)); this._nodeToId.set(node, id); return id; From 5e0c7648034fd094fa4426f4d84c3a48f8ad5c06 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Sun, 16 Nov 2025 11:14:34 +0800 Subject: [PATCH 21/36] feat(mcp): getNodes by DOM expression --- packages/@devtoolcss-mcp/DOMExpression.js | 114 ++++++++++++++++++ .../@devtoolcss-mcp/offscreen_inspectors.js | 40 ++---- 2 files changed, 121 insertions(+), 33 deletions(-) create mode 100644 packages/@devtoolcss-mcp/DOMExpression.js diff --git a/packages/@devtoolcss-mcp/DOMExpression.js b/packages/@devtoolcss-mcp/DOMExpression.js new file mode 100644 index 0000000..ce9e25b --- /dev/null +++ b/packages/@devtoolcss-mcp/DOMExpression.js @@ -0,0 +1,114 @@ +function splitExpression(expr) { + if (expr[0] !== ".") throw new Error("Expression must start with a dot"); + const parts = []; + let current = ""; + let inSingle = false, + inDouble = false, + bracketDepth = 0, + parenDepth = 0; + + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + current += c; + if (c === "'" && !inDouble) inSingle = !inSingle; + else if (c === '"' && !inSingle) inDouble = !inDouble; + else if (!inSingle && !inDouble) { + if (c === "[") bracketDepth++; + else if (c === "]") bracketDepth--; + else if (c === "(") parenDepth++; + else if (c === ")") parenDepth--; + + if (bracketDepth > 1 || parenDepth > 1) { + throw new Error( + "cannot evaluate complex expression with nested [] or ()", + ); + } else if (bracketDepth < 0 || parenDepth < 0) { + throw new Error("[] or () not balanced"); + } + + const nextChar = expr[i + 1]; + if ( + [".", "[", "("].includes(nextChar) && + bracketDepth === 0 && + parenDepth === 0 + ) { + parts.push(current); + current = ""; + continue; + } + } + } + if (current) parts.push(current); + return parts; +} + +function evalMethods(target, expr) { + const operations = splitExpression(expr); + + for (const op of operations) { + if (op.startsWith("(")) { + // handle method calls e.g. querySelectorAll('div') + const argStr = op.slice(1, -1).trim(); + const args = argStr + ? argStr.split(",").map((arg) => { + // probably cannot use JSON.parse because it is js expression + // not really json with field quoted and only double quotes allowed + arg = arg.trim(); + if (arg === "undefined") return undefined; + if (arg === "null") return null; + if (arg === "true") return true; + if (arg === "false") return false; + if (!isNaN(Number(arg))) return Number(arg); + return arg.trim().replace(/^['"]|['"]$/g, ""); + }) + : []; + target = target(...args); + } else { + // handle property access e.g. .parentNode, [0] + const accessorStr = op.startsWith(".") ? op.slice(1) : op.slice(1, -1); + const accessor = !isNaN(Number(accessorStr)) + ? Number(accessorStr) + : accessorStr; + const field = target[accessor]; + target = typeof field === "function" ? field.bind(target) : field; + } + } + return target; +} + +export async function evaluateDOMExpression( + expression, + inspector, + getNode, + setNode, +) { + expression = expression.trim(); + const targetNodeName = expression.split(".")[0]; + const targetNode = getNode(targetNodeName, inspector); + if (!targetNode) { + return { error: `Target node '${targetNodeName}' not found` }; + } + + const remainingExpression = expression.slice(targetNodeName.length); + // TODO: validate remainingExpression to ensure safety + + try { + // cannot use dynamic code eval due to MV3 + const result = evalMethods(targetNode, remainingExpression); + + // Normalize result to array + let nodes; + if (result === null || result === undefined) { + nodes = []; + } else if (Array.isArray(result)) { + nodes = result; + } else { + nodes = [result]; + } + + const uids = nodes.map((node) => setNode(node)); + return { uids }; + } catch (error) { + return { error: `Failed to evaluate expression: ${error.message}` }; + } +} diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js index 055b390..e72c3ec 100644 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -6,6 +6,7 @@ import { filterMatchedStyles, simplifyMatchedStyles, } from "./styleUtils"; +import { evaluateDOMExpression } from "./DOMExpression"; // Create a chromeDebugger wrapper that works in offscreen context const chromeDebugger = { @@ -119,38 +120,6 @@ function getNode(uid, inspector) { * @param {string} expression - DOM expression to evaluate * @returns {Promise<{uids: string[]} | {error: string}>} */ -async function evaluateNodeExpression(inspector, expression) { - expression = expression.trim(); - const targetNodeName = expression.split(".")[0]; - const targetNode = getNode(targetNodeName, inspector); - if (!targetNode) { - return { error: `Target node '${targetNodeName}' not found` }; - } - - const remainingExpression = expression.slice(targetNodeName.length); - // TODO: validate remainingExpression to ensure safety - - try { - let result = eval(` - targetNode${remainingExpression}; - `); - - // Normalize result to array - let nodes; - if (result === null || result === undefined) { - nodes = []; - } else if (Array.isArray(result)) { - nodes = result; - } else { - nodes = [result]; - } - - const uids = nodes.map((node) => biMap.set(node)); - return { uids }; - } catch (error) { - return { error: `Failed to evaluate expression: ${error.message}` }; - } -} async function serveRequest(request) { const inspector = await getInspector(request.tabId); @@ -160,7 +129,12 @@ async function serveRequest(request) { if (!request.expression) { return { error: "Missing 'expression' parameter" }; } - return await evaluateNodeExpression(inspector, request.expression); + return await evaluateDOMExpression( + request.expression, + inspector, + getNode, + biMap.set.bind(biMap), + ); } case "getMatchedStyles": { From 3146f1758ad545eb89c410c24fd6aa32603bdae3 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Sun, 16 Nov 2025 18:05:35 +0800 Subject: [PATCH 22/36] chore: bump chrome-inspector to 1.0.4 for .document --- packages/@devtoolcss-mcp/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@devtoolcss-mcp/package.json b/packages/@devtoolcss-mcp/package.json index 7a482db..0d65de0 100644 --- a/packages/@devtoolcss-mcp/package.json +++ b/packages/@devtoolcss-mcp/package.json @@ -11,7 +11,7 @@ "sync:manifest": "node scripts/sync-manifest.js" }, "dependencies": { - "chrome-inspector": "link:/home/bill/chrome-inspector/", + "chrome-inspector": "^1.0.4", "ws": "^8.18.3" }, "devDependencies": { From bfab355c49926dc6bf50acdad5f8c7ff03fba8d3 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Sun, 16 Nov 2025 18:12:23 +0800 Subject: [PATCH 23/36] feat(mcp): document as predefined target --- packages/@devtoolcss-mcp/offscreen_inspectors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js index e72c3ec..33f2254 100644 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -103,7 +103,7 @@ const biMap = new BiWeakNodeMap(); // handling predefined nodes function getNode(uid, inspector) { - if (uid === "html") return inspector.querySelector("html"); + if (uid === "document") return inspector.document; return biMap.getNode(uid); } From d02a2e2cdb50dbddc6f58ff0be208b2bfc74e38c Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Mon, 17 Nov 2025 20:14:02 +0800 Subject: [PATCH 24/36] refactor(mcp): usable TODO: handle errors --- packages/@devtoolcss-mcp/htmlUtils.js | 3 +- .../@devtoolcss-mcp/offscreen_inspectors.js | 87 +++--- packages/@devtoolcss-mcp/styleUtils.js | 254 +++++++++--------- 3 files changed, 177 insertions(+), 167 deletions(-) diff --git a/packages/@devtoolcss-mcp/htmlUtils.js b/packages/@devtoolcss-mcp/htmlUtils.js index adfa936..6a010db 100644 --- a/packages/@devtoolcss-mcp/htmlUtils.js +++ b/packages/@devtoolcss-mcp/htmlUtils.js @@ -85,7 +85,8 @@ function truncateByDepth(node, maxDepth) { if (clone.nodeType === Node.ELEMENT_NODE) { const summary = summarizeRemainingStructure(node); const summaryText = `... ${summary.elements} more element(s), ${summary.textNodes} text node(s), max depth +${summary.totalDepth}`; - clone.appendChild(document.createComment(summaryText)); + if (summary.totalDepth > 0) + clone.appendChild(document.createComment(summaryText)); } return clone; } diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js index 33f2254..beb5e14 100644 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -1,11 +1,7 @@ import { Inspector } from "chrome-inspector"; import { BiWeakNodeMap } from "./BiWeakNodeMap"; import { truncateHTML } from "./htmlUtils"; -import { - filterComputedStyle, - filterMatchedStyles, - simplifyMatchedStyles, -} from "./styleUtils"; +import { filterMatchedStyles, toStyleSheetText } from "./styleUtils"; import { evaluateDOMExpression } from "./DOMExpression"; // Create a chromeDebugger wrapper that works in offscreen context @@ -104,6 +100,7 @@ const biMap = new BiWeakNodeMap(); // handling predefined nodes function getNode(uid, inspector) { if (uid === "document") return inspector.document; + if (uid === "$0") return inspector.$0; return biMap.getNode(uid); } @@ -138,24 +135,37 @@ async function serveRequest(request) { } case "getMatchedStyles": { - const node = getNode(request.uid, inspector); + const { + uid, + removeUnusedVar = true, + appliedOnly = false, + filter, + } = request; + const node = getNode(uid, inspector); if (!node) { - return { error: "Node not found for uid: " + request.uid }; + return { error: `Node not found for uid: ${uid}` }; } - let styles = await node.getMatchedStyles(request.options || {}); - console.log("serveRequest - getMatchedStyles styles:", styles); + const options = { + parseOptions: { removeUnusedVar }, + }; + let styles = await node.getMatchedStyles(options); // Apply filters to reduce response size - if (request.filter) { - styles = filterMatchedStyles(styles, request.filter); - } - - // Optionally simplify the response - if (request.simplify) { - styles = simplifyMatchedStyles(styles); + if (filter) { + styles = filterMatchedStyles(styles, filter); } + const toStyleSheetOptions = { + applied: appliedOnly ? false : true, + matchedSelectors: true, + }; + const styleSheetText = toStyleSheetText( + styles, + node, + toStyleSheetOptions, + ); + console.log("serveRequest - getMatchedStyles styles:", styleSheetText); - return { styles }; + return { styles: styleSheetText }; } case "getComputedStyle": { @@ -164,39 +174,38 @@ async function serveRequest(request) { return { error: "Node not found for uid: " + request.uid }; } - let styles = await node.getComputedStyle(request.options || {}); - console.log("serveRequest - getComputedStyle styles:", styles); - - // Apply filters to reduce response size - if (request.filter) { - styles = filterComputedStyle(styles, request.filter); - } else if (request.properties) { - // Backward compatibility: support direct properties array - styles = filterComputedStyle(styles, { - properties: request.properties, - }); - } - - return { styles }; + const styles = await node.getComputedStyle(); + const filtered = {}; + request.properties.map((prop) => { + filtered[prop] = styles[prop]; + }); + console.log("serveRequest - getComputedStyle styles:", filtered); + return { styles: filtered }; } case "outerHTML": { - const node = getNode(request.uid, inspector); + // some safe defaults + const { + uid, + maxDepth = 3, + maxLineLength = 1000, + maxChars = 500000, + } = request; + const node = getNode(uid, inspector); if (!node) { - return { error: "Node not found for uid: " + request.uid }; + return { error: `Node not found for uid: ${uid}` }; } else if (!node.tracked) { return { - error: "Node is no longer existed for uid: " + request.uid, + error: `Node is no longer existed for uid: ${uid}`, }; } // Apply depth and line length controls if provided - html = truncateHTML( + const html = truncateHTML( node._docNode, - request.maxDepth, - request.maxLineLength, - request.maxChars, + maxDepth, + maxLineLength, + maxChars, ); - return { outerHTML: html }; } diff --git a/packages/@devtoolcss-mcp/styleUtils.js b/packages/@devtoolcss-mcp/styleUtils.js index 75dd22f..204ade8 100644 --- a/packages/@devtoolcss-mcp/styleUtils.js +++ b/packages/@devtoolcss-mcp/styleUtils.js @@ -1,157 +1,157 @@ -/** - * Filters computed styles based on provided property names or patterns - * @param {Object} styles - The computed styles object - * @param {Object} filter - Filter options - * @param {string[]} [filter.properties] - Specific property names to include - * @param {string[]} [filter.patterns] - Regex patterns to match property names - * @returns {Object} Filtered styles - */ -export function filterComputedStyle(styles, filter = {}) { - if (!filter || (!filter.properties && !filter.patterns && !filter.exclude)) { - return styles; - } - - const result = {}; - - // If specific properties are requested, only include those - if (filter.properties && Array.isArray(filter.properties)) { - for (const prop of filter.properties) { - if (prop in styles) { - result[prop] = styles[prop]; - } - } - return result; - } - - // If patterns are provided, match property names - let matchedProps = new Set(); - if (filter.patterns && Array.isArray(filter.patterns)) { - const regexes = filter.patterns.map((pattern) => new RegExp(pattern)); - for (const prop in styles) { - for (const regex of regexes) { - if (regex.test(prop)) { - matchedProps.add(prop); - break; - } - } - } - } else { - // No patterns, include all - matchedProps = new Set(Object.keys(styles)); - } - - return result; -} - /** * Filters matched styles response to reduce size - * @param {Object} matchedStyles - The matched styles object from chrome-inspector + * @param {Object} styles - The matched styles object from chrome-inspector * @param {Object} filter - Filter options - * @param {string[]} [filter.field] - Array of field types to include (e.g., ['inlineStyle', 'matchedCSSRules', 'inherited', 'pseudoElements']) - * @param {string[]} [filter.selectors] - Regex pattern for selector matching + * @param {string[]} [filter.selectors] - Regex pattern for selector matching in matched and pseudoElements * @param {string[]} [filter.properties] - Properties to include rules with matching properties + * @param {boolean} [filter.appliedOnly] - If true, only include applied properties * @returns {Object} Filtered matched styles */ -export function filterMatchedStyles(matchedStyles, filter = {}) { - if (!filter || Object.keys(filter).length === 0) { - return matchedStyles; - } - +export function filterMatchedStyles(styles, filter) { // Compile regex patterns if provided - const selectorRegexes = filter.selectors - ? filter.selectors.map((pattern) => new RegExp(pattern)) - : null; - if (filter.field) { - const fieldsToInclude = new Set(filter.field); - for (const key of Object.keys(matchedStyles)) { - if (!fieldsToInclude.has(key)) { - delete matchedStyles[key]; - } - } + if (filter.selectors) { + const selectorRegexes = filter.selectors + ? filter.selectors.map((pattern) => new RegExp(pattern)) + : null; + const filterRulesBySelectors = (rules) => { + return rules.filter((rule) => { + return selectorRegexes.some((regex) => + regex.test(rule.matchedSelectors.join(", ")), + ); + }); + }; + styles.matchedCSSRules = filterRulesBySelectors(styles.matchedCSSRules); + styles.pseudoElements = filterRulesBySelectors(styles.pseudoElements); } - const filterRuleBySelectors = (rules) => { - return rules.filter((rule) => { - return selectorRegexes.some((regex) => - regex.test(rule.matchedSelectors.join(", ")), - ); - }); - }; - - if (filter.selectors) { - if (matchedStyles.inherited) { - for (const inheritedItem of matchedStyles.inherited) { - inherited; + const filterAllProperties = (styles, filter) => { + const filterProperties = (properties) => { + return properties.filter(filter); + }; + for (const parentCSS of styles.inherited) { + parentCSS.inline = filterProperties(parentCSS.inline); + for (const rule of parentCSS.matched) { + rule.properties = filterProperties(rule.properties); } } + styles.attributes = filterProperties(styles.attributes); + for (const rule of styles.matchedCSSRules) { + rule.properties = filterProperties(rule.properties); + } + for (const rule of styles.pseudoElements) { + rule.properties = filterProperties(rule.properties); + } + styles.inline = filterProperties(styles.inline); + }; + + if (filter.properties) { + const propertiesSet = new Set(filter.properties); + const filter = (decl) => propertiesSet.has(decl.name); + filterAllProperties(styles, filter); } - // TODO: fix logic + if (filter.appliedOnly) { + const filter = (decl) => decl.applied === true; + filterAllProperties(styles, filter); + } - return result; + return styles; } -/** - * Simplifies matched styles to a more readable format - * @param {Object} matchedStyles - The matched styles object - * @returns {Object} Simplified styles object - */ -export function simplifyMatchedStyles(matchedStyles) { - const simplified = { - inline: {}, - matched: [], - inherited: [], +export function toStyleSheetText(styles, element, commentConfig = {}) { + let cssText = ""; + + const toCSSRuleText = (rule) => { + const allSelectorsStr = rule.allSelectors.join(", "); + const matchedSelectorsStr = rule.matchedSelectors.join(", "); + let css = ""; + // TODO: inspector need CSS.styleSheetAdded event to get origin info + //if (commentConfig.origin && rule.origin) { + // css += `/* Origin: ${rule.origin} */\n`; + //} + if ( + commentConfig.matchedSelectors && + matchedSelectorsStr !== allSelectorsStr + ) { + css += `/* Matched: ${matchedSelectorsStr} */\n`; + } + css += `${allSelectorsStr} {\n`; + for (const prop of rule.properties) { + css += ` ${prop.name}: ${prop.value};`; + if (commentConfig.applied && prop.applied) { + css += ` /* applied */`; + } + css += "\n"; + } + css += `}\n\n`; + return css; }; - // Extract inline styles - if (matchedStyles.inlineStyle?.cssProperties) { - for (const prop of matchedStyles.inlineStyle.cssProperties) { - simplified.inline[prop.name] = prop.value; - } + // inline + if (styles.inline.length > 0) { + cssText += toCSSRuleText({ + allSelectors: ["element.style"], + matchedSelectors: ["element.style"], + properties: styles.inline, + }); } - // Extract matched rules - if (matchedStyles.matchedCSSRules) { - for (const rule of matchedStyles.matchedCSSRules) { - const ruleObj = { - selector: rule.rule?.selectorList?.text || rule.rule?.selectorText, - properties: {}, - }; - - if (rule.rule?.style?.cssProperties) { - for (const prop of rule.rule.style.cssProperties) { - ruleObj.properties[prop.name] = prop.value; - } - } + // matched & pseudoElements + const allMatchedRules = [ + ...styles.matched, + ...styles.pseudoElements, + ].reverse(); - simplified.matched.push(ruleObj); - } + for (const rule of allMatchedRules) { + if (rule.properties.length > 0) cssText += toCSSRuleText(rule); } - // Extract inherited styles - if (matchedStyles.inherited) { - for (const inheritedItem of matchedStyles.inherited) { - const inheritedObj = { - from: inheritedItem.inlineStyle ? "inline" : "rules", - properties: {}, - }; + // attributes + if (styles.attributes.length > 0) { + const selectorPlaceholder = `${element.nodeName.toLowerCase()}[Attributes Style]`; + cssText += toCSSRuleText({ + allSelectors: [selectorPlaceholder], + matchedSelectors: [selectorPlaceholder], + properties: styles.attributes, + }); + } - if (inheritedItem.matchedCSSRules) { - for (const rule of inheritedItem.matchedCSSRules) { - if (rule.rule?.style?.cssProperties) { - for (const prop of rule.rule.style.cssProperties) { - inheritedObj.properties[prop.name] = prop.value; - } - } - } + for (const parentCSS of styles.inherited) { + const { inline, matched, distance } = parentCSS; + if ( + inline.length === 0 && + matched.every((rule) => rule.properties.length === 0) + ) + continue; + + const getParentSelector = (element, distance) => { + let parentNode = element; + for (let i = 0; i < distance; i++) { + parentNode = parentNode.parentNode; } - - if (Object.keys(inheritedObj.properties).length > 0) { - simplified.inherited.push(inheritedObj); + let parentSelector = parentNode.nodeName.toLowerCase(); + if (parentNode.id) { + parentSelector += `#${parentNode.id}`; + } else if (parentNode.classList && parentNode.classList.length > 0) { + parentSelector += `.${[...parentNode.classList].slice(0, 3).join(".")}`; + } + return parentSelector; + }; + cssText += `/* Inherited from ${getParentSelector(element, distance)} */\n`; + + if (inline.length > 0) { + cssText += toCSSRuleText({ + allSelectors: ["style attribute"], + matchedSelectors: ["style attribute"], + properties: inline, + }); + } + for (const rule of matched) { + if (rule.properties.length > 0) { + cssText += toCSSRuleText(rule); } } } - - return simplified; + return cssText; } From b4bc55df498501fa1520b68a9a82c3a70efad0fe Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Tue, 18 Nov 2025 12:29:38 +0800 Subject: [PATCH 25/36] feat(mcp): handle devtool tab and record 0 before inspector created --- packages/@devtoolcss-mcp/background.js | 23 ++++++- packages/@devtoolcss-mcp/devtools.html | 6 ++ packages/@devtoolcss-mcp/devtools.js | 54 ++++++++++++++++ packages/@devtoolcss-mcp/manifest.json | 3 +- .../@devtoolcss-mcp/offscreen_inspectors.js | 30 ++++++--- packages/@devtoolcss-mcp/package.json | 2 +- .../@devtoolcss-mcp/scripts/esbuild.config.js | 7 ++- packages/@devtoolcss-mcp/xpath.js | 62 +++++++++++++++++++ 8 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 packages/@devtoolcss-mcp/devtools.html create mode 100644 packages/@devtoolcss-mcp/devtools.js create mode 100644 packages/@devtoolcss-mcp/xpath.js diff --git a/packages/@devtoolcss-mcp/background.js b/packages/@devtoolcss-mcp/background.js index 9035dad..7a3486c 100644 --- a/packages/@devtoolcss-mcp/background.js +++ b/packages/@devtoolcss-mcp/background.js @@ -17,9 +17,21 @@ let settings = { pollingInterval: 2000, // 2 seconds }; +let inspectedTabId = null; + async function getActiveTabId() { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const activeTab = tabs[0]; + if (!activeTab) { + if (!inspectedTabId) + throw new Error( + "No active tab found. Click an element if you are in DevTools.", + ); + console.log(inspectedTabId); + return inspectedTabId; + } else if (activeTab.url && activeTab.url.startsWith("chrome://")) { + throw new Error("Cannot inspect chrome:// pages"); + } return activeTab.id; } @@ -27,6 +39,7 @@ chrome.tabs.onRemoved.addListener((tabId, removeInfo) => { console.log(`Tab ${tabId} closed`); // Clean up any resources related to this tab chrome.runtime.sendMessage({ + receiver: "offscreen", event: "TAB_CLOSED", tabId: tabId, }); @@ -35,6 +48,7 @@ chrome.tabs.onRemoved.addListener((tabId, removeInfo) => { chrome.debugger.onEvent.addListener((source, method, params) => { // Forward debugger events to offscreen inspector chrome.runtime.sendMessage({ + receiver: "offscreen", event: "DEBUGGER_EVENT", source, method, @@ -44,7 +58,9 @@ chrome.debugger.onEvent.addListener((source, method, params) => { // Handle debugger commands from offscreen chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { - switch (msg.type) { + if (msg.receiver !== "background") return; + + switch (msg.event) { case "DEBUGGER_SEND_COMMAND": try { chrome.debugger.sendCommand( @@ -77,6 +93,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { sendResponse({ error: error.message }); } break; + case "SET_INSPECTED_TAB_ID": + inspectedTabId = msg.tabId; + break; } return true; // Keep the message channel open for async response }); @@ -85,6 +104,8 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { async function handleRequest(request) { const activeTabId = await getActiveTabId(); const response = await chrome.runtime.sendMessage({ + receiver: "offscreen", + event: "REQUEST", ...request, tabId: activeTabId, }); diff --git a/packages/@devtoolcss-mcp/devtools.html b/packages/@devtoolcss-mcp/devtools.html new file mode 100644 index 0000000..5dbb29f --- /dev/null +++ b/packages/@devtoolcss-mcp/devtools.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/@devtoolcss-mcp/devtools.js b/packages/@devtoolcss-mcp/devtools.js new file mode 100644 index 0000000..f01a10e --- /dev/null +++ b/packages/@devtoolcss-mcp/devtools.js @@ -0,0 +1,54 @@ +// Modified From https://github.com/devtoolcss/chrome-inspector/blob/main/extension/devtools.js +// Licensed under MIT License +// +// Added SET_INSPECTED_TAB_ID messaging to inform background script of the inspected tab ID + +import { getAbsoluteXPath } from "./xpath.js"; + +function sendSelector() { + // TODO: debug mode + const expression = ` +(function() { + ${getAbsoluteXPath.toString()} + + const sender = $0.ownerDocument.defaultView.__chrome_inspector_send_$0_xpath; + try{ + const xpath = getAbsoluteXPath($0); + if (typeof sender !== "function") return xpath; + sender(xpath); + } catch {} +})(); +`; + + chrome.devtools.inspectedWindow.eval( + expression, + {}, + (result, exceptionInfo) => { + if (result) { + chrome.runtime.sendMessage({ + receiver: "offscreen", + event: "SET_INSPECTED_TAB_$0", + tabId: chrome.devtools.inspectedWindow.tabId, + xpath: result, + }); + } else if (exceptionInfo && exceptionInfo.isException) { + console.error( + `Unable to evaluate selection change script.`, + exceptionInfo, + ); + } + }, + ); + + chrome.runtime.sendMessage({ + receiver: "background", + event: "SET_INSPECTED_TAB_ID", + tabId: chrome.devtools.inspectedWindow.tabId, + }); +} + +if (chrome?.devtools?.panels?.elements) { + chrome.devtools.panels.elements.onSelectionChanged.addListener(sendSelector); +} else { + console.warn(`chrome.devtools API is not available in this context.`); +} diff --git a/packages/@devtoolcss-mcp/manifest.json b/packages/@devtoolcss-mcp/manifest.json index 3d7d166..5e9dc5e 100644 --- a/packages/@devtoolcss-mcp/manifest.json +++ b/packages/@devtoolcss-mcp/manifest.json @@ -7,7 +7,8 @@ "service_worker": "background.js", "type": "module" }, - "permissions": ["storage", "debugger", "offscreen"], + "devtools_page": "devtools.html", + "permissions": ["tabs", "storage", "debugger", "offscreen"], "action": { "default_popup": "popup.html", "default_title": "DevtoolCSS MCP Settings" diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js index beb5e14..6387d62 100644 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -12,7 +12,8 @@ const chromeDebugger = { async attach(target, version, callback = () => {}) { chrome.runtime.sendMessage( { - type: "DEBUGGER_ATTACH", + receiver: "background", + event: "DEBUGGER_ATTACH", target, }, (response) => { @@ -27,7 +28,8 @@ const chromeDebugger = { async detach(target, callback = () => {}) { chrome.runtime.sendMessage( { - type: "DEBUGGER_DETACH", + receiver: "background", + event: "DEBUGGER_DETACH", target, }, (response) => { @@ -53,7 +55,8 @@ const chromeDebugger = { return new Promise((resolve, reject) => { chrome.runtime.sendMessage( { - type: "DEBUGGER_SEND_COMMAND", + receiver: "background", + event: "DEBUGGER_SEND_COMMAND", target, method, params, @@ -84,12 +87,16 @@ const chromeDebugger = { // inspector management per tab const inspectors = {}; +// record $0 xpaths for tabs not having inspector yet +const tab$0Map = new Map(); + async function getInspector(tabId) { if (!inspectors[tabId]) { await chromeDebugger.attach({ tabId }, "1.3"); inspectors[tabId] = await Inspector.fromChromeDebugger( chromeDebugger, tabId, + { $0XPath: tab$0Map.get(tabId) }, ); } return inspectors[tabId]; @@ -119,6 +126,7 @@ function getNode(uid, inspector) { */ async function serveRequest(request) { + console.log("serveRequest - request:", request); const inspector = await getInspector(request.tabId); switch (request.tool) { case "getNodes": { @@ -216,11 +224,15 @@ async function serveRequest(request) { // listener must be sync, return true to indicate async response chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg.receiver !== "offscreen") return; + switch (msg.event) { case "TAB_CLOSED": - chromeDebugger.detach({ tabId: msg.tabId }); - delete inspectors[msg.tabId]; - biMap.cleanUp(); + if (inspectors[msg.tabId]) { + chromeDebugger.detach({ tabId: msg.tabId }); + delete inspectors[msg.tabId]; + biMap.cleanUp(); + } break; case "DEBUGGER_EVENT": @@ -228,7 +240,11 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { chromeDebugger._dispatchEvent(source, method, params); break; - default: // request + case "SET_INSPECTED_TAB_$0": + tab$0Map.set(msg.tabId, msg.xpath); + break; + + case "REQUEST": serveRequest(msg).then(sendResponse); return true; // Keep the message channel open for async response } diff --git a/packages/@devtoolcss-mcp/package.json b/packages/@devtoolcss-mcp/package.json index 0d65de0..c307e05 100644 --- a/packages/@devtoolcss-mcp/package.json +++ b/packages/@devtoolcss-mcp/package.json @@ -11,7 +11,7 @@ "sync:manifest": "node scripts/sync-manifest.js" }, "dependencies": { - "chrome-inspector": "^1.0.4", + "chrome-inspector": "^1.0.6", "ws": "^8.18.3" }, "devDependencies": { diff --git a/packages/@devtoolcss-mcp/scripts/esbuild.config.js b/packages/@devtoolcss-mcp/scripts/esbuild.config.js index 884d532..bfbd6f5 100644 --- a/packages/@devtoolcss-mcp/scripts/esbuild.config.js +++ b/packages/@devtoolcss-mcp/scripts/esbuild.config.js @@ -3,7 +3,12 @@ import { build } from "esbuild"; const isProd = process.env.NODE_ENV === "production"; const commonOptions = { - entryPoints: ["popup.js", "background.js", "offscreen_inspectors.js"], + entryPoints: [ + "popup.js", + "background.js", + "offscreen_inspectors.js", + "devtools.js", + ], bundle: true, outdir: "dist", format: "esm", diff --git a/packages/@devtoolcss-mcp/xpath.js b/packages/@devtoolcss-mcp/xpath.js new file mode 100644 index 0000000..356ad37 --- /dev/null +++ b/packages/@devtoolcss-mcp/xpath.js @@ -0,0 +1,62 @@ +// From https://github.com/devtoolcss/chrome-inspector/blob/main/extension/xpath.js +// Licensed under MIT License + +export function getAbsoluteXPath(node) { + if (!node) return ""; + const pathSegments = []; + + while (node && node.nodeType !== Node.DOCUMENT_NODE) { + let segment = ""; + let index = 1; + let sibling = node.previousSibling; + + switch (node.nodeType) { + case Node.ELEMENT_NODE: { + const ns = node.namespaceURI; + let prefix = ""; + if (ns === "http://www.w3.org/2000/svg") prefix = "svg:"; + else if (ns === "http://www.w3.org/1999/xhtml") prefix = ""; // default HTML + + while (sibling) { + if ( + sibling.nodeType === Node.ELEMENT_NODE && + sibling.nodeName === node.nodeName + ) + index++; + sibling = sibling.previousSibling; + } + + segment = `${prefix}${node.localName}[${index}]`; + break; + } + + case Node.TEXT_NODE: + while (sibling) { + if (sibling.nodeType === Node.TEXT_NODE) index++; + sibling = sibling.previousSibling; + } + segment = `text()[${index}]`; + break; + + case Node.COMMENT_NODE: + while (sibling) { + if (sibling.nodeType === Node.COMMENT_NODE) index++; + sibling = sibling.previousSibling; + } + segment = `comment()[${index}]`; + break; + + case Node.ATTRIBUTE_NODE: + const ownerPath = getAbsoluteXPath(node.ownerElement); + return `${ownerPath}/@${node.nodeName}`; + + default: + segment = `node()[${index}]`; + } + + pathSegments.unshift(segment); + node = node.parentNode; + } + + return "/" + pathSegments.join("/"); +} From 4681a5c0fd02cc937cd84c4c6d35b4937695fe67 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Wed, 19 Nov 2025 09:59:17 +0800 Subject: [PATCH 26/36] feat: handle debugger detach --- packages/@devtoolcss-mcp/background.js | 9 +++++++++ packages/@devtoolcss-mcp/offscreen_inspectors.js | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/@devtoolcss-mcp/background.js b/packages/@devtoolcss-mcp/background.js index 7a3486c..863949d 100644 --- a/packages/@devtoolcss-mcp/background.js +++ b/packages/@devtoolcss-mcp/background.js @@ -56,6 +56,15 @@ chrome.debugger.onEvent.addListener((source, method, params) => { }); }); +chrome.debugger.onDetach.addListener((source, reason) => { + chrome.runtime.sendMessage({ + receiver: "offscreen", + event: "DEBUGGER_DETACHED", + tabId: source.tabId, + reason, + }); +}); + // Handle debugger commands from offscreen chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.receiver !== "background") return; diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js index 6387d62..f4fb894 100644 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -235,6 +235,16 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } break; + case "DEBUGGER_DETACHED": + if (inspectors[msg.tabId]) { + delete inspectors[msg.tabId]; + biMap.cleanUp(); + console.log( + `Inspector for tab ${msg.tabId} detached for ${msg.reason}`, + ); + } + break; + case "DEBUGGER_EVENT": const { source, method, params } = msg; chromeDebugger._dispatchEvent(source, method, params); From 35f733f9997024d01bdd2fe128d10821adf492d6 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Wed, 19 Nov 2025 11:05:34 +0800 Subject: [PATCH 27/36] feat: error handling --- packages/@devtoolcss-mcp/BiWeakNodeMap.js | 1 + packages/@devtoolcss-mcp/DOMExpression.js | 4 +- packages/@devtoolcss-mcp/background.js | 72 ++++++++----------- .../@devtoolcss-mcp/offscreen_inspectors.js | 27 +++---- 4 files changed, 49 insertions(+), 55 deletions(-) diff --git a/packages/@devtoolcss-mcp/BiWeakNodeMap.js b/packages/@devtoolcss-mcp/BiWeakNodeMap.js index ad311bc..f530f7d 100644 --- a/packages/@devtoolcss-mcp/BiWeakNodeMap.js +++ b/packages/@devtoolcss-mcp/BiWeakNodeMap.js @@ -38,6 +38,7 @@ export class BiWeakNodeMap { } cleanUp() { + // FIXME: didn't really cleanup deleted inspector's nodes for (const [id, ref] of this._idToRef.entries()) { if (ref.deref() === undefined) { this._idToRef.delete(id); diff --git a/packages/@devtoolcss-mcp/DOMExpression.js b/packages/@devtoolcss-mcp/DOMExpression.js index ce9e25b..9545c13 100644 --- a/packages/@devtoolcss-mcp/DOMExpression.js +++ b/packages/@devtoolcss-mcp/DOMExpression.js @@ -86,7 +86,7 @@ export async function evaluateDOMExpression( const targetNodeName = expression.split(".")[0]; const targetNode = getNode(targetNodeName, inspector); if (!targetNode) { - return { error: `Target node '${targetNodeName}' not found` }; + throw new Error(`Target node '${targetNodeName}' not found`); } const remainingExpression = expression.slice(targetNodeName.length); @@ -109,6 +109,6 @@ export async function evaluateDOMExpression( const uids = nodes.map((node) => setNode(node)); return { uids }; } catch (error) { - return { error: `Failed to evaluate expression: ${error.message}` }; + throw new Error(`Failed to evaluate "${expression}": ${error.message}`); } } diff --git a/packages/@devtoolcss-mcp/background.js b/packages/@devtoolcss-mcp/background.js index 863949d..aaba1bc 100644 --- a/packages/@devtoolcss-mcp/background.js +++ b/packages/@devtoolcss-mcp/background.js @@ -25,12 +25,11 @@ async function getActiveTabId() { if (!activeTab) { if (!inspectedTabId) throw new Error( - "No active tab found. Click an element if you are in DevTools.", + "No active tab found. Tell user to click an element if in DevTools.", ); - console.log(inspectedTabId); return inspectedTabId; } else if (activeTab.url && activeTab.url.startsWith("chrome://")) { - throw new Error("Cannot inspect chrome:// pages"); + throw new Error("Cannot access a chrome:// URL"); } return activeTab.id; } @@ -71,36 +70,22 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { switch (msg.event) { case "DEBUGGER_SEND_COMMAND": - try { - chrome.debugger.sendCommand( - msg.target, - msg.method, - msg.params, - (result) => { - sendResponse({ result }); - }, - ); - } catch (error) { - sendResponse({ error: error.message }); - } + chrome.debugger + .sendCommand(msg.target, msg.method, msg.params) + .then((result) => sendResponse({ result })) + .catch((error) => sendResponse({ error: error.message })); break; case "DEBUGGER_ATTACH": - try { - chrome.debugger.attach(msg.target, "1.3", () => { - sendResponse({ success: true }); - }); - } catch (error) { - sendResponse({ error: error.message }); - } + chrome.debugger + .attach(msg.target, "1.3") + .then(() => sendResponse({ success: true })) + .catch((error) => sendResponse({ error: error.message })); break; case "DEBUGGER_DETACH": - try { - chrome.debugger.detach(msg.target, () => { - sendResponse({ success: true }); - }); - } catch (error) { - sendResponse({ error: error.message }); - } + chrome.debugger + .detach(msg.target) + .then(() => sendResponse({ success: true })) + .catch((error) => sendResponse({ error: error.message })); break; case "SET_INSPECTED_TAB_ID": inspectedTabId = msg.tabId; @@ -110,15 +95,15 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { }); // Main serving logic -async function handleRequest(request) { - const activeTabId = await getActiveTabId(); +async function serveRequest(request) { + if (request.tabId === undefined) { + request.tabId = await getActiveTabId(); + } const response = await chrome.runtime.sendMessage({ receiver: "offscreen", event: "REQUEST", - ...request, - tabId: activeTabId, + request, }); - console.log("response received:", response); return response; } @@ -187,21 +172,26 @@ function connectWebSocket() { ws.onmessage = async (event) => { console.log("[WS] Request received:", event.data); + let req; + try { + req = JSON.parse(event.data); + } catch (e) { + console.error(`[WS] Failed to parse message ${event.data}:`, e); + ws.send(JSON.stringify({ error: e.message || String(e) })); + return; + } + try { - const req = JSON.parse(event.data); - const response = await handleRequest(req); + const response = await serveRequest(req); console.log("response:", response); ws.send(JSON.stringify({ id: req.id, ...response })); } catch (e) { - console.error("[WS] Failed to parse message:", e); + console.error(`[WS] Failed to serve message ${event.data}:`, e); + ws.send(JSON.stringify({ id: req.id, error: e.message || String(e) })); } }; ws.onerror = (event) => { - if (ws.readyState === WebSocket.CLOSED) { - // Connection lost, will be handled by onclose - return; - } console.error("[WS] Error occurred:", event); }; diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js index f4fb894..264733d 100644 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -125,14 +125,14 @@ function getNode(uid, inspector) { * @returns {Promise<{uids: string[]} | {error: string}>} */ -async function serveRequest(request) { - console.log("serveRequest - request:", request); +async function processRequest(request) { + console.log("processRequest - request:", request); const inspector = await getInspector(request.tabId); switch (request.tool) { case "getNodes": { // Unified node retrieval using DOM expression syntax if (!request.expression) { - return { error: "Missing 'expression' parameter" }; + throw new Error("Missing 'expression' parameter"); } return await evaluateDOMExpression( request.expression, @@ -151,7 +151,7 @@ async function serveRequest(request) { } = request; const node = getNode(uid, inspector); if (!node) { - return { error: `Node not found for uid: ${uid}` }; + throw new Error(`Node not found for uid: ${uid}`); } const options = { parseOptions: { removeUnusedVar }, @@ -179,7 +179,7 @@ async function serveRequest(request) { case "getComputedStyle": { const node = getNode(request.uid, inspector); if (!node) { - return { error: "Node not found for uid: " + request.uid }; + throw new Error("Node not found for uid: " + request.uid); } const styles = await node.getComputedStyle(); @@ -191,7 +191,7 @@ async function serveRequest(request) { return { styles: filtered }; } - case "outerHTML": { + case "getOuterHTML": { // some safe defaults const { uid, @@ -201,11 +201,9 @@ async function serveRequest(request) { } = request; const node = getNode(uid, inspector); if (!node) { - return { error: `Node not found for uid: ${uid}` }; + throw new Error(`Node not found for uid: ${uid}`); } else if (!node.tracked) { - return { - error: `Node is no longer existed for uid: ${uid}`, - }; + throw new Error(`Node is no longer existed for uid: ${uid}`); } // Apply depth and line length controls if provided const html = truncateHTML( @@ -214,11 +212,12 @@ async function serveRequest(request) { maxLineLength, maxChars, ); + console.log("serveRequest - getOuterHTML html:", html); return { outerHTML: html }; } default: - return { error: "Unknown tool: " + request.tool }; + throw new Error("Unknown tool: " + request.tool); } } @@ -255,7 +254,11 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { break; case "REQUEST": - serveRequest(msg).then(sendResponse); + processRequest(msg.request) + .then(sendResponse) + .catch((error) => { + sendResponse({ error: error.message || String(error) }); + }); return true; // Keep the message channel open for async response } }); From c73e4dd82de207f88a8e06ecc57cf8af5066398d Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Wed, 19 Nov 2025 11:07:53 +0800 Subject: [PATCH 28/36] feat: getTabs to allow working on non-active tabs --- packages/@devtoolcss-mcp/background.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/@devtoolcss-mcp/background.js b/packages/@devtoolcss-mcp/background.js index aaba1bc..a54f517 100644 --- a/packages/@devtoolcss-mcp/background.js +++ b/packages/@devtoolcss-mcp/background.js @@ -94,8 +94,34 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { return true; // Keep the message channel open for async response }); +function formatRelativeTime(timestamp) { + const diffMs = Date.now() - timestamp; + const diffSec = Math.floor(diffMs / 1000); + const hr = Math.floor(diffSec / 3600); + const min = Math.floor((diffSec % 3600) / 60); + const sec = diffSec % 60; + let parts = []; + if (hr > 0) return `${hr}hr ago`; + if (min > 0) return `${min}min ago`; + return `${sec}sec ago`; +} + // Main serving logic async function serveRequest(request) { + if (request.tool === "getTabs") { + const tabs = await chrome.tabs.query({}); + return { + tabs: tabs.map((tab) => ({ + id: tab.id, + title: tab.title, + url: tab.url, + active: tab.active ? true : undefined, + lastAccessed: formatRelativeTime(tab.lastAccessed), + })), + }; + } + + // other inspector requests if (request.tabId === undefined) { request.tabId = await getActiveTabId(); } From fa22477e8dcdd0b28f1a0cf2d7774888420d597a Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Wed, 19 Nov 2025 11:35:28 +0800 Subject: [PATCH 29/36] fix: debugger API --- packages/@devtoolcss-mcp/background.js | 4 +- .../@devtoolcss-mcp/offscreen_inspectors.js | 83 ++++++++----------- 2 files changed, 35 insertions(+), 52 deletions(-) diff --git a/packages/@devtoolcss-mcp/background.js b/packages/@devtoolcss-mcp/background.js index a54f517..4e1f929 100644 --- a/packages/@devtoolcss-mcp/background.js +++ b/packages/@devtoolcss-mcp/background.js @@ -78,13 +78,13 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { case "DEBUGGER_ATTACH": chrome.debugger .attach(msg.target, "1.3") - .then(() => sendResponse({ success: true })) + .then((result) => sendResponse({ result })) .catch((error) => sendResponse({ error: error.message })); break; case "DEBUGGER_DETACH": chrome.debugger .detach(msg.target) - .then(() => sendResponse({ success: true })) + .then((result) => sendResponse({ result })) .catch((error) => sendResponse({ error: error.message })); break; case "SET_INSPECTED_TAB_ID": diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js index 264733d..1aa03b2 100644 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -5,40 +5,45 @@ import { filterMatchedStyles, toStyleSheetText } from "./styleUtils"; import { evaluateDOMExpression } from "./DOMExpression"; // Create a chromeDebugger wrapper that works in offscreen context +async function sendDebuggerMessage(payload) { + const response = await chrome.runtime.sendMessage({ + receiver: "background", + ...payload, + }); + + if (response?.error) { + throw new Error(response.error); + } else { + return response?.result; + } +} + const chromeDebugger = { // Event listeners storage _listeners: new Set(), - async attach(target, version, callback = () => {}) { - chrome.runtime.sendMessage( - { - receiver: "background", - event: "DEBUGGER_ATTACH", - target, - }, - (response) => { - if (response?.error) { - throw new Error(response.error); - } - callback(); - }, - ); + async attach(target, version) { + return sendDebuggerMessage({ + event: "DEBUGGER_ATTACH", + target, + }); }, - async detach(target, callback = () => {}) { - chrome.runtime.sendMessage( - { - receiver: "background", - event: "DEBUGGER_DETACH", - target, - }, - (response) => { - if (response?.error) { - throw new Error(response.error); - } - callback(); - }, - ); + async detach(target) { + return sendDebuggerMessage({ + event: "DEBUGGER_DETACH", + target, + }); + }, + + // Send command to the actual chrome.debugger in background.js + async sendCommand(target, method, params) { + return sendDebuggerMessage({ + event: "DEBUGGER_SEND_COMMAND", + target, + method, + params, + }); }, onEvent: { @@ -50,28 +55,6 @@ const chromeDebugger = { }, }, - // Send command to the actual chrome.debugger in background.js - async sendCommand(target, method, params) { - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage( - { - receiver: "background", - event: "DEBUGGER_SEND_COMMAND", - target, - method, - params, - }, - (response) => { - if (response?.error) { - reject(new Error(response.error)); - } else { - resolve(response?.result); - } - }, - ); - }); - }, - // Internal method to dispatch events to listeners _dispatchEvent(source, method, params) { for (const listener of chromeDebugger._listeners) { From 87c7ca8c91b5db9ea9c1ae4568899813c298fdf0 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Wed, 19 Nov 2025 13:00:49 +0800 Subject: [PATCH 30/36] refactor: split maintaince logic to classes --- .../@devtoolcss-mcp/ChromeDebuggerBridge.js | 75 +++++++++ packages/@devtoolcss-mcp/DOMExpression.js | 7 +- packages/@devtoolcss-mcp/InspectorManager.js | 55 +++++++ .../{BiWeakNodeMap.js => NodeUidManager.js} | 20 +-- packages/@devtoolcss-mcp/background.js | 2 +- .../@devtoolcss-mcp/offscreen_inspectors.js | 143 ++---------------- 6 files changed, 154 insertions(+), 148 deletions(-) create mode 100644 packages/@devtoolcss-mcp/ChromeDebuggerBridge.js create mode 100644 packages/@devtoolcss-mcp/InspectorManager.js rename packages/@devtoolcss-mcp/{BiWeakNodeMap.js => NodeUidManager.js} (75%) diff --git a/packages/@devtoolcss-mcp/ChromeDebuggerBridge.js b/packages/@devtoolcss-mcp/ChromeDebuggerBridge.js new file mode 100644 index 0000000..1a63ea9 --- /dev/null +++ b/packages/@devtoolcss-mcp/ChromeDebuggerBridge.js @@ -0,0 +1,75 @@ +async function sendDebuggerMessage(payload) { + const response = await chrome.runtime.sendMessage({ + receiver: "background", + ...payload, + }); + + if (response?.error) { + throw new Error(response.error); + } else { + return response?.result; + } +} + +// A chrome.debugger wrapper implemented in runtime.messaging for offscreen context +class ChromeDebuggerBridge { + _listeners = new Set(); + + constructor() { + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + switch (msg.event) { + case "DEBUGGER_EVENT": + const { source, method, params } = msg; + this._dispatchEvent(source, method, params); + break; + } + }); + } + + async attach(target, version) { + return sendDebuggerMessage({ + event: "DEBUGGER_ATTACH", + target, + }); + } + + async detach(target) { + return sendDebuggerMessage({ + event: "DEBUGGER_DETACH", + target, + }); + } + + // Send command to the actual chrome.debugger in background.js + async sendCommand(target, method, params) { + return sendDebuggerMessage({ + event: "DEBUGGER_SEND_COMMAND", + target, + method, + params, + }); + } + + onEvent = { + addListener: (callback) => { + this._listeners.add(callback); + }, + removeListener: (callback) => { + this._listeners.delete(callback); + }, + }; + + // Internal method to dispatch events to listeners + _dispatchEvent(source, method, params) { + for (const listener of this._listeners) { + try { + listener(source, method, params); + } catch (e) { + console.error("Error in debugger event listener:", e); + } + } + } +} + +// A chrome.debugger wrapper implemented in runtime.messaging for offscreen context +export const chromeDebugger = new ChromeDebuggerBridge(); diff --git a/packages/@devtoolcss-mcp/DOMExpression.js b/packages/@devtoolcss-mcp/DOMExpression.js index 9545c13..65dc43d 100644 --- a/packages/@devtoolcss-mcp/DOMExpression.js +++ b/packages/@devtoolcss-mcp/DOMExpression.js @@ -79,12 +79,11 @@ function evalMethods(target, expr) { export async function evaluateDOMExpression( expression, inspector, - getNode, - setNode, + nodeManager, ) { expression = expression.trim(); const targetNodeName = expression.split(".")[0]; - const targetNode = getNode(targetNodeName, inspector); + const targetNode = nodeManager.getNode(targetNodeName, inspector); if (!targetNode) { throw new Error(`Target node '${targetNodeName}' not found`); } @@ -106,7 +105,7 @@ export async function evaluateDOMExpression( nodes = [result]; } - const uids = nodes.map((node) => setNode(node)); + const uids = nodes.map((node) => nodeManager.setNode(node)); return { uids }; } catch (error) { throw new Error(`Failed to evaluate "${expression}": ${error.message}`); diff --git a/packages/@devtoolcss-mcp/InspectorManager.js b/packages/@devtoolcss-mcp/InspectorManager.js new file mode 100644 index 0000000..245955a --- /dev/null +++ b/packages/@devtoolcss-mcp/InspectorManager.js @@ -0,0 +1,55 @@ +import { chromeDebugger } from "./ChromeDebuggerBridge"; +import { Inspector } from "chrome-inspector"; + +// inspector management per tab +export class InspectorManager { + constructor() { + this.inspectors = {}; + // record $0 xpaths for tabs not having inspector yet + this.tab$0Map = new Map(); + + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + switch (msg.event) { + case "TAB_CLOSED": + if (this.get(msg.tabId)) { + chromeDebugger.detach({ tabId: msg.tabId }); + this.remove(msg.tabId); + // TODO: cleanup nodeUidManager + } + break; + + case "DEBUGGER_DETACHED": + if (this.get(msg.tabId)) { + this.remove(msg.tabId); + console.log( + `Inspector for tab ${msg.tabId} detached for ${msg.reason}`, + ); + // TODO: cleanup nodeUidManager + } + break; + + case "SET_INSPECTED_TAB_$0": + this.tab$0Map.set(tabId, xpath); + break; + } + }); + } + + async create(tabId) { + await chromeDebugger.attach({ tabId }, "1.3"); + this.inspectors[tabId] = await Inspector.fromChromeDebugger( + chromeDebugger, + tabId, + { $0XPath: this.tab$0Map.get(tabId) }, + ); + return this.inspectors[tabId]; + } + + get(tabId) { + return this.inspectors[tabId]; + } + + remove(tabId) { + delete this.inspectors[tabId]; + } +} diff --git a/packages/@devtoolcss-mcp/BiWeakNodeMap.js b/packages/@devtoolcss-mcp/NodeUidManager.js similarity index 75% rename from packages/@devtoolcss-mcp/BiWeakNodeMap.js rename to packages/@devtoolcss-mcp/NodeUidManager.js index f530f7d..2d989a9 100644 --- a/packages/@devtoolcss-mcp/BiWeakNodeMap.js +++ b/packages/@devtoolcss-mcp/NodeUidManager.js @@ -1,4 +1,5 @@ -export class BiWeakNodeMap { +// generate and record uids for node. +export class NodeUidManager { constructor() { this._nodeCounters = new Map(); // nodeName -> counter this._idToRef = new Map(); // id -> WeakRef(node) @@ -12,7 +13,7 @@ export class BiWeakNodeMap { return `${nodeName}_${counter}`; } - set(node) { + setNode(node) { if (this._nodeToId.has(node)) { return this._nodeToId.get(node); } @@ -22,21 +23,22 @@ export class BiWeakNodeMap { return id; } - getNode(id) { - const ref = this._idToRef.get(id); + getNode(uid, inspector) { + // predefined + if (uid === "document") return inspector.document; + if (uid === "$0") return inspector.$0; + + // from map + const ref = this._idToRef.get(uid); if (!ref) return undefined; const node = ref.deref(); if (!node) { // Node GC'd, clean up stale entry - this._idToRef.delete(id); + this._idToRef.delete(uid); } return node; } - getId(node) { - return this._nodeToId.get(node); - } - cleanUp() { // FIXME: didn't really cleanup deleted inspector's nodes for (const [id, ref] of this._idToRef.entries()) { diff --git a/packages/@devtoolcss-mcp/background.js b/packages/@devtoolcss-mcp/background.js index 4e1f929..c99dac7 100644 --- a/packages/@devtoolcss-mcp/background.js +++ b/packages/@devtoolcss-mcp/background.js @@ -212,7 +212,7 @@ function connectWebSocket() { console.log("response:", response); ws.send(JSON.stringify({ id: req.id, ...response })); } catch (e) { - console.error(`[WS] Failed to serve message ${event.data}:`, e); + console.log(`[WS] Failed to serve message ${event.data}:`, e); ws.send(JSON.stringify({ id: req.id, error: e.message || String(e) })); } }; diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js index 1aa03b2..74bda17 100644 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ b/packages/@devtoolcss-mcp/offscreen_inspectors.js @@ -1,116 +1,19 @@ -import { Inspector } from "chrome-inspector"; -import { BiWeakNodeMap } from "./BiWeakNodeMap"; import { truncateHTML } from "./htmlUtils"; import { filterMatchedStyles, toStyleSheetText } from "./styleUtils"; import { evaluateDOMExpression } from "./DOMExpression"; -// Create a chromeDebugger wrapper that works in offscreen context -async function sendDebuggerMessage(payload) { - const response = await chrome.runtime.sendMessage({ - receiver: "background", - ...payload, - }); +import { NodeUidManager } from "./NodeUidManager"; +import { InspectorManager } from "./InspectorManager"; - if (response?.error) { - throw new Error(response.error); - } else { - return response?.result; - } -} - -const chromeDebugger = { - // Event listeners storage - _listeners: new Set(), - - async attach(target, version) { - return sendDebuggerMessage({ - event: "DEBUGGER_ATTACH", - target, - }); - }, - - async detach(target) { - return sendDebuggerMessage({ - event: "DEBUGGER_DETACH", - target, - }); - }, - - // Send command to the actual chrome.debugger in background.js - async sendCommand(target, method, params) { - return sendDebuggerMessage({ - event: "DEBUGGER_SEND_COMMAND", - target, - method, - params, - }); - }, - - onEvent: { - addListener(callback) { - chromeDebugger._listeners.add(callback); - }, - removeListener(callback) { - chromeDebugger._listeners.delete(callback); - }, - }, - - // Internal method to dispatch events to listeners - _dispatchEvent(source, method, params) { - for (const listener of chromeDebugger._listeners) { - try { - listener(source, method, params); - } catch (e) { - console.error("Error in debugger event listener:", e); - } - } - }, -}; - -// inspector management per tab -const inspectors = {}; - -// record $0 xpaths for tabs not having inspector yet -const tab$0Map = new Map(); - -async function getInspector(tabId) { - if (!inspectors[tabId]) { - await chromeDebugger.attach({ tabId }, "1.3"); - inspectors[tabId] = await Inspector.fromChromeDebugger( - chromeDebugger, - tabId, - { $0XPath: tab$0Map.get(tabId) }, - ); - } - return inspectors[tabId]; -} - -const biMap = new BiWeakNodeMap(); - -// handling predefined nodes -function getNode(uid, inspector) { - if (uid === "document") return inspector.document; - if (uid === "$0") return inspector.$0; - return biMap.getNode(uid); -} - -/** - * Evaluates a DOM expression by replacing UID variables with actual nodes - * Examples: - * "html" -> predefined html element (querySelector('html')) - * "html.querySelectorAll('div.container')[0]" -> query from html - * "uid_1.querySelectorAll('span')[0]" -> query from node - * "uid_1.parentNode" -> get parent node - * "uid_1.children[1]" -> get second child - * - * @param {Inspector} inspector - The inspector instance - * @param {string} expression - DOM expression to evaluate - * @returns {Promise<{uids: string[]} | {error: string}>} - */ +const inspectorManager = new InspectorManager(); +const nodeManager = new NodeUidManager(); async function processRequest(request) { console.log("processRequest - request:", request); - const inspector = await getInspector(request.tabId); + const inspector = + inspectorManager.get(request.tabId) ?? + (await inspectorManager.create(request.tabId)); + switch (request.tool) { case "getNodes": { // Unified node retrieval using DOM expression syntax @@ -120,8 +23,7 @@ async function processRequest(request) { return await evaluateDOMExpression( request.expression, inspector, - getNode, - biMap.set.bind(biMap), + nodeManager, ); } @@ -209,33 +111,6 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.receiver !== "offscreen") return; switch (msg.event) { - case "TAB_CLOSED": - if (inspectors[msg.tabId]) { - chromeDebugger.detach({ tabId: msg.tabId }); - delete inspectors[msg.tabId]; - biMap.cleanUp(); - } - break; - - case "DEBUGGER_DETACHED": - if (inspectors[msg.tabId]) { - delete inspectors[msg.tabId]; - biMap.cleanUp(); - console.log( - `Inspector for tab ${msg.tabId} detached for ${msg.reason}`, - ); - } - break; - - case "DEBUGGER_EVENT": - const { source, method, params } = msg; - chromeDebugger._dispatchEvent(source, method, params); - break; - - case "SET_INSPECTED_TAB_$0": - tab$0Map.set(msg.tabId, msg.xpath); - break; - case "REQUEST": processRequest(msg.request) .then(sendResponse) From bff8310165943f318e80f562b5860932f42b1939 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Wed, 19 Nov 2025 16:25:50 +0800 Subject: [PATCH 31/36] chore: install packages --- packages/@devtoolcss-mcp/package.json | 4 +++- pnpm-lock.yaml | 23 +++++++++++++++++++++-- pnpm-workspace.yaml | 4 ++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/@devtoolcss-mcp/package.json b/packages/@devtoolcss-mcp/package.json index c307e05..53130ce 100644 --- a/packages/@devtoolcss-mcp/package.json +++ b/packages/@devtoolcss-mcp/package.json @@ -12,11 +12,13 @@ }, "dependencies": { "chrome-inspector": "^1.0.6", + "js-beautify": "^1.15.4", "ws": "^8.18.3" }, "devDependencies": { "@types/chrome": "^0.1.27", "chokidar": "^4.0.3", - "esbuild": "^0.27.0" + "esbuild": "^0.27.0", + "typescript": "^5.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6ecad1..e95c0cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,8 +37,11 @@ importers: packages/@devtoolcss-mcp: dependencies: chrome-inspector: - specifier: link:/home/bill/chrome-inspector/ - version: link:../../../chrome-inspector + specifier: ^1.0.6 + version: 1.0.6 + js-beautify: + specifier: ^1.15.4 + version: 1.15.4 ws: specifier: ^8.18.3 version: 8.18.3 @@ -52,6 +55,9 @@ importers: esbuild: specifier: ^0.27.0 version: 0.27.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 packages/@devtoolcss-parser: devDependencies: @@ -615,6 +621,9 @@ packages: chrome-inspector@1.0.3: resolution: {integrity: sha512-rF2YrgaCzZNOimM91ESna+4+dTHBMJlDViDUvkwVHsEOxZzrPoeZ4mldP5zOZYFhWpgDrDnIMUlycEKsbq56Ag==} + chrome-inspector@1.0.6: + resolution: {integrity: sha512-5OhOccte20uGlI2WHqhtfOq6/NX3wTCmogfjUKqqfpLu6Ikp+0cjdfSmjibPmqYOW10BsUAQiQv8ACna+jfJOw==} + chrome-launcher@1.2.1: resolution: {integrity: sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==} engines: {node: '>=12.13.0'} @@ -1531,6 +1540,16 @@ snapshots: - supports-color - utf-8-validate + chrome-inspector@1.0.6: + dependencies: + '@devtoolcss/parser': 1.0.1 + jsdom: 27.1.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + chrome-launcher@1.2.1: dependencies: '@types/node': 24.10.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8a4444d..abc3af1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,5 @@ packages: - - packages/@devtoolcss-mcp - + - packages/* + onlyBuiltDependencies: - esbuild From e974a3a801fd4e475d844c2f2c808ba32b2a25a4 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Fri, 12 Dec 2025 17:02:52 +0800 Subject: [PATCH 32/36] refactor: split out mcp --- .../@devtoolcss-mcp/ChromeDebuggerBridge.js | 75 ----- packages/@devtoolcss-mcp/DOMExpression.js | 113 -------- packages/@devtoolcss-mcp/InspectorManager.js | 55 ---- packages/@devtoolcss-mcp/NodeUidManager.js | 50 ---- packages/@devtoolcss-mcp/README.md | 19 -- packages/@devtoolcss-mcp/background.js | 265 ------------------ packages/@devtoolcss-mcp/devtools.html | 6 - packages/@devtoolcss-mcp/devtools.js | 54 ---- packages/@devtoolcss-mcp/htmlUtils.js | 126 --------- packages/@devtoolcss-mcp/manifest.json | 17 -- .../@devtoolcss-mcp/offscreen_inspectors.html | 6 - .../@devtoolcss-mcp/offscreen_inspectors.js | 122 -------- packages/@devtoolcss-mcp/package.json | 24 -- packages/@devtoolcss-mcp/popup.html | 105 ------- packages/@devtoolcss-mcp/popup.js | 42 --- packages/@devtoolcss-mcp/scripts/copy.js | 30 -- .../@devtoolcss-mcp/scripts/esbuild.config.js | 22 -- .../@devtoolcss-mcp/scripts/sync-manifest.js | 33 --- packages/@devtoolcss-mcp/scripts/watch.js | 51 ---- packages/@devtoolcss-mcp/styleUtils.js | 157 ----------- packages/@devtoolcss-mcp/ws-server.js | 111 -------- packages/@devtoolcss-mcp/xpath.js | 62 ---- 22 files changed, 1545 deletions(-) delete mode 100644 packages/@devtoolcss-mcp/ChromeDebuggerBridge.js delete mode 100644 packages/@devtoolcss-mcp/DOMExpression.js delete mode 100644 packages/@devtoolcss-mcp/InspectorManager.js delete mode 100644 packages/@devtoolcss-mcp/NodeUidManager.js delete mode 100644 packages/@devtoolcss-mcp/README.md delete mode 100644 packages/@devtoolcss-mcp/background.js delete mode 100644 packages/@devtoolcss-mcp/devtools.html delete mode 100644 packages/@devtoolcss-mcp/devtools.js delete mode 100644 packages/@devtoolcss-mcp/htmlUtils.js delete mode 100644 packages/@devtoolcss-mcp/manifest.json delete mode 100644 packages/@devtoolcss-mcp/offscreen_inspectors.html delete mode 100644 packages/@devtoolcss-mcp/offscreen_inspectors.js delete mode 100644 packages/@devtoolcss-mcp/package.json delete mode 100644 packages/@devtoolcss-mcp/popup.html delete mode 100644 packages/@devtoolcss-mcp/popup.js delete mode 100644 packages/@devtoolcss-mcp/scripts/copy.js delete mode 100644 packages/@devtoolcss-mcp/scripts/esbuild.config.js delete mode 100644 packages/@devtoolcss-mcp/scripts/sync-manifest.js delete mode 100644 packages/@devtoolcss-mcp/scripts/watch.js delete mode 100644 packages/@devtoolcss-mcp/styleUtils.js delete mode 100644 packages/@devtoolcss-mcp/ws-server.js delete mode 100644 packages/@devtoolcss-mcp/xpath.js diff --git a/packages/@devtoolcss-mcp/ChromeDebuggerBridge.js b/packages/@devtoolcss-mcp/ChromeDebuggerBridge.js deleted file mode 100644 index 1a63ea9..0000000 --- a/packages/@devtoolcss-mcp/ChromeDebuggerBridge.js +++ /dev/null @@ -1,75 +0,0 @@ -async function sendDebuggerMessage(payload) { - const response = await chrome.runtime.sendMessage({ - receiver: "background", - ...payload, - }); - - if (response?.error) { - throw new Error(response.error); - } else { - return response?.result; - } -} - -// A chrome.debugger wrapper implemented in runtime.messaging for offscreen context -class ChromeDebuggerBridge { - _listeners = new Set(); - - constructor() { - chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { - switch (msg.event) { - case "DEBUGGER_EVENT": - const { source, method, params } = msg; - this._dispatchEvent(source, method, params); - break; - } - }); - } - - async attach(target, version) { - return sendDebuggerMessage({ - event: "DEBUGGER_ATTACH", - target, - }); - } - - async detach(target) { - return sendDebuggerMessage({ - event: "DEBUGGER_DETACH", - target, - }); - } - - // Send command to the actual chrome.debugger in background.js - async sendCommand(target, method, params) { - return sendDebuggerMessage({ - event: "DEBUGGER_SEND_COMMAND", - target, - method, - params, - }); - } - - onEvent = { - addListener: (callback) => { - this._listeners.add(callback); - }, - removeListener: (callback) => { - this._listeners.delete(callback); - }, - }; - - // Internal method to dispatch events to listeners - _dispatchEvent(source, method, params) { - for (const listener of this._listeners) { - try { - listener(source, method, params); - } catch (e) { - console.error("Error in debugger event listener:", e); - } - } - } -} - -// A chrome.debugger wrapper implemented in runtime.messaging for offscreen context -export const chromeDebugger = new ChromeDebuggerBridge(); diff --git a/packages/@devtoolcss-mcp/DOMExpression.js b/packages/@devtoolcss-mcp/DOMExpression.js deleted file mode 100644 index 65dc43d..0000000 --- a/packages/@devtoolcss-mcp/DOMExpression.js +++ /dev/null @@ -1,113 +0,0 @@ -function splitExpression(expr) { - if (expr[0] !== ".") throw new Error("Expression must start with a dot"); - const parts = []; - let current = ""; - let inSingle = false, - inDouble = false, - bracketDepth = 0, - parenDepth = 0; - - for (let i = 0; i < expr.length; i++) { - const c = expr[i]; - current += c; - if (c === "'" && !inDouble) inSingle = !inSingle; - else if (c === '"' && !inSingle) inDouble = !inDouble; - else if (!inSingle && !inDouble) { - if (c === "[") bracketDepth++; - else if (c === "]") bracketDepth--; - else if (c === "(") parenDepth++; - else if (c === ")") parenDepth--; - - if (bracketDepth > 1 || parenDepth > 1) { - throw new Error( - "cannot evaluate complex expression with nested [] or ()", - ); - } else if (bracketDepth < 0 || parenDepth < 0) { - throw new Error("[] or () not balanced"); - } - - const nextChar = expr[i + 1]; - if ( - [".", "[", "("].includes(nextChar) && - bracketDepth === 0 && - parenDepth === 0 - ) { - parts.push(current); - current = ""; - continue; - } - } - } - if (current) parts.push(current); - return parts; -} - -function evalMethods(target, expr) { - const operations = splitExpression(expr); - - for (const op of operations) { - if (op.startsWith("(")) { - // handle method calls e.g. querySelectorAll('div') - const argStr = op.slice(1, -1).trim(); - const args = argStr - ? argStr.split(",").map((arg) => { - // probably cannot use JSON.parse because it is js expression - // not really json with field quoted and only double quotes allowed - arg = arg.trim(); - if (arg === "undefined") return undefined; - if (arg === "null") return null; - if (arg === "true") return true; - if (arg === "false") return false; - if (!isNaN(Number(arg))) return Number(arg); - return arg.trim().replace(/^['"]|['"]$/g, ""); - }) - : []; - target = target(...args); - } else { - // handle property access e.g. .parentNode, [0] - const accessorStr = op.startsWith(".") ? op.slice(1) : op.slice(1, -1); - const accessor = !isNaN(Number(accessorStr)) - ? Number(accessorStr) - : accessorStr; - const field = target[accessor]; - target = typeof field === "function" ? field.bind(target) : field; - } - } - return target; -} - -export async function evaluateDOMExpression( - expression, - inspector, - nodeManager, -) { - expression = expression.trim(); - const targetNodeName = expression.split(".")[0]; - const targetNode = nodeManager.getNode(targetNodeName, inspector); - if (!targetNode) { - throw new Error(`Target node '${targetNodeName}' not found`); - } - - const remainingExpression = expression.slice(targetNodeName.length); - // TODO: validate remainingExpression to ensure safety - - try { - // cannot use dynamic code eval due to MV3 - const result = evalMethods(targetNode, remainingExpression); - - // Normalize result to array - let nodes; - if (result === null || result === undefined) { - nodes = []; - } else if (Array.isArray(result)) { - nodes = result; - } else { - nodes = [result]; - } - - const uids = nodes.map((node) => nodeManager.setNode(node)); - return { uids }; - } catch (error) { - throw new Error(`Failed to evaluate "${expression}": ${error.message}`); - } -} diff --git a/packages/@devtoolcss-mcp/InspectorManager.js b/packages/@devtoolcss-mcp/InspectorManager.js deleted file mode 100644 index 245955a..0000000 --- a/packages/@devtoolcss-mcp/InspectorManager.js +++ /dev/null @@ -1,55 +0,0 @@ -import { chromeDebugger } from "./ChromeDebuggerBridge"; -import { Inspector } from "chrome-inspector"; - -// inspector management per tab -export class InspectorManager { - constructor() { - this.inspectors = {}; - // record $0 xpaths for tabs not having inspector yet - this.tab$0Map = new Map(); - - chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { - switch (msg.event) { - case "TAB_CLOSED": - if (this.get(msg.tabId)) { - chromeDebugger.detach({ tabId: msg.tabId }); - this.remove(msg.tabId); - // TODO: cleanup nodeUidManager - } - break; - - case "DEBUGGER_DETACHED": - if (this.get(msg.tabId)) { - this.remove(msg.tabId); - console.log( - `Inspector for tab ${msg.tabId} detached for ${msg.reason}`, - ); - // TODO: cleanup nodeUidManager - } - break; - - case "SET_INSPECTED_TAB_$0": - this.tab$0Map.set(tabId, xpath); - break; - } - }); - } - - async create(tabId) { - await chromeDebugger.attach({ tabId }, "1.3"); - this.inspectors[tabId] = await Inspector.fromChromeDebugger( - chromeDebugger, - tabId, - { $0XPath: this.tab$0Map.get(tabId) }, - ); - return this.inspectors[tabId]; - } - - get(tabId) { - return this.inspectors[tabId]; - } - - remove(tabId) { - delete this.inspectors[tabId]; - } -} diff --git a/packages/@devtoolcss-mcp/NodeUidManager.js b/packages/@devtoolcss-mcp/NodeUidManager.js deleted file mode 100644 index 2d989a9..0000000 --- a/packages/@devtoolcss-mcp/NodeUidManager.js +++ /dev/null @@ -1,50 +0,0 @@ -// generate and record uids for node. -export class NodeUidManager { - constructor() { - this._nodeCounters = new Map(); // nodeName -> counter - this._idToRef = new Map(); // id -> WeakRef(node) - this._nodeToId = new WeakMap(); // node -> id - } - - generateId(node) { - const nodeName = node.nodeName.toLowerCase(); - const counter = this._nodeCounters.get(nodeName) || 0; - this._nodeCounters.set(nodeName, counter + 1); - return `${nodeName}_${counter}`; - } - - setNode(node) { - if (this._nodeToId.has(node)) { - return this._nodeToId.get(node); - } - const id = this.generateId(node); - this._idToRef.set(id, new WeakRef(node)); - this._nodeToId.set(node, id); - return id; - } - - getNode(uid, inspector) { - // predefined - if (uid === "document") return inspector.document; - if (uid === "$0") return inspector.$0; - - // from map - const ref = this._idToRef.get(uid); - if (!ref) return undefined; - const node = ref.deref(); - if (!node) { - // Node GC'd, clean up stale entry - this._idToRef.delete(uid); - } - return node; - } - - cleanUp() { - // FIXME: didn't really cleanup deleted inspector's nodes - for (const [id, ref] of this._idToRef.entries()) { - if (ref.deref() === undefined) { - this._idToRef.delete(id); - } - } - } -} diff --git a/packages/@devtoolcss-mcp/README.md b/packages/@devtoolcss-mcp/README.md deleted file mode 100644 index 70e25bc..0000000 --- a/packages/@devtoolcss-mcp/README.md +++ /dev/null @@ -1,19 +0,0 @@ -## mcp - -chrome extension (ws polling) + stateful inspector + lazy init + detach handle (exception & tab toggling dashboard) - -### feature - -get handle: - -- querySelectorAll - -handle methods: - -- getMatchedStyles -- getComputedStyle (by attr array) -- querySelectorAll -- parent -- children -- attributes -- outerHTML (with depth/line lenght control) diff --git a/packages/@devtoolcss-mcp/background.js b/packages/@devtoolcss-mcp/background.js deleted file mode 100644 index c99dac7..0000000 --- a/packages/@devtoolcss-mcp/background.js +++ /dev/null @@ -1,265 +0,0 @@ -/// - -// Keep service worker alive -// https://stackoverflow.com/a/66618269 -const KEEPALIVE_INTERVAL = 20000; // 20 seconds -const keepAlive = () => - setInterval(chrome.runtime.getPlatformInfo, KEEPALIVE_INTERVAL); -chrome.runtime.onStartup.addListener(keepAlive); -keepAlive(); - -// WebSocket connection state -let ws = null; -let settings = { - host: "127.0.0.1", - port: 9333, - pollingEnabled: true, - pollingInterval: 2000, // 2 seconds -}; - -let inspectedTabId = null; - -async function getActiveTabId() { - const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); - const activeTab = tabs[0]; - if (!activeTab) { - if (!inspectedTabId) - throw new Error( - "No active tab found. Tell user to click an element if in DevTools.", - ); - return inspectedTabId; - } else if (activeTab.url && activeTab.url.startsWith("chrome://")) { - throw new Error("Cannot access a chrome:// URL"); - } - return activeTab.id; -} - -chrome.tabs.onRemoved.addListener((tabId, removeInfo) => { - console.log(`Tab ${tabId} closed`); - // Clean up any resources related to this tab - chrome.runtime.sendMessage({ - receiver: "offscreen", - event: "TAB_CLOSED", - tabId: tabId, - }); -}); - -chrome.debugger.onEvent.addListener((source, method, params) => { - // Forward debugger events to offscreen inspector - chrome.runtime.sendMessage({ - receiver: "offscreen", - event: "DEBUGGER_EVENT", - source, - method, - params, - }); -}); - -chrome.debugger.onDetach.addListener((source, reason) => { - chrome.runtime.sendMessage({ - receiver: "offscreen", - event: "DEBUGGER_DETACHED", - tabId: source.tabId, - reason, - }); -}); - -// Handle debugger commands from offscreen -chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { - if (msg.receiver !== "background") return; - - switch (msg.event) { - case "DEBUGGER_SEND_COMMAND": - chrome.debugger - .sendCommand(msg.target, msg.method, msg.params) - .then((result) => sendResponse({ result })) - .catch((error) => sendResponse({ error: error.message })); - break; - case "DEBUGGER_ATTACH": - chrome.debugger - .attach(msg.target, "1.3") - .then((result) => sendResponse({ result })) - .catch((error) => sendResponse({ error: error.message })); - break; - case "DEBUGGER_DETACH": - chrome.debugger - .detach(msg.target) - .then((result) => sendResponse({ result })) - .catch((error) => sendResponse({ error: error.message })); - break; - case "SET_INSPECTED_TAB_ID": - inspectedTabId = msg.tabId; - break; - } - return true; // Keep the message channel open for async response -}); - -function formatRelativeTime(timestamp) { - const diffMs = Date.now() - timestamp; - const diffSec = Math.floor(diffMs / 1000); - const hr = Math.floor(diffSec / 3600); - const min = Math.floor((diffSec % 3600) / 60); - const sec = diffSec % 60; - let parts = []; - if (hr > 0) return `${hr}hr ago`; - if (min > 0) return `${min}min ago`; - return `${sec}sec ago`; -} - -// Main serving logic -async function serveRequest(request) { - if (request.tool === "getTabs") { - const tabs = await chrome.tabs.query({}); - return { - tabs: tabs.map((tab) => ({ - id: tab.id, - title: tab.title, - url: tab.url, - active: tab.active ? true : undefined, - lastAccessed: formatRelativeTime(tab.lastAccessed), - })), - }; - } - - // other inspector requests - if (request.tabId === undefined) { - request.tabId = await getActiveTabId(); - } - const response = await chrome.runtime.sendMessage({ - receiver: "offscreen", - event: "REQUEST", - request, - }); - return response; -} - -// Listen for settings changes -chrome.storage.onChanged.addListener((changes, areaName) => { - loadSettings().then(() => { - if (changes.pollingEnabled.newValue && !changes.pollingEnabled.oldValue) { - pollAndConnect(); - } - }); -}); - -// Load settings from storage -async function loadSettings() { - const stored = await chrome.storage.sync.get([ - "host", - "port", - "pollingEnabled", - "pollingInterval", - ]); - settings = { - host: stored.host || "127.0.0.1", - port: stored.port || 9333, - pollingEnabled: stored.pollingEnabled !== false, // default true - pollingInterval: stored.pollingInterval || 2000, - }; - console.log("[Settings] Loaded:", settings); -} - -// Check if server is available using HTTP polling (silent on failure) -async function checkServerAvailability() { - if (!settings.pollingEnabled) { - return false; - } - - const healthUrl = `http://${settings.host}:${settings.port}/health`; - - try { - const response = await fetch(healthUrl, { - method: "GET", - signal: AbortSignal.timeout(5000), - }); - - return response.ok; - } catch (error) { - // Silent failure - this is expected when server is not available - return false; - } -} - -// Connect to WebSocket server -function connectWebSocket() { - const wsUrl = `ws://${settings.host}:${settings.port}`; - console.log(`[WS] Connecting to ${wsUrl}...`); - - ws = new WebSocket(wsUrl); - - const cleanUp = () => { - ws.close(); - ws = null; // remove the only reference, effectively cleanup - }; - - ws.onopen = () => { - console.log("[WS] Connected successfully"); - }; - - ws.onmessage = async (event) => { - console.log("[WS] Request received:", event.data); - let req; - try { - req = JSON.parse(event.data); - } catch (e) { - console.error(`[WS] Failed to parse message ${event.data}:`, e); - ws.send(JSON.stringify({ error: e.message || String(e) })); - return; - } - - try { - const response = await serveRequest(req); - console.log("response:", response); - ws.send(JSON.stringify({ id: req.id, ...response })); - } catch (e) { - console.log(`[WS] Failed to serve message ${event.data}:`, e); - ws.send(JSON.stringify({ id: req.id, error: e.message || String(e) })); - } - }; - - ws.onerror = (event) => { - console.error("[WS] Error occurred:", event); - }; - - ws.onclose = () => { - console.log("[WS] Connection closed"); - cleanUp(); - pollAndConnect(); - }; -} - -// Poll and connect -async function pollAndConnect() { - while (settings.pollingEnabled) { - const available = await checkServerAvailability(); - - if (available) { - console.log("[Poll] Server is available, connecting WebSocket..."); - connectWebSocket(); - return; - } - // Server not available, wait until next poll - await new Promise((r) => setTimeout(r, settings.pollingInterval)); - } -} - -async function main() { - await loadSettings(); - await chrome.offscreen.createDocument({ - url: "offscreen_inspectors.html", - reasons: ["DOM_PARSER"], - justification: "Providing DOM implementation for inspector.", - }); - pollAndConnect(); -} - -// Handle extension lifecycle -chrome.runtime.onStartup.addListener(() => { - console.log("[Lifecycle] Extension started"); - main(); -}); - -chrome.runtime.onInstalled.addListener(() => { - console.log("[Lifecycle] Extension installed/updated"); - main(); -}); diff --git a/packages/@devtoolcss-mcp/devtools.html b/packages/@devtoolcss-mcp/devtools.html deleted file mode 100644 index 5dbb29f..0000000 --- a/packages/@devtoolcss-mcp/devtools.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/packages/@devtoolcss-mcp/devtools.js b/packages/@devtoolcss-mcp/devtools.js deleted file mode 100644 index f01a10e..0000000 --- a/packages/@devtoolcss-mcp/devtools.js +++ /dev/null @@ -1,54 +0,0 @@ -// Modified From https://github.com/devtoolcss/chrome-inspector/blob/main/extension/devtools.js -// Licensed under MIT License -// -// Added SET_INSPECTED_TAB_ID messaging to inform background script of the inspected tab ID - -import { getAbsoluteXPath } from "./xpath.js"; - -function sendSelector() { - // TODO: debug mode - const expression = ` -(function() { - ${getAbsoluteXPath.toString()} - - const sender = $0.ownerDocument.defaultView.__chrome_inspector_send_$0_xpath; - try{ - const xpath = getAbsoluteXPath($0); - if (typeof sender !== "function") return xpath; - sender(xpath); - } catch {} -})(); -`; - - chrome.devtools.inspectedWindow.eval( - expression, - {}, - (result, exceptionInfo) => { - if (result) { - chrome.runtime.sendMessage({ - receiver: "offscreen", - event: "SET_INSPECTED_TAB_$0", - tabId: chrome.devtools.inspectedWindow.tabId, - xpath: result, - }); - } else if (exceptionInfo && exceptionInfo.isException) { - console.error( - `Unable to evaluate selection change script.`, - exceptionInfo, - ); - } - }, - ); - - chrome.runtime.sendMessage({ - receiver: "background", - event: "SET_INSPECTED_TAB_ID", - tabId: chrome.devtools.inspectedWindow.tabId, - }); -} - -if (chrome?.devtools?.panels?.elements) { - chrome.devtools.panels.elements.onSelectionChanged.addListener(sendSelector); -} else { - console.warn(`chrome.devtools API is not available in this context.`); -} diff --git a/packages/@devtoolcss-mcp/htmlUtils.js b/packages/@devtoolcss-mcp/htmlUtils.js deleted file mode 100644 index 6a010db..0000000 --- a/packages/@devtoolcss-mcp/htmlUtils.js +++ /dev/null @@ -1,126 +0,0 @@ -import beautify from "js-beautify"; -/** - * Truncates HTML based on depth and line length controls - * @param {Node} node - The DOM node to truncate - * @param {number} [maxDepth] - Maximum nesting depth of tags to include - * @param {number} [maxLineLength] - Maximum length of each line - * @returns {string} Truncated HTML - */ -export function truncateHTML(node, maxDepth, maxLineLength, maxChars) { - let result = node.cloneNode(true); - - // Truncate by depth - if (maxDepth > 0) { - for (let depth = maxDepth; depth > 0; depth--) { - // has to use original node each time to ensure summary is correct - let truncated = truncateByDepth(node, depth); - let html = truncated.outerHTML || truncated.textContent; - if (maxChars === undefined || (html && html.length < maxChars)) { - result = truncated; - break; - } - } - } - - // Get HTML string - let html = result.outerHTML || result.textContent; - html = beautify.html(html, { - indent_size: 2, - wrap_line_length: 0, // Don't wrap - let truncateByLineLength handle it - preserve_newlines: false, - }); - - // Truncate by line length - if (maxLineLength !== undefined && maxLineLength > 0) { - html = truncateByLineLength(html, maxLineLength); - } - - return html; -} - -/** - * Counts the structure of remaining nodes - * @param {Node} node - The DOM node to analyze - * @returns {Object} Summary of node counts - */ -function summarizeRemainingStructure(node) { - const summary = { - elements: 0, - textNodes: 0, - totalDepth: 0, - }; - - function traverse(n, depth) { - summary.totalDepth = Math.max(summary.totalDepth, depth); - - for (let child of n.childNodes) { - if (child.nodeType === Node.ELEMENT_NODE) { - summary.elements++; - traverse(child, depth + 1); - } else if ( - child.nodeType === Node.TEXT_NODE && - child.textContent.trim() - ) { - summary.textNodes++; - } - } - } - - traverse(node, 0); - return summary; -} - -/** - * Truncates a DOM node by maximum nesting depth - * @param {Node} node - The DOM node to truncate - * @param {number} maxDepth - Maximum depth to traverse - * @returns {Node} Cloned and truncated node - */ -function truncateByDepth(node, maxDepth) { - // Clone the node without children - const clone = node.cloneNode(false); - - // If we're at max depth, add summary comment and return - if (maxDepth <= 0) { - if (clone.nodeType === Node.ELEMENT_NODE) { - const summary = summarizeRemainingStructure(node); - const summaryText = `... ${summary.elements} more element(s), ${summary.textNodes} text node(s), max depth +${summary.totalDepth}`; - if (summary.totalDepth > 0) - clone.appendChild(document.createComment(summaryText)); - } - return clone; - } - - // Process children - for (let child of node.childNodes) { - if (child.nodeType === Node.ELEMENT_NODE) { - // Recursively truncate element children - clone.appendChild(truncateByDepth(child, maxDepth - 1)); - } else if ( - child.nodeType === Node.TEXT_NODE || - child.nodeType === Node.COMMENT_NODE - ) { - // Copy text and comment nodes as-is - clone.appendChild(child.cloneNode(true)); - } - } - - return clone; -} - -/** - * Truncates each line of text to a maximum length - * @param {string} text - The text to truncate - * @param {number} maxLineLength - Maximum length per line - * @returns {string} Text with truncated lines - */ -function truncateByLineLength(text, maxLineLength) { - const lines = text.split("\n"); - return lines - .map((line) => - line.length > maxLineLength - ? line.substring(0, maxLineLength - 3) + "..." - : line, - ) - .join("\n"); -} diff --git a/packages/@devtoolcss-mcp/manifest.json b/packages/@devtoolcss-mcp/manifest.json deleted file mode 100644 index 5e9dc5e..0000000 --- a/packages/@devtoolcss-mcp/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "DevtoolCSS MCP", - "version": "0.0.0", - "manifest_version": 3, - "description": "Chrome extension with WebSocket polling to MCP server", - "background": { - "service_worker": "background.js", - "type": "module" - }, - "devtools_page": "devtools.html", - "permissions": ["tabs", "storage", "debugger", "offscreen"], - "action": { - "default_popup": "popup.html", - "default_title": "DevtoolCSS MCP Settings" - }, - "host_permissions": ["http://127.0.0.1/*", "ws://127.0.0.1/*"] -} diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.html b/packages/@devtoolcss-mcp/offscreen_inspectors.html deleted file mode 100644 index 16804b4..0000000 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/packages/@devtoolcss-mcp/offscreen_inspectors.js b/packages/@devtoolcss-mcp/offscreen_inspectors.js deleted file mode 100644 index 74bda17..0000000 --- a/packages/@devtoolcss-mcp/offscreen_inspectors.js +++ /dev/null @@ -1,122 +0,0 @@ -import { truncateHTML } from "./htmlUtils"; -import { filterMatchedStyles, toStyleSheetText } from "./styleUtils"; -import { evaluateDOMExpression } from "./DOMExpression"; - -import { NodeUidManager } from "./NodeUidManager"; -import { InspectorManager } from "./InspectorManager"; - -const inspectorManager = new InspectorManager(); -const nodeManager = new NodeUidManager(); - -async function processRequest(request) { - console.log("processRequest - request:", request); - const inspector = - inspectorManager.get(request.tabId) ?? - (await inspectorManager.create(request.tabId)); - - switch (request.tool) { - case "getNodes": { - // Unified node retrieval using DOM expression syntax - if (!request.expression) { - throw new Error("Missing 'expression' parameter"); - } - return await evaluateDOMExpression( - request.expression, - inspector, - nodeManager, - ); - } - - case "getMatchedStyles": { - const { - uid, - removeUnusedVar = true, - appliedOnly = false, - filter, - } = request; - const node = getNode(uid, inspector); - if (!node) { - throw new Error(`Node not found for uid: ${uid}`); - } - const options = { - parseOptions: { removeUnusedVar }, - }; - let styles = await node.getMatchedStyles(options); - - // Apply filters to reduce response size - if (filter) { - styles = filterMatchedStyles(styles, filter); - } - const toStyleSheetOptions = { - applied: appliedOnly ? false : true, - matchedSelectors: true, - }; - const styleSheetText = toStyleSheetText( - styles, - node, - toStyleSheetOptions, - ); - console.log("serveRequest - getMatchedStyles styles:", styleSheetText); - - return { styles: styleSheetText }; - } - - case "getComputedStyle": { - const node = getNode(request.uid, inspector); - if (!node) { - throw new Error("Node not found for uid: " + request.uid); - } - - const styles = await node.getComputedStyle(); - const filtered = {}; - request.properties.map((prop) => { - filtered[prop] = styles[prop]; - }); - console.log("serveRequest - getComputedStyle styles:", filtered); - return { styles: filtered }; - } - - case "getOuterHTML": { - // some safe defaults - const { - uid, - maxDepth = 3, - maxLineLength = 1000, - maxChars = 500000, - } = request; - const node = getNode(uid, inspector); - if (!node) { - throw new Error(`Node not found for uid: ${uid}`); - } else if (!node.tracked) { - throw new Error(`Node is no longer existed for uid: ${uid}`); - } - // Apply depth and line length controls if provided - const html = truncateHTML( - node._docNode, - maxDepth, - maxLineLength, - maxChars, - ); - console.log("serveRequest - getOuterHTML html:", html); - return { outerHTML: html }; - } - - default: - throw new Error("Unknown tool: " + request.tool); - } -} - -// listener must be sync, return true to indicate async response -chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { - if (msg.receiver !== "offscreen") return; - - switch (msg.event) { - case "REQUEST": - processRequest(msg.request) - .then(sendResponse) - .catch((error) => { - sendResponse({ error: error.message || String(error) }); - }); - return true; // Keep the message channel open for async response - } -}); diff --git a/packages/@devtoolcss-mcp/package.json b/packages/@devtoolcss-mcp/package.json deleted file mode 100644 index 53130ce..0000000 --- a/packages/@devtoolcss-mcp/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@devtoolcss/mcp", - "version": "0.0.0", - "private": true, - "description": "Chrome extension with WebSocket polling to MCP server", - "type": "module", - "scripts": { - "build": "node scripts/copy.js && node scripts/esbuild.config.js", - "prepare": "pnpm build && pnpm sync:manifest", - "watch": "node scripts/watch.js", - "sync:manifest": "node scripts/sync-manifest.js" - }, - "dependencies": { - "chrome-inspector": "^1.0.6", - "js-beautify": "^1.15.4", - "ws": "^8.18.3" - }, - "devDependencies": { - "@types/chrome": "^0.1.27", - "chokidar": "^4.0.3", - "esbuild": "^0.27.0", - "typescript": "^5.9.3" - } -} diff --git a/packages/@devtoolcss-mcp/popup.html b/packages/@devtoolcss-mcp/popup.html deleted file mode 100644 index cba8594..0000000 --- a/packages/@devtoolcss-mcp/popup.html +++ /dev/null @@ -1,105 +0,0 @@ - - - - - DevtoolCSS MCP Settings - - - -

MCP Server Settings

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - -
Settings saved!
- - - - diff --git a/packages/@devtoolcss-mcp/popup.js b/packages/@devtoolcss-mcp/popup.js deleted file mode 100644 index 8504beb..0000000 --- a/packages/@devtoolcss-mcp/popup.js +++ /dev/null @@ -1,42 +0,0 @@ -// Load current settings -async function loadSettings() { - const settings = await chrome.storage.sync.get([ - "host", - "port", - "pollingEnabled", - "pollingInterval", - ]); - - document.getElementById("host").value = settings.host || "127.0.0.1"; - document.getElementById("port").value = settings.port || 9333; - document.getElementById("pollingInterval").value = - settings.pollingInterval || 2000; - document.getElementById("pollingEnabled").checked = - settings.pollingEnabled !== false; -} - -// Save settings -async function saveSettings() { - const settings = { - host: document.getElementById("host").value.trim() || "127.0.0.1", - port: parseInt(document.getElementById("port").value) || 9333, - pollingInterval: - parseInt(document.getElementById("pollingInterval").value) || 2000, - pollingEnabled: document.getElementById("pollingEnabled").checked, - }; - - await chrome.storage.sync.set(settings); - - // Show success message - const status = document.getElementById("status"); - status.classList.add("success"); - setTimeout(() => { - status.classList.remove("success"); - }, 2000); -} - -// Event listeners -document.getElementById("save").addEventListener("click", saveSettings); - -// Load settings on popup open -loadSettings(); diff --git a/packages/@devtoolcss-mcp/scripts/copy.js b/packages/@devtoolcss-mcp/scripts/copy.js deleted file mode 100644 index 133ecf8..0000000 --- a/packages/@devtoolcss-mcp/scripts/copy.js +++ /dev/null @@ -1,30 +0,0 @@ -import fs from "fs"; -import path from "path"; - -// Ensure dist/ directory exists -if (!fs.existsSync("dist")) { - fs.mkdirSync("dist"); -} - -// Recursive directory copy function -function copyDir(srcDir, destDir) { - if (!fs.existsSync(destDir)) fs.mkdirSync(destDir); - fs.readdirSync(srcDir).forEach((item) => { - const srcPath = path.join(srcDir, item); - const destPath = path.join(destDir, item); - if (fs.lstatSync(srcPath).isDirectory()) { - copyDir(srcPath, destPath); - } else { - fs.copyFileSync(srcPath, destPath); - } - }); -} - -// Copy all .css and .html files, and manifest.json -fs.readdirSync(".") - .filter( - (f) => f.endsWith(".css") || f.endsWith(".html") || f === "manifest.json", - ) - .forEach((f) => fs.copyFileSync(f, path.join("dist", f))); - -//copyDir("icons", path.join("dist", "icons")); diff --git a/packages/@devtoolcss-mcp/scripts/esbuild.config.js b/packages/@devtoolcss-mcp/scripts/esbuild.config.js deleted file mode 100644 index bfbd6f5..0000000 --- a/packages/@devtoolcss-mcp/scripts/esbuild.config.js +++ /dev/null @@ -1,22 +0,0 @@ -import { build } from "esbuild"; - -const isProd = process.env.NODE_ENV === "production"; - -const commonOptions = { - entryPoints: [ - "popup.js", - "background.js", - "offscreen_inspectors.js", - "devtools.js", - ], - bundle: true, - outdir: "dist", - format: "esm", - external: ["chrome"], -}; - -build({ - ...commonOptions, - sourcemap: !isProd, - minify: isProd, -}).catch(() => process.exit(1)); diff --git a/packages/@devtoolcss-mcp/scripts/sync-manifest.js b/packages/@devtoolcss-mcp/scripts/sync-manifest.js deleted file mode 100644 index 9728cba..0000000 --- a/packages/@devtoolcss-mcp/scripts/sync-manifest.js +++ /dev/null @@ -1,33 +0,0 @@ -import fs from "fs"; -import path from "path"; - -const cwd = process.cwd(); -const packageJsonPath = path.join(cwd, "package.json"); -const manifestJsonPath = path.join(cwd, "manifest.json"); - -function syncManifestVersion() { - if (!fs.existsSync(packageJsonPath) || !fs.existsSync(manifestJsonPath)) { - console.error( - "package.json or manifest.json not found in current directory.", - ); - process.exit(1); - } - - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); - if (!packageJson.version) { - console.error("No version field found in package.json."); - process.exit(1); - } - - const manifestText = fs.readFileSync(manifestJsonPath, "utf8"); - // use regex to not disturb formatting - const updatedManifestText = manifestText.replace( - /("version"\s*:\s*)"(.*?)"/, - `$1"${packageJson.version}"`, - ); - - fs.writeFileSync(manifestJsonPath, updatedManifestText, "utf8"); - console.log(`manifest.json version synced to ${packageJson.version}`); -} - -syncManifestVersion(); diff --git a/packages/@devtoolcss-mcp/scripts/watch.js b/packages/@devtoolcss-mcp/scripts/watch.js deleted file mode 100644 index 7c67c7f..0000000 --- a/packages/@devtoolcss-mcp/scripts/watch.js +++ /dev/null @@ -1,51 +0,0 @@ -import chokidar from "chokidar"; -import { exec } from "child_process"; - -const ignored = [ - "package.json", - "package-lock.json", - "README.md", - "tsconfig.json", - "ws-server.js", -]; - -const copyIgnoreMatcher = (path) => { - if ( - ignored.includes(path) || - path.startsWith("node_modules/") || - path.startsWith("dist/") || - path.startsWith("scripts/") || - path.startsWith(".") - ) - return true; - - return false; -}; - -chokidar.watch(".").on("change", (path) => { - if (copyIgnoreMatcher(path)) { - return; - } - - if (path.endsWith(".js")) { - // any js change could affect the bundle - console.log(`Rebuilding due to change in ${path}`); - exec( - "NODE_ENV=development node scripts/esbuild.config.js", - (error, stdout, stderr) => { - if (error) { - console.error(`Error during build: ${stderr}`); - } - }, - ); - } else { - console.log(`Copying due to change in ${path}`); - exec(`cp ${path} dist/`, (error, stdout, stderr) => { - if (error) { - console.error(`Error during copy: ${stderr}`); - } - }); - } -}); - -console.log("Watching for changes..."); diff --git a/packages/@devtoolcss-mcp/styleUtils.js b/packages/@devtoolcss-mcp/styleUtils.js deleted file mode 100644 index 204ade8..0000000 --- a/packages/@devtoolcss-mcp/styleUtils.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Filters matched styles response to reduce size - * @param {Object} styles - The matched styles object from chrome-inspector - * @param {Object} filter - Filter options - * @param {string[]} [filter.selectors] - Regex pattern for selector matching in matched and pseudoElements - * @param {string[]} [filter.properties] - Properties to include rules with matching properties - * @param {boolean} [filter.appliedOnly] - If true, only include applied properties - * @returns {Object} Filtered matched styles - */ -export function filterMatchedStyles(styles, filter) { - // Compile regex patterns if provided - - if (filter.selectors) { - const selectorRegexes = filter.selectors - ? filter.selectors.map((pattern) => new RegExp(pattern)) - : null; - const filterRulesBySelectors = (rules) => { - return rules.filter((rule) => { - return selectorRegexes.some((regex) => - regex.test(rule.matchedSelectors.join(", ")), - ); - }); - }; - styles.matchedCSSRules = filterRulesBySelectors(styles.matchedCSSRules); - styles.pseudoElements = filterRulesBySelectors(styles.pseudoElements); - } - - const filterAllProperties = (styles, filter) => { - const filterProperties = (properties) => { - return properties.filter(filter); - }; - for (const parentCSS of styles.inherited) { - parentCSS.inline = filterProperties(parentCSS.inline); - for (const rule of parentCSS.matched) { - rule.properties = filterProperties(rule.properties); - } - } - styles.attributes = filterProperties(styles.attributes); - for (const rule of styles.matchedCSSRules) { - rule.properties = filterProperties(rule.properties); - } - for (const rule of styles.pseudoElements) { - rule.properties = filterProperties(rule.properties); - } - styles.inline = filterProperties(styles.inline); - }; - - if (filter.properties) { - const propertiesSet = new Set(filter.properties); - const filter = (decl) => propertiesSet.has(decl.name); - filterAllProperties(styles, filter); - } - - if (filter.appliedOnly) { - const filter = (decl) => decl.applied === true; - filterAllProperties(styles, filter); - } - - return styles; -} - -export function toStyleSheetText(styles, element, commentConfig = {}) { - let cssText = ""; - - const toCSSRuleText = (rule) => { - const allSelectorsStr = rule.allSelectors.join(", "); - const matchedSelectorsStr = rule.matchedSelectors.join(", "); - let css = ""; - // TODO: inspector need CSS.styleSheetAdded event to get origin info - //if (commentConfig.origin && rule.origin) { - // css += `/* Origin: ${rule.origin} */\n`; - //} - if ( - commentConfig.matchedSelectors && - matchedSelectorsStr !== allSelectorsStr - ) { - css += `/* Matched: ${matchedSelectorsStr} */\n`; - } - css += `${allSelectorsStr} {\n`; - for (const prop of rule.properties) { - css += ` ${prop.name}: ${prop.value};`; - if (commentConfig.applied && prop.applied) { - css += ` /* applied */`; - } - css += "\n"; - } - css += `}\n\n`; - return css; - }; - - // inline - if (styles.inline.length > 0) { - cssText += toCSSRuleText({ - allSelectors: ["element.style"], - matchedSelectors: ["element.style"], - properties: styles.inline, - }); - } - - // matched & pseudoElements - const allMatchedRules = [ - ...styles.matched, - ...styles.pseudoElements, - ].reverse(); - - for (const rule of allMatchedRules) { - if (rule.properties.length > 0) cssText += toCSSRuleText(rule); - } - - // attributes - if (styles.attributes.length > 0) { - const selectorPlaceholder = `${element.nodeName.toLowerCase()}[Attributes Style]`; - cssText += toCSSRuleText({ - allSelectors: [selectorPlaceholder], - matchedSelectors: [selectorPlaceholder], - properties: styles.attributes, - }); - } - - for (const parentCSS of styles.inherited) { - const { inline, matched, distance } = parentCSS; - if ( - inline.length === 0 && - matched.every((rule) => rule.properties.length === 0) - ) - continue; - - const getParentSelector = (element, distance) => { - let parentNode = element; - for (let i = 0; i < distance; i++) { - parentNode = parentNode.parentNode; - } - let parentSelector = parentNode.nodeName.toLowerCase(); - if (parentNode.id) { - parentSelector += `#${parentNode.id}`; - } else if (parentNode.classList && parentNode.classList.length > 0) { - parentSelector += `.${[...parentNode.classList].slice(0, 3).join(".")}`; - } - return parentSelector; - }; - cssText += `/* Inherited from ${getParentSelector(element, distance)} */\n`; - - if (inline.length > 0) { - cssText += toCSSRuleText({ - allSelectors: ["style attribute"], - matchedSelectors: ["style attribute"], - properties: inline, - }); - } - for (const rule of matched) { - if (rule.properties.length > 0) { - cssText += toCSSRuleText(rule); - } - } - } - return cssText; -} diff --git a/packages/@devtoolcss-mcp/ws-server.js b/packages/@devtoolcss-mcp/ws-server.js deleted file mode 100644 index 6d1afa5..0000000 --- a/packages/@devtoolcss-mcp/ws-server.js +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env node -import http from "http"; -import WebSocket, { WebSocketServer } from "ws"; -import readline from "readline"; - -const PORT = process.env.PORT || 9333; - -// Create HTTP server -const server = http.createServer((req, res) => { - // Health check endpoint - if (req.url === "/health" && req.method === "GET") { - res.writeHead(200, { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*", - }); - res.end( - JSON.stringify({ - status: "ok", - timestamp: Date.now(), - service: "DevtoolCSS MCP Server", - }), - ); - return; - } - - // Handle CORS preflight - if (req.method === "OPTIONS") { - res.writeHead(204, { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type", - }); - res.end(); - return; - } - - res.writeHead(404); - res.end("Not Found"); -}); - -// Create WebSocket server -const wss = new WebSocketServer({ server }); - -console.log(`[Server] HTTP + WebSocket server listening on:`); -console.log(` - HTTP: http://127.0.0.1:${PORT}/health`); -console.log(` - WebSocket: ws://127.0.0.1:${PORT}`); - -const handleMessage = (message) => { - try { - const data = JSON.parse(message.toString()); - console.log("[WS] Message received:", data); - } catch (e) { - console.error("[WS] Failed to parse message:", e); - } -}; - -let activeWs = null; // Track the active WebSocket connection - -wss.on("connection", (ws) => { - if (activeWs && activeWs.readyState === WebSocket.OPEN) { - ws.close(1000, "Only one connection allowed"); - console.log("[WS] Refused new connection: already connected"); - return; - } - activeWs = ws; - console.log("[WS] Client connected"); - - ws.on("message", (message) => { - console.log("[WS] Received:", message.toString()); - handleMessage(message); - }); - - ws.on("close", () => { - console.log("[WS] Client disconnected"); - if (activeWs === ws) { - activeWs = null; - } - }); - - ws.on("error", (error) => { - console.error("[WS] Error:", error); - }); -}); - -wss.on("error", (error) => { - console.error("[WS] Server error:", error); -}); - -// Start server -server.listen(PORT, "127.0.0.1", () => { - console.log("[Server] Ready to accept connections"); -}); -// Setup readline interface for stdin -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: false, -}); - -rl.on("line", (line) => { - try { - const json = JSON.parse(line); - if (activeWs && activeWs.readyState === WebSocket.OPEN) { - activeWs.send(JSON.stringify(json)); - } - // Optionally, log if no active connection - } catch (e) { - console.error("Failed to parse stdin line as JSON:", e); - // Ignore lines that are not valid JSON - } -}); diff --git a/packages/@devtoolcss-mcp/xpath.js b/packages/@devtoolcss-mcp/xpath.js deleted file mode 100644 index 356ad37..0000000 --- a/packages/@devtoolcss-mcp/xpath.js +++ /dev/null @@ -1,62 +0,0 @@ -// From https://github.com/devtoolcss/chrome-inspector/blob/main/extension/xpath.js -// Licensed under MIT License - -export function getAbsoluteXPath(node) { - if (!node) return ""; - const pathSegments = []; - - while (node && node.nodeType !== Node.DOCUMENT_NODE) { - let segment = ""; - let index = 1; - let sibling = node.previousSibling; - - switch (node.nodeType) { - case Node.ELEMENT_NODE: { - const ns = node.namespaceURI; - let prefix = ""; - if (ns === "http://www.w3.org/2000/svg") prefix = "svg:"; - else if (ns === "http://www.w3.org/1999/xhtml") prefix = ""; // default HTML - - while (sibling) { - if ( - sibling.nodeType === Node.ELEMENT_NODE && - sibling.nodeName === node.nodeName - ) - index++; - sibling = sibling.previousSibling; - } - - segment = `${prefix}${node.localName}[${index}]`; - break; - } - - case Node.TEXT_NODE: - while (sibling) { - if (sibling.nodeType === Node.TEXT_NODE) index++; - sibling = sibling.previousSibling; - } - segment = `text()[${index}]`; - break; - - case Node.COMMENT_NODE: - while (sibling) { - if (sibling.nodeType === Node.COMMENT_NODE) index++; - sibling = sibling.previousSibling; - } - segment = `comment()[${index}]`; - break; - - case Node.ATTRIBUTE_NODE: - const ownerPath = getAbsoluteXPath(node.ownerElement); - return `${ownerPath}/@${node.nodeName}`; - - default: - segment = `node()[${index}]`; - } - - pathSegments.unshift(segment); - node = node.parentNode; - } - - return "/" + pathSegments.join("/"); -} From a357dd5fc82bd74350c69dc80178e1b754f99afd Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Fri, 12 Dec 2025 17:22:29 +0800 Subject: [PATCH 33/36] fix(inliner): rewrite with ParsedCSS --- .../src/optimizers/AriaExpanded.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@devtoolcss-inliner/src/optimizers/AriaExpanded.ts b/packages/@devtoolcss-inliner/src/optimizers/AriaExpanded.ts index df51b9c..c561152 100644 --- a/packages/@devtoolcss-inliner/src/optimizers/AriaExpanded.ts +++ b/packages/@devtoolcss-inliner/src/optimizers/AriaExpanded.ts @@ -1,5 +1,5 @@ import type { Optimizer } from "./optimizer.js"; -import { parseCSSProperties } from "@devtoolcss/parser"; +import { ParsedCSS } from "@devtoolcss/parser"; import type { ParsedCSSRules, CDPNodeWithId } from "../types.js"; import type { InspectorElement } from "chrome-inspector"; @@ -8,8 +8,8 @@ import type { InspectorElement } from "chrome-inspector"; */ export class AriaExpandedOptimizer implements Optimizer { checkChildrenNodeIds: Set; - childrenStyleBefore: Map; - childrenStyleAfter: Map; + childrenStyleBefore: Map; + childrenStyleAfter: Map; constructor() { this.checkChildrenNodeIds = new Set(); @@ -78,15 +78,15 @@ export class AriaExpandedOptimizer implements Optimizer { if (childrenStyleBefore.length > 0 && childrenStyleAfter.length > 0) { for (let i = 0; i < node.children.length; ++i) { const serializedRuleSet = new Set(); - for (const ruleMatch of childrenStyleBefore[i].matchedCSSRules) { + for (const ruleMatch of childrenStyleBefore[i].matched) { serializedRuleSet.add(JSON.stringify(ruleMatch)); } - for (const ruleMatch of childrenStyleAfter[i].matchedCSSRules) { + for (const ruleMatch of childrenStyleAfter[i].matched) { if (!serializedRuleSet.has(JSON.stringify(ruleMatch))) { // TODO: parse selector to determine pseudo class const selector = `#${node.id}:hover > #${node.children[i].id}`; if (!rules[selector]) rules[selector] = []; - rules[selector].push(...parseCSSProperties(ruleMatch.rule.style)); + rules[selector].push(...ruleMatch.properties); } } } From 8bea40e54f7bbd5f0e443a82c2c9c8e4f025f643 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Fri, 12 Dec 2025 19:35:20 +0800 Subject: [PATCH 34/36] fix(inliner,cleanclone): progress with device index --- packages/@devtoolcss-inliner/src/inliner.ts | 8 ++++++-- packages/cleanclone/src/crawler.ts | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/@devtoolcss-inliner/src/inliner.ts b/packages/@devtoolcss-inliner/src/inliner.ts index ece0e66..21af04c 100644 --- a/packages/@devtoolcss-inliner/src/inliner.ts +++ b/packages/@devtoolcss-inliner/src/inliner.ts @@ -479,7 +479,11 @@ function getFreezedCdpTree(cdpNode: CDPNode): CDPNodeWithId { async function getInlinedComponent( selector: string, inspector: Inspector, - onProgress: (completed: number, total: number) => void = () => {}, + onProgress: ( + completed: number, + total: number, + deviceIndex: number, + ) => void = () => {}, onError: (e: any) => void = () => {}, options: InlineOptions = {}, ): Promise<{ element: Element; rootStyle?: Element }> { @@ -568,7 +572,7 @@ async function getInlinedComponent( removeUnusedVar: true, }); await inspector.forcePseudoState(inspectorElement, []); // reset forced pseudo states - onProgress(++completed, total); + onProgress(++completed, total, i); }, onError, -1, diff --git a/packages/cleanclone/src/crawler.ts b/packages/cleanclone/src/crawler.ts index ae3793d..edcfa4c 100644 --- a/packages/cleanclone/src/crawler.ts +++ b/packages/cleanclone/src/crawler.ts @@ -489,11 +489,12 @@ export class Crawler extends EventEmitter { const { element: body } = await getInlinedComponent( "body", inspector, - (completed, total) => { + (completed, total, deviceIndex) => { this.emitProgress({ crawlProgress: { processedElements: completed, totalElements: total, + deviceIndex, }, }); }, From 36f776192904dcbd3f815a9a48c5fea69ebd71d9 Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Fri, 12 Dec 2025 19:37:10 +0800 Subject: [PATCH 35/36] chore(inliner,cleanclone): bump to 1.0.2 --- packages/@devtoolcss-inliner/package.json | 2 +- packages/cleanclone/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@devtoolcss-inliner/package.json b/packages/@devtoolcss-inliner/package.json index f4fd96d..bd5914b 100644 --- a/packages/@devtoolcss-inliner/package.json +++ b/packages/@devtoolcss-inliner/package.json @@ -1,6 +1,6 @@ { "name": "@devtoolcss/inliner", - "version": "1.0.1", + "version": "1.0.2", "description": "A CSS inliner library powered by Chrome DevTools Protocol.", "license": "MIT", "files": [ diff --git a/packages/cleanclone/package.json b/packages/cleanclone/package.json index 8b55376..364286b 100644 --- a/packages/cleanclone/package.json +++ b/packages/cleanclone/package.json @@ -1,6 +1,6 @@ { "name": "cleanclone", - "version": "1.0.1", + "version": "1.0.2", "description": "Inline DevTool-parsed CSS for any website and crawl it on the fly.", "license": "MIT", "files": [ From 576a5a722397f58940f4c97416ae7a1737ce8a0d Mon Sep 17 00:00:00 2001 From: Bill Tsui Date: Fri, 12 Dec 2025 20:12:41 +0800 Subject: [PATCH 36/36] ci: merge publish workflow --- .github/workflows/publish-single.yml | 32 ------------------- .../{publish-all.yml => publish.yml} | 22 +++++++++++-- 2 files changed, 19 insertions(+), 35 deletions(-) delete mode 100644 .github/workflows/publish-single.yml rename .github/workflows/{publish-all.yml => publish.yml} (53%) diff --git a/.github/workflows/publish-single.yml b/.github/workflows/publish-single.yml deleted file mode 100644 index e5df99b..0000000 --- a/.github/workflows/publish-single.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Publish Single Package to npmjs -on: - workflow_dispatch: - inputs: - package: - description: "Name of the package to publish (e.g. uiexport)" - required: true - type: string - -jobs: - publish-single: - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - env: - NODE_ENV: production - steps: - - uses: actions/checkout@v5 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: "24.x" - registry-url: "https://registry.npmjs.org" - cache: "pnpm" - - - run: pnpm install --frozen-lockfile - - - name: Publish single package - run: pnpm publish --filter ${{ github.event.inputs.package }} --no-git-checks diff --git a/.github/workflows/publish-all.yml b/.github/workflows/publish.yml similarity index 53% rename from .github/workflows/publish-all.yml rename to .github/workflows/publish.yml index 4945120..528b474 100644 --- a/.github/workflows/publish-all.yml +++ b/.github/workflows/publish.yml @@ -1,9 +1,16 @@ -name: Publish Package to npmjs +name: Publish Package(s) to npmjs on: release: types: [published] + workflow_dispatch: + inputs: + package: + description: "Name of the package to publish (e.g. uiexport)" + required: true + type: string + jobs: - build: + publish: runs-on: ubuntu-latest permissions: contents: write @@ -23,12 +30,21 @@ jobs: - run: pnpm install --frozen-lockfile - - run: pnpm -r publish --no-git-checks + - name: Publish all packages (on release) + if: github.event_name == 'release' + run: pnpm -r publish --no-git-checks - name: Zip dist/uiexport + if: github.event_name == 'release' run: | cd packages/uiexport/dist && zip -r uiexport.zip . && mv uiexport.zip ../../.. + - name: Upload extension to GitHub Release + if: github.event_name == 'release' uses: softprops/action-gh-release@v2 with: files: uiexport.zip + + - name: Publish single package (on manual dispatch) + if: github.event_name == 'workflow_dispatch' + run: pnpm publish --filter ${{ github.event.inputs.package }} --no-git-checks