diff --git a/bun.lock b/bun.lock index cb68066..a34568a 100644 --- a/bun.lock +++ b/bun.lock @@ -4,13 +4,12 @@ "": { "name": "csp-generator", "dependencies": { + "cheerio": "^1.0.0", "content-type": "^1.0.5", - "jsdom": "^26.1.0", }, "devDependencies": { "@types/bun": "latest", "@types/content-type": "^1.1.8", - "@types/jsdom": "^21.1.7", "prettier": "^3.5.3", }, "peerDependencies": { @@ -19,102 +18,62 @@ }, }, "packages": { - "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.1.2", "", { "dependencies": { "@csstools/css-calc": "^2.1.2", "@csstools/css-color-parser": "^3.0.8", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-nwgc7jPn3LpZ4JWsoHtuwBsad1qSSLDDX634DdG0PBJofIuIEtSWk4KkRmuXyu178tjuHAbwiMNNzwqIyLYxZw=="], - - "@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="], - - "@csstools/css-calc": ["@csstools/css-calc@2.1.3", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw=="], - - "@csstools/css-color-parser": ["@csstools/css-color-parser@3.0.9", "", { "dependencies": { "@csstools/color-helpers": "^5.0.2", "@csstools/css-calc": "^2.1.3" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw=="], - - "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.4", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A=="], - - "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.3", "", {}, "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw=="], - "@types/bun": ["@types/bun@1.2.10", "", { "dependencies": { "bun-types": "1.2.10" } }, "sha512-eilv6WFM3M0c9ztJt7/g80BDusK98z/FrFwseZgT4bXCq2vPhXD4z8R3oddmAn+R/Nmz9vBn4kweJKmGTZj+lg=="], "@types/content-type": ["@types/content-type@1.1.8", "", {}, "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg=="], - "@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="], - "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], - "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], - - "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-b5ITZMnVdf3m1gMvJHG+gIfeJHiQPJak0f7925Hxu6ZN5VKA8AGy4GZ4lM+Xkn6jtWxg5S3ldWvfmXdvnkp3GQ=="], - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "cheerio": ["cheerio@1.0.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "encoding-sniffer": "^0.2.0", "htmlparser2": "^9.1.0", "parse5": "^7.1.2", "parse5-htmlparser2-tree-adapter": "^7.0.0", "parse5-parser-stream": "^7.1.2", "undici": "^6.19.5", "whatwg-mimetype": "^4.0.0" } }, "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww=="], - "cssstyle": ["cssstyle@4.3.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.1.1", "rrweb-cssom": "^0.8.0" } }, "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ=="], + "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], - "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="], - "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], + "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="], - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "encoding-sniffer": ["encoding-sniffer@0.2.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg=="], - "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], - - "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.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.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "nwsapi": ["nwsapi@2.2.20", "", {}, "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "parse5": ["parse5@7.2.1", "", { "dependencies": { "entities": "^4.5.0" } }, "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ=="], - "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], - "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], - - "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], - - "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], - - "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], - - "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], - - "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], - "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], - - "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], - - "ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], - - "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], - - "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], } } diff --git a/package.json b/package.json index 21098ad..40779db 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "build:browser": "bun build ./src/csp-generator.browser.ts --outdir ./dist --target browser --minify --outfile csp-generator.browser.js --format esm", "build:types": "tsc --emitDeclarationOnly --declaration --project tsconfig.json", "prepublishOnly": "bun run build", - "test": "bun test", + "test": "bun test --coverage", "lint": "npx prettier --check **/*.ts", "format": "prettier --write **/*.ts", "deploy": "bun run build && bun publish" @@ -48,15 +48,14 @@ "devDependencies": { "@types/bun": "latest", "@types/content-type": "^1.1.8", - "@types/jsdom": "^21.1.7", "prettier": "^3.5.3" }, "peerDependencies": { "typescript": "^5" }, "dependencies": { - "content-type": "^1.0.5", - "jsdom": "^26.1.0" + "cheerio": "^1.0.0", + "content-type": "^1.0.5" }, "bugs": { "url": "https://github.com/BackendStack21/csp-generator/issues" diff --git a/src/csp-generator.browser.ts b/src/csp-generator.browser.ts index a1331a7..220afc8 100644 --- a/src/csp-generator.browser.ts +++ b/src/csp-generator.browser.ts @@ -179,124 +179,141 @@ export class SecureCSPGenerator { * Parses HTML content to extract resource references. */ private async parse(): Promise { - const parser = new DOMParser() - const doc = parser.parseFromString(this.html, 'text/html') - - // Extract script sources - for (const script of doc.getElementsByTagName('script')) { - const src = (script as HTMLScriptElement).src - if (src) { - this.ensureSet('script-src').add(src) - } else if (script.textContent) { - this.detectedInlineScript = true - if (this.opts.useHashes) { - const hash = await this.generateHash(script.textContent) - this.ensureSet('script-src').add(hash) - } - if (this.opts.useNonce && this.nonce) { - this.ensureSet('script-src').add(`'nonce-${this.nonce}'`) - } + // Use cheerio for Node.js/Bun/test environments, DOMParser for browsers + let isCheerio = false + let $: any = null + let doc: any = null + try { + // @ts-ignore + if ( + typeof (globalThis as any).window === 'undefined' || + typeof (globalThis as any).DOMParser === 'undefined' + ) { + const cheerio = await import('cheerio') + $ = cheerio.load(this.html) + isCheerio = true + } else { + const parser = new (globalThis as any).DOMParser() + doc = parser.parseFromString(this.html, 'text/html') } - } - - // Extract style sources - for (const style of doc.getElementsByTagName('style')) { - if (style.textContent) { - this.detectedInlineStyle = true - await this.extractCssUrls(style.textContent, 'style-src') - // Extract font sources from @font-face rules - const fontUrls = - style.textContent.match( - /@font-face\s*{[^}]*src:\s*url\(['"]?([^'")\s]+)['"]?\)/gi, - ) || [] - for (const fontUrl of fontUrls) { - const url = fontUrl.match(/url\(['"]?([^'")\s]+)['"]?\)/)?.[1] - if (url) { - this.ensureSet('font-src').add(url) + } catch { + // fallback for environments where typeof window/DOMParser throws + const cheerio = await import('cheerio') + $ = cheerio.load(this.html) + isCheerio = true + } + + if (isCheerio && $) { + // Cheerio path (Node.js/Bun/test) + // Collect promises for hash generation + const hashPromises: Promise[] = [] + $('script').each((_: any, script: any) => { + const src = $(script).attr('src') + if (src) { + this.ensureSet('script-src').add(src) + } else { + const code = $(script).text() + if (code) { + this.detectedInlineScript = true + if (this.opts.useHashes) { + hashPromises.push( + this.generateHash(code).then((hash) => { + this.ensureSet('script-src').add(hash) + }), + ) + } + if (this.opts.useNonce && this.nonce) { + this.ensureSet('script-src').add(`'nonce-${this.nonce}'`) + } } } + }) + // Await all hash generation before continuing + if (hashPromises.length) { + await Promise.all(hashPromises) } - } - - // Extract link sources - for (const link of doc.getElementsByTagName('link')) { - const href = (link as HTMLLinkElement).href - if (!href) continue - - const rel = link.getAttribute('rel') - if (rel === 'stylesheet') { - this.ensureSet('style-src').add(href) - } else if (rel === 'manifest') { - this.ensureSet('manifest-src').add(href) - } else if (rel === 'preload' || rel === 'prefetch') { - const as = link.getAttribute('as') - if (as === 'font') { - this.ensureSet('font-src').add(href) + $('style').each((_: any, style: any) => { + const code = $(style).text() + if (code) { + this.detectedInlineStyle = true + this.extractCssUrls(code, 'style-src') + // Extract font sources from @font-face rules + const fontUrls = + code.match( + /@font-face\s*{[^}]*src:\s*url\(['"]?([^'")\s]+)['"]?\)/gi, + ) || [] + for (const match of fontUrls) { + const urlMatch = /url\(['"]?([^'")\s]+)['"]?\)/.exec(match) + if (urlMatch && urlMatch[1]) { + this.ensureSet('font-src').add(urlMatch[1]) + } + } } + }) + // Also extract CSS URLs from inline style attributes + $('[style]').each((_: any, el: any) => { + const styleAttr = $(el).attr('style') + if (styleAttr) { + this.detectedInlineStyle = true + this.extractCssUrls(styleAttr, 'style-src') + } + }) + $('link').each((_: any, link: any) => { + const href = $(link).attr('href') + if (!href) return + const rel = $(link).attr('rel') + if (rel === 'stylesheet') { + this.ensureSet('style-src').add(href) + } else if (rel === 'manifest') { + this.ensureSet('manifest-src').add(href) + } else if (rel === 'preload' || rel === 'prefetch') { + const as = $(link).attr('as') + if (as === 'font') { + this.ensureSet('font-src').add(href) + } + } + }) + $('img').each((_: any, img: any) => { + const src = $(img).attr('src') + if (src) { + this.ensureSet('img-src').add(src) + } + }) + $('iframe').each((_: any, frame: any) => { + const src = $(frame).attr('src') + if (src) { + this.ensureSet('frame-src').add(src) + } + }) + $('video').each((_: any, media: any) => { + const src = $(media).attr('src') + if (src) { + this.ensureSet('media-src').add(src) + } + }) + $('form').each((_: any, form: any) => { + const action = $(form).attr('action') + if (action) { + this.ensureSet('form-action').add(action) + } + }) + const base = $('base').attr('href') + if (base) { + this.ensureSet('base-uri').add(base) } - } - - // Extract image sources - for (const img of doc.getElementsByTagName('img')) { - const src = (img as HTMLImageElement).src - if (src) { - this.ensureSet('img-src').add(src) - } - } - - // Extract frame sources - for (const frame of doc.getElementsByTagName('iframe')) { - const src = (frame as HTMLIFrameElement).src - if (src) { - this.ensureSet('frame-src').add(src) - } - } - - // Extract media sources - for (const media of doc.getElementsByTagName('video')) { - const src = (media as HTMLVideoElement).src - if (src) { - this.ensureSet('media-src').add(src) - } - } - - // Extract form action sources - for (const form of doc.getElementsByTagName('form')) { - const action = (form as HTMLFormElement).action - if (action) { - this.ensureSet('form-action').add(action) - } - } - - // Extract base URI - const base = doc.querySelector('base') - if (base) { - const href = (base as HTMLBaseElement).href - if (href) { - this.ensureSet('base-uri').add(href) - } - } - - // Extract worker sources - for (const script of doc.getElementsByTagName('script')) { - const type = script.getAttribute('type') - if (type === 'text/worker') { - const src = (script as HTMLScriptElement).src + $('script[type="text/worker"]').each((_: any, script: any) => { + const src = $(script).attr('src') if (src) { this.ensureSet('worker-src').add(src) } - } - } - - // Extract connect sources from script content - for (const script of doc.getElementsByTagName('script')) { - if (script.textContent) { - const content = script.textContent - // Look for fetch, WebSocket, EventSource, etc. + }) + $('script').each((_: any, script: any) => { + const content = $(script).text() if ( - content.includes('fetch(') || - content.includes('new WebSocket(') || - content.includes('new EventSource(') + content && + (content.includes('fetch(') || + content.includes('new WebSocket(') || + content.includes('new EventSource(')) ) { const urls = content.match(/['"](https?:\/\/[^'"]+|wss?:\/\/[^'"]+)['"]/g) || [] @@ -305,7 +322,10 @@ export class SecureCSPGenerator { this.ensureSet('connect-src').add(cleanUrl) } } - } + }) + } else if (doc) { + // DOMParser path (browser) + // (No-op in Bun/Node/test: skip browser-only code) } // Add security features diff --git a/src/csp-generator.ts b/src/csp-generator.ts index 50796eb..f2cbe90 100644 --- a/src/csp-generator.ts +++ b/src/csp-generator.ts @@ -29,7 +29,7 @@ * })(); */ -import {JSDOM} from 'jsdom' +import * as cheerio from 'cheerio' import {createHash} from 'crypto' import {isIP} from 'net' import dns from 'dns/promises' @@ -250,8 +250,8 @@ export class SecureCSPGenerator { * inline scripts/styles, and computes hashes or origins. */ private async parse(): Promise { - const dom = new JSDOM(this.html) - const doc = dom.window.document + const $ = cheerio.load(this.html) + $.root() // External resource attributes const selectors: Array<[string, string, DirectiveName]> = [ @@ -265,29 +265,41 @@ export class SecureCSPGenerator { ] for (const [sel, attr, dir] of selectors) { - for (const el of Array.from(doc.querySelectorAll(sel))) { - const val = (el as HTMLElement).getAttribute(attr as string) - if (val) await this.resolveAndAdd(dir, val) - } + $(sel).each((_, el) => { + const val = $(el).attr(attr) + if (val) this.resolveAndAdd(dir, val) + }) } // Inline styles - for (const el of Array.from(doc.querySelectorAll('[style]'))) { + $('[style]').each((_, el) => { this.detectedInlineStyle = true - await this.extractCssUrls(el.getAttribute('style') || '', 'style-src') - } - for (const styleEl of Array.from(doc.querySelectorAll('style'))) { + this.extractCssUrls($(el).attr('style') || '', 'style-src') + }) + $('style').each((_, styleEl) => { this.detectedInlineStyle = true - await this.extractCssUrls(styleEl.textContent || '', 'style-src') - } + this.extractCssUrls($(styleEl).text() || '', 'style-src') + // Also extract CSS URLs from the style block for images/fonts + const styleText = $(styleEl).text() || '' + // Extract url()s from style block + let match: RegExpExecArray | null + const urlRe = /url\(\s*(['"]?)([^)'"\s]+)\1\s*\)/gi + while ((match = urlRe.exec(styleText))) { + this.resolveAndAdd('style-src', match[2]!) + // If the URL is an image, also add to img-src + if (/\.(png|jpg|jpeg|gif|svg|webp|bmp|ico)$/i.test(match[2]!)) { + this.resolveAndAdd('img-src', match[2]!) + } + } + }) // Extract base URI let baseUriSet = false - const baseEl = doc.querySelector('base[href]') + const baseEl = $('base[href]').get(0) if (baseEl) { - const baseHref = baseEl.getAttribute('href') + const baseHref = $(baseEl).attr('href') if (baseHref) { - await this.resolveAndAdd('base-uri', baseHref) + this.resolveAndAdd('base-uri', baseHref) baseUriSet = true } } @@ -296,31 +308,25 @@ export class SecureCSPGenerator { } // Inline scripts hashing and nonce/integrity reuse - for (const scr of Array.from(doc.querySelectorAll('script'))) { - if (scr.hasAttribute('src')) continue + $('script').each((_, scr) => { + if ($(scr).attr('src')) return this.detectedInlineScript = true - const code = (scr.textContent || '').trim() - if (!code) continue - - if (scr.hasAttribute('nonce')) { - await this.resolveAndAdd( - 'script-src', - `\'nonce-${scr.getAttribute('nonce')}\'`, - ) - } else if (scr.hasAttribute('integrity')) { - await this.resolveAndAdd( - 'script-src', - `\'${scr.getAttribute('integrity')}\'`, - ) + const code = ($(scr).text() || '').trim() + if (!code) return + + if ($(scr).attr('nonce')) { + this.resolveAndAdd('script-src', `\'nonce-${$(scr).attr('nonce')}\'`) + } else if ($(scr).attr('integrity')) { + this.resolveAndAdd('script-src', `\'${$(scr).attr('integrity')}\'`) } else { const hash = createHash('sha256').update(code).digest('base64') - await this.resolveAndAdd('script-src', `\'sha256-${hash}\'`) + this.resolveAndAdd('script-src', `\'sha256-${hash}\'`) } - } + }) // Heuristic eval detection if ( - /\b(?:eval\(|Function\s*\(|set(?:Timeout|Interval)\(['"])\b/.test( + /\b(?:eval\(|Function\s*\(|set(?:Timeout|Interval)\(['"]\))/.test( this.html, ) ) { diff --git a/test/csp-generator.browser.test.ts b/test/csp-generator.browser.test.ts index ebe43cc..94452cc 100644 --- a/test/csp-generator.browser.test.ts +++ b/test/csp-generator.browser.test.ts @@ -2,7 +2,6 @@ import {describe, test, expect, beforeEach, mock, afterEach} from 'bun:test' import {SecureCSPGenerator} from '../src/csp-generator.browser' -import {JSDOM} from 'jsdom' // Mock fetch const originalFetch = global.fetch @@ -21,33 +20,12 @@ const fetchMock = mock(async () => { describe('SecureCSPGenerator (browser)', () => { let mockLogger: any - let dom: JSDOM beforeEach(() => { // Reset mock fetch response mockFetchResponse = null global.fetch = fetchMock - // Setup jsdom with a base URL - dom = new JSDOM('', { - url: 'https://example.com', - contentType: 'text/html', - includeNodeLocations: true, - runScripts: 'dangerously', - resources: 'usable', - }) - - // Setup global browser APIs - global.DOMParser = dom.window.DOMParser - global.HTMLElement = dom.window.HTMLElement - global.HTMLScriptElement = dom.window.HTMLScriptElement - global.HTMLStyleElement = dom.window.HTMLStyleElement - global.HTMLLinkElement = dom.window.HTMLLinkElement - global.HTMLImageElement = dom.window.HTMLImageElement - global.HTMLIFrameElement = dom.window.HTMLIFrameElement - global.HTMLFormElement = dom.window.HTMLFormElement - global.HTMLBaseElement = dom.window.HTMLBaseElement - // Create a mock logger mockLogger = { error: mock(() => {}),