From b6ae9e0d41e018d80a6670add9430bc820728929 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 28 Apr 2026 10:48:21 -0400 Subject: [PATCH 1/7] Add README.md into plugin package --- package.json | 12 +- playgrounds/build-backend/README.md | 8 + .../__tests__/build-backend.spec.ts | 27 ++ playgrounds/build-backend/package.json | 1 + playgrounds/build-frontend/README.md | 58 +++ .../__tests__/build-frontend.spec.ts | 94 ++++ playgrounds/build-frontend/package.json | 1 + pnpm-lock.yaml | 452 +++++++++++++++++- src/bundle/index.ts | 31 ++ src/bundle/readme-assets.ts | 301 ++++++++++++ src/commands/watch.ts | 3 +- 11 files changed, 981 insertions(+), 7 deletions(-) create mode 100644 playgrounds/build-backend/README.md create mode 100644 playgrounds/build-frontend/README.md create mode 100644 src/bundle/readme-assets.ts diff --git a/package.json b/package.json index 4690ec0..a48764b 100644 --- a/package.json +++ b/package.json @@ -41,14 +41,20 @@ "glob": "11.0.1", "jiti": "2.4.2", "jszip": "3.10.1", - "ws": "8.18.0", - "zod": "3.24.1", + "remark": "^15.0.1", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "tsup": "8.3.5", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", "vite": "6.0.7", - "tsup": "8.3.5" + "ws": "8.18.0", + "zod": "3.24.1" }, "devDependencies": { "@caido/eslint-config": "0.0.6", "@types/express": "5.0.0", + "@types/mdast": "^4.0.4", "@types/node": "22.10.2", "@types/ws": "8.5.13", "eslint": "9.17.0", diff --git a/playgrounds/build-backend/README.md b/playgrounds/build-backend/README.md new file mode 100644 index 0000000..26ae209 --- /dev/null +++ b/playgrounds/build-backend/README.md @@ -0,0 +1,8 @@ +# Backend Plugin Playground + +This is a test playground for building backend plugins. + +![Test Asset](assets/test.txt) +![External Image](https://example.com/image.png) + +The plugin includes a backend plugin with assets. diff --git a/playgrounds/build-backend/__tests__/build-backend.spec.ts b/playgrounds/build-backend/__tests__/build-backend.spec.ts index bf97b72..5d7557f 100644 --- a/playgrounds/build-backend/__tests__/build-backend.spec.ts +++ b/playgrounds/build-backend/__tests__/build-backend.spec.ts @@ -83,4 +83,31 @@ describe("build-backend", () => { expect(assetContent).toBeUndefined(); }); + + it("should have README.md file", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + expect(readmeContent).toContain("Backend Plugin Playground"); + }); + + it("should transform README image links to GitHub raw URLs", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + // Verify that the image link was transformed to a GitHub raw URL + expect(readmeContent).toMatch( + /https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt/, + ); + }); + + it("should remove external image URLs (http, https, data)", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + // Verify that external URLs are removed (set to empty string) + // The README has: ![External Image](https://example.com/image.png) + // After transformation, it should be: ![](...) + expect(readmeContent).toMatch(/!\[External Image\]\(\)/); + }); }); diff --git a/playgrounds/build-backend/package.json b/playgrounds/build-backend/package.json index 0997dbe..88ba041 100644 --- a/playgrounds/build-backend/package.json +++ b/playgrounds/build-backend/package.json @@ -9,5 +9,6 @@ "email": "hello@caido.com", "url": "https://caido.com" }, + "repository": "https://github.com/caido-community/dev", "license": "ISC" } diff --git a/playgrounds/build-frontend/README.md b/playgrounds/build-frontend/README.md new file mode 100644 index 0000000..c5e6e4c --- /dev/null +++ b/playgrounds/build-frontend/README.md @@ -0,0 +1,58 @@ +# Frontend Build Playground + +This directory serves as a test fixture for validating frontend plugin build behavior in `@caido-community/dev`. + +## Purpose + +It demonstrates a minimal frontend Caido plugin configuration and is used in integration tests to verify: + +- Vite-based frontend builds +- Asset bundling +- README and referenced asset inclusion in final plugin packages + +## Test Asset Reference + +The following image reference is used to test automatic asset detection from README content: + +![Test Asset](assets/test.txt) + +## Link Test Cases + +Local link that should be transformed to a GitHub raw URL: + +[Local Doc](assets/test.txt) + +External link that should be removed: + +[External Link](https://example.com/docs) + +Fragment-only link that should be preserved: + +[Jump to section](#purpose) + +## Reference-Style Definition Test Cases + +Reference-style image and link using definitions: + +![Ref Image][ref-image] +[Ref Link][ref-link] + +[ref-image]: assets/test.txt +[ref-link]: https://example.com/docs + +## HTML Test Cases + +Raw HTML with local and external URLs: + +HTML Local Image +HTML External Image +HTML Local Link +HTML External Link + +## External URL Test Cases + +The following images test external URL handling (should be removed): + +![External HTTP](http://example.com/image.png) +![External HTTPS](https://example.com/image.png) +![Data URI](data:image/png;base64,abc123) diff --git a/playgrounds/build-frontend/__tests__/build-frontend.spec.ts b/playgrounds/build-frontend/__tests__/build-frontend.spec.ts index 4edb181..74e1479 100644 --- a/playgrounds/build-frontend/__tests__/build-frontend.spec.ts +++ b/playgrounds/build-frontend/__tests__/build-frontend.spec.ts @@ -5,6 +5,100 @@ import { describe, expect, it } from "vitest"; import { getZipFileContent } from "../../../playgrounds/utils"; describe("build-frontend", () => { + it("should have README.md file", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + expect(readmeContent).toContain("Frontend Build Playground"); + }); + + it("should transform README image links to GitHub raw URLs", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + expect(readmeContent).toContain("raw.githubusercontent.com"); + expect(readmeContent).toContain("assets/test.txt"); + }); + + it("should remove external image URLs", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + // External URLs should be removed (set to empty string) + expect(readmeContent).not.toContain("http://example.com/image.png"); + expect(readmeContent).not.toContain("https://example.com/image.png"); + // data: URIs should be kept as-is (not removed) + expect(readmeContent).toContain("data:image/png;base64,abc123"); + // Image syntax should still exist but with empty URLs (alt text preserved) + expect(readmeContent).toContain("![External HTTP]()"); + expect(readmeContent).toContain("![External HTTPS]()"); + expect(readmeContent).toContain( + "![Data URI](data:image/png;base64,abc123)", + ); + }); + + it("should transform local markdown links to GitHub raw URLs", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + // [Local Doc](assets/test.txt) should become a GitHub raw URL + expect(readmeContent).toMatch( + /\[Local Doc\]\(https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt\)/, + ); + }); + + it("should remove external markdown link URLs", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + // External link URL should be removed but link text preserved + expect(readmeContent).not.toContain("https://example.com/docs"); + expect(readmeContent).toContain("[External Link]()"); + }); + + it("should preserve fragment-only links", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + // Same-document anchors should be preserved as-is + expect(readmeContent).toContain("[Jump to section](#purpose)"); + }); + + it("should transform reference-style definition URLs", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + // [ref-image]: assets/test.txt should be transformed to a GitHub raw URL + expect(readmeContent).toMatch( + /\[ref-image\]:\s*https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt/, + ); + // [ref-link]: https://example.com/docs is external and should be removed + expect(readmeContent).not.toContain("https://example.com/docs"); + }); + + it("should transform local HTML src/href attributes", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + // HTML and should be transformed + expect(readmeContent).toMatch( + /src="https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt"/, + ); + expect(readmeContent).toMatch( + /href="https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt"/, + ); + }); + + it("should remove external URLs in HTML attributes", async () => { + const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); + const readmeContent = await getZipFileContent(zipPath, "README.md"); + expect(readmeContent).toBeDefined(); + // External URLs in HTML should be emptied + expect(readmeContent).not.toContain("https://example.com/image.png"); + expect(readmeContent).not.toContain("https://example.com/docs"); + expect(readmeContent).toContain('src=""'); + expect(readmeContent).toContain('href=""'); + }); it("should have manifest.json file", async () => { const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); diff --git a/playgrounds/build-frontend/package.json b/playgrounds/build-frontend/package.json index 73c17d8..2507ec6 100644 --- a/playgrounds/build-frontend/package.json +++ b/playgrounds/build-frontend/package.json @@ -9,5 +9,6 @@ "email": "hello@caido.com", "url": "https://caido.com" }, + "repository": "https://github.com/caido-community/dev", "license": "ISC" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b5cf1f..03e2d60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,9 +32,24 @@ importers: jszip: specifier: 3.10.1 version: 3.10.1 + remark: + specifier: ^15.0.1 + version: 15.0.1 + remark-parse: + specifier: ^11.0.0 + version: 11.0.0 + remark-stringify: + specifier: ^11.0.0 + version: 11.0.0 tsup: specifier: 8.3.5 version: 8.3.5(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2) + unified: + specifier: ^11.0.5 + version: 11.0.5 + unist-util-visit: + specifier: ^5.1.0 + version: 5.1.0 vite: specifier: 6.0.7 version: 6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) @@ -51,6 +66,9 @@ importers: '@types/express': specifier: 5.0.0 version: 5.0.0 + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 '@types/node': specifier: 22.10.2 version: 22.10.2 @@ -68,7 +86,7 @@ importers: version: 5.7.2 vitest: specifier: 3.0.5 - version: 3.0.5(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) + version: 3.0.5(@types/debug@4.1.13)(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) packages: @@ -508,51 +526,61 @@ packages: resolution: {integrity: sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.29.1': resolution: {integrity: sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.29.1': resolution: {integrity: sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.29.1': resolution: {integrity: sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.29.1': resolution: {integrity: sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.29.1': resolution: {integrity: sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.29.1': resolution: {integrity: sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.29.1': resolution: {integrity: sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.29.1': resolution: {integrity: sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.29.1': resolution: {integrity: sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.29.1': resolution: {integrity: sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==} @@ -584,6 +612,9 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -602,9 +633,15 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.10.2': resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} @@ -620,6 +657,9 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/ws@8.5.13': resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} @@ -809,6 +849,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -879,6 +922,9 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + character-parser@2.2.0: resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} @@ -994,6 +1040,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1013,10 +1062,17 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1258,6 +1314,9 @@ packages: resolution: {integrity: sha512-V4UkHQc+B7ldh1YC84HCXHwf60M4BOMvp9rkvTUWCK5apqDC1Esnbid4wm6nFyVuDy8XMfETsJw5lsIGBWyo0A==} engines: {node: '>= 18'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1535,6 +1594,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -1658,6 +1721,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} @@ -1675,6 +1741,18 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -1695,6 +1773,69 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1963,6 +2104,15 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2208,6 +2358,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -2304,6 +2457,21 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -2328,6 +2496,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@3.0.5: resolution: {integrity: sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2482,6 +2656,9 @@ packages: zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@caido/eslint-config@0.0.6(@typescript-eslint/parser@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(prettier@3.4.2)(typescript@5.7.2)': @@ -2837,6 +3014,10 @@ snapshots: dependencies: '@types/node': 22.10.2 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/estree@1.0.6': {} '@types/express-serve-static-core@5.0.3': @@ -2859,8 +3040,14 @@ snapshots: '@types/json5@0.0.29': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} + '@types/node@22.10.2': dependencies: undici-types: 6.20.0 @@ -2880,6 +3067,8 @@ snapshots: '@types/node': 22.10.2 '@types/send': 0.17.4 + '@types/unist@3.0.3': {} + '@types/ws@8.5.13': dependencies: '@types/node': 22.10.2 @@ -3138,6 +3327,8 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 + bail@2.0.2: {} + balanced-match@1.0.2: {} body-parser@2.0.2: @@ -3222,6 +3413,8 @@ snapshots: chalk@5.4.1: {} + character-entities@2.0.2: {} + character-parser@2.2.0: dependencies: is-regex: 1.2.1 @@ -3304,6 +3497,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + deep-eql@5.0.2: {} deep-is@0.1.4: {} @@ -3322,8 +3519,14 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -3736,6 +3939,8 @@ snapshots: transitivePeerDependencies: - supports-color + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -4029,6 +4234,8 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} is-regex@1.2.1: @@ -4144,6 +4351,8 @@ snapshots: lodash@4.17.21: {} + longest-streak@3.1.0: {} + loupe@3.1.2: {} lru-cache@10.4.3: {} @@ -4156,6 +4365,44 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -4166,6 +4413,139 @@ snapshots: methods@1.1.2: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.0 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -4422,6 +4802,30 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -4717,6 +5121,8 @@ snapshots: tree-kill@1.2.2: {} + trough@2.2.0: {} + ts-api-utils@1.4.3(typescript@5.7.2): dependencies: typescript: 5.7.2 @@ -4841,6 +5247,35 @@ snapshots: undici-types@6.20.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + unpipe@1.0.0: {} update-browserslist-db@1.1.1(browserslist@4.24.3): @@ -4859,6 +5294,16 @@ snapshots: vary@1.1.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-node@3.0.5(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2): dependencies: cac: 6.7.14 @@ -4891,7 +5336,7 @@ snapshots: jiti: 2.4.2 tsx: 4.19.2 - vitest@3.0.5(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2): + vitest@3.0.5(@types/debug@4.1.13)(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2): dependencies: '@vitest/expect': 3.0.5 '@vitest/mocker': 3.0.5(vite@6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)) @@ -4914,6 +5359,7 @@ snapshots: vite-node: 3.0.5(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.13 '@types/node': 22.10.2 transitivePeerDependencies: - jiti @@ -5027,3 +5473,5 @@ snapshots: yocto-queue@0.1.0: {} zod@3.24.1: {} + + zwitch@2.0.4: {} diff --git a/src/bundle/index.ts b/src/bundle/index.ts index 50e84f3..182d9f2 100644 --- a/src/bundle/index.ts +++ b/src/bundle/index.ts @@ -10,6 +10,7 @@ import { addDirectoryToZip, logInfo, logSuccess } from "../utils"; import { bundleBackendPlugin } from "./backend"; import { bundleFrontendPlugin } from "./frontend"; +import { parseGitHubRepoInfo, transformReadmeImages } from "./readme-assets"; /** * Creates the dist directories. @@ -51,6 +52,36 @@ export async function bundlePackage(options: { // Create dist directories const { distDir, pluginPackageDir } = await createDistDirectories(cwd); + // Copy README.md (always included, always at root) + const readmePath = path.join(cwd, "README.md"); + try { + await fs.access(readmePath); + } catch { + throw new Error("README.md is required but not found in project root"); + } + + // Read repository URL from project's package.json + const packageJsonPath = path.join(cwd, "package.json"); + const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); + const packageJson = JSON.parse(packageJsonContent); + const repoUrl = + typeof packageJson.repository === "string" + ? packageJson.repository + : packageJson.repository?.url; + if (!repoUrl) { + throw new Error("package.json must have a valid 'repository' field"); + } + + // Parse GitHub repo info (default to main branch) + const repoInfo = parseGitHubRepoInfo(repoUrl, "main"); + + // Transform README image links to GitHub raw URLs + const transformedReadme = await transformReadmeImages(readmePath, repoInfo); + + // Write transformed README to package directory + const readmeDest = path.join(pluginPackageDir, "README.md"); + await fs.writeFile(readmeDest, transformedReadme); + // Create manifest const manifest = createManifest({ config }); diff --git a/src/bundle/readme-assets.ts b/src/bundle/readme-assets.ts new file mode 100644 index 0000000..fd0ecf9 --- /dev/null +++ b/src/bundle/readme-assets.ts @@ -0,0 +1,301 @@ +import fs from "fs/promises"; + +import type { Definition, Html, Image, Link } from "mdast"; +import remarkParse from "remark-parse"; +import remarkStringify from "remark-stringify"; +import { unified } from "unified"; +import { visit } from "unist-util-visit"; + +import { logInfo } from "../utils"; + +export interface GitHubRepoInfo { + owner: string; + repo: string; + commitHash: string; +} + +type UrlNode = Image | Link | Definition; + +/** + * Parses a GitHub repository URL to extract owner, repo, and branch info. + * Handles common URL formats including git+https:// and .git suffixes. + * @param repoUrl - The repository URL from package.json or config. + * @param branch - The branch to use for raw URLs (defaults to "main"). + * @returns Parsed GitHub repository information. + * @throws Error if the URL is not a valid GitHub repository URL. + */ +export function parseGitHubRepoInfo( + repoUrl: string, + branch = "main", +): GitHubRepoInfo { + // Clean common URL prefixes/suffixes + const cleanedUrl = repoUrl.replace(/^git\+/, "").replace(/\.git$/, ""); + const match = cleanedUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/); + + if (!match) { + throw new Error(`Invalid GitHub repository URL: ${repoUrl}`); + } + + const [, owner, repo] = match; + + // Check if running in GitHub Actions + const isGitHubActions = process.env.GITHUB_ACTIONS === "true"; + + let commitHash: string; + if (isGitHubActions) { + // Use GITHUB_SHA environment variable (automatically set in GitHub Actions) + const githubSha = process.env.GITHUB_SHA; + if (!githubSha) { + throw new Error( + "GITHUB_SHA environment variable is not set in GitHub Actions", + ); + } + commitHash = githubSha; + logInfo(`Using commit hash from GITHUB_SHA: ${commitHash}`); + } else { + // Not in GitHub Actions: use placeholder and log transformation + commitHash = "LOCAL_BUILD"; + logInfo( + `Not running in GitHub Actions, skipping commit hash fetch. README image links will use placeholder URL.`, + ); + } + + return { owner, repo, commitHash }; +} + +/** + * Checks if a URL is external (http or https). + * data: URIs and fragment identifiers are not considered external. + */ +function isExternalUrl(url: string): boolean { + if (url.startsWith("data:") || url.startsWith("#")) { + return false; + } + try { + const parsed = new URL(url); + return parsed.protocol === "http:" || parsed.protocol === "https:"; + } catch { + return false; + } +} + +/** + * Builds a GitHub raw URL for a local asset path. + */ +function buildRawUrl(originalUrl: string, repoInfo: GitHubRepoInfo): string { + return `https://raw.githubusercontent.com/${repoInfo.owner}/${repoInfo.repo}/${repoInfo.commitHash}/${originalUrl}`; +} + +/** + * Validates a GitHub raw URL by issuing a HEAD request. + * Only runs in GitHub Actions to avoid local rate limits. + * @returns true if URL is valid or validation was skipped, false if invalid. + */ +async function validateRawUrl(rawUrl: string): Promise { + if (process.env.GITHUB_ACTIONS !== "true") { + return true; + } + + try { + const response = await fetch(rawUrl, { method: "HEAD" }); + if (!response.ok) { + logInfo(`Warning: Asset not found at ${rawUrl}, removing reference`); + return false; + } + return true; + } catch { + logInfo( + `Warning: Failed to validate asset URL ${rawUrl}, removing reference`, + ); + return false; + } +} + +/** + * Transforms a URL on a node (image, link, or definition) to a GitHub raw URL + * if it points to a local asset. Removes the URL if it's external or invalid. + */ +async function transformNodeUrl( + node: UrlNode, + repoInfo: GitHubRepoInfo, + kind: string, +): Promise { + const originalUrl = node.url; + + // Skip empty URLs and fragment-only links (same-document anchors) + if (!originalUrl || originalUrl.startsWith("#")) { + return; + } + + // data: URIs are self-contained and should be kept as-is + if (originalUrl.startsWith("data:")) { + return; + } + + // External URLs are removed to prevent loading external resources + if (isExternalUrl(originalUrl)) { + logInfo(`Warning: Skipping external ${kind} URL in README: ${originalUrl}`); + node.url = ""; + return; + } + + // Construct GitHub raw URL for the local asset + const rawUrl = buildRawUrl(originalUrl, repoInfo); + + // Validate URL reachability (only in GitHub Actions) + const isValid = await validateRawUrl(rawUrl); + if (!isValid) { + node.url = ""; + return; + } + + node.url = rawUrl; + + if (repoInfo.commitHash === "LOCAL_BUILD") { + logInfo( + `Would transform README ${kind} link (not in GitHub Actions): ${originalUrl} → ${rawUrl}`, + ); + } else { + logInfo(`Transformed README ${kind} link: ${originalUrl} → ${rawUrl}`); + } +} + +/** + * Transforms URLs found in raw HTML nodes (e.g., , ). + * External URLs are removed (attribute value emptied), local paths are rewritten + * to GitHub raw URLs. + */ +async function transformHtmlNode( + node: Html, + repoInfo: GitHubRepoInfo, +): Promise { + // Match src="..." and href="..." attributes (single or double quotes) + const attributeRegex = /\b(src|href)\s*=\s*(["'])([^"']*)\2/gi; + const matches: { + attr: string; + quote: string; + value: string; + full: string; + }[] = []; + + let match: RegExpExecArray | undefined; + while ((match = attributeRegex.exec(node.value) ?? undefined) !== undefined) { + matches.push({ + attr: match[1], + quote: match[2], + value: match[3], + full: match[0], + }); + } + + let updatedValue = node.value; + for (const { attr, quote, value, full } of matches) { + const kind = attr === "src" ? "image" : "link"; + + // Skip empty, fragment-only, and data: URIs + if (!value || value.startsWith("#") || value.startsWith("data:")) { + continue; + } + + let replacement: string; + if (isExternalUrl(value)) { + logInfo( + `Warning: Skipping external ${kind} URL in README HTML: ${value}`, + ); + replacement = `${attr}=${quote}${quote}`; + } else { + const rawUrl = buildRawUrl(value, repoInfo); + const isValid = await validateRawUrl(rawUrl); + if (!isValid) { + replacement = `${attr}=${quote}${quote}`; + } else { + replacement = `${attr}=${quote}${rawUrl}${quote}`; + if (repoInfo.commitHash === "LOCAL_BUILD") { + logInfo( + `Would transform README HTML ${kind} (not in GitHub Actions): ${value} → ${rawUrl}`, + ); + } else { + logInfo(`Transformed README HTML ${kind}: ${value} → ${rawUrl}`); + } + } + } + + updatedValue = updatedValue.replace(full, replacement); + } + + node.value = updatedValue; +} + +/** + * Transforms local asset references in README.md to GitHub raw URLs. + * Uses remark to parse the markdown AST and handles multiple node types: + * - `image`: ![alt](path) + * - `link`: [text](path) + * - `definition`: [ref]: path (reference-style images/links) + * - `html`: and in raw HTML blocks + * + * External URLs (http, https) are removed to prevent loading external resources. + * data: URIs are preserved as they are self-contained. + * Fragment-only links (#anchor) are preserved as same-document anchors. + * + * @param readmePath - Absolute path to the project's README.md. + * @param repoInfo - Parsed GitHub repository information. + * @returns Modified README content with transformed URLs. + */ +export async function transformReadmeImages( + readmePath: string, + repoInfo: GitHubRepoInfo, +): Promise { + const content = await fs.readFile(readmePath, "utf-8"); + + // Initialize unified processor with remark parse and stringify plugins + const processor = unified().use(remarkParse).use(remarkStringify); + + // Parse markdown to AST + const ast = processor.parse(content); + + // Collect URL-bearing nodes by type. We collect first and process afterwards + // because unist-util-visit does not support async visitors. + const imageNodes: Image[] = []; + const linkNodes: Link[] = []; + const definitionNodes: Definition[] = []; + const htmlNodes: Html[] = []; + + visit(ast, (node) => { + switch (node.type) { + case "image": + imageNodes.push(node); + break; + case "link": + linkNodes.push(node); + break; + case "definition": + definitionNodes.push(node); + break; + case "html": + htmlNodes.push(node); + break; + default: + // Other node types do not carry URLs we need to transform. + break; + } + }); + + // Process each node type (async for URL validation via fetch) + for (const node of imageNodes) { + await transformNodeUrl(node, repoInfo, "image"); + } + for (const node of linkNodes) { + await transformNodeUrl(node, repoInfo, "link"); + } + for (const node of definitionNodes) { + await transformNodeUrl(node, repoInfo, "definition"); + } + for (const node of htmlNodes) { + await transformHtmlNode(node, repoInfo); + } + + // Stringify the modified AST back to markdown + const modifiedContent = processor.stringify(ast); + return modifiedContent; +} diff --git a/src/commands/watch.ts b/src/commands/watch.ts index 315eb8c..f5272ca 100644 --- a/src/commands/watch.ts +++ b/src/commands/watch.ts @@ -2,10 +2,9 @@ import fs from "fs/promises"; import { createServer } from "http"; import path from "path"; -import { Glob } from "glob"; - import { watch as chokidarWatch } from "chokidar"; import express, { type Request, type Response } from "express"; +import { Glob } from "glob"; import { type WebSocket, WebSocketServer } from "ws"; import { loadConfig } from "../config"; From 03f4a4d835ee448417b506ab11f552212a1aff11 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 28 Apr 2026 11:16:36 -0400 Subject: [PATCH 2/7] Fix assets tests --- assets/test.png | 1 + assets/test.txt | 1 + playgrounds/build-frontend/README.md | 2 +- playgrounds/build-frontend/__tests__/build-frontend.spec.ts | 4 ++-- 4 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 assets/test.png create mode 100644 assets/test.txt diff --git a/assets/test.png b/assets/test.png new file mode 100644 index 0000000..407f899 --- /dev/null +++ b/assets/test.png @@ -0,0 +1 @@ +PNG_PLACEHOLDER_FOR_TESTS diff --git a/assets/test.txt b/assets/test.txt new file mode 100644 index 0000000..802992c --- /dev/null +++ b/assets/test.txt @@ -0,0 +1 @@ +Hello world diff --git a/playgrounds/build-frontend/README.md b/playgrounds/build-frontend/README.md index c5e6e4c..91b8dba 100644 --- a/playgrounds/build-frontend/README.md +++ b/playgrounds/build-frontend/README.md @@ -44,7 +44,7 @@ Reference-style image and link using definitions: Raw HTML with local and external URLs: -HTML Local Image +HTML Local Image HTML External Image HTML Local Link HTML External Link diff --git a/playgrounds/build-frontend/__tests__/build-frontend.spec.ts b/playgrounds/build-frontend/__tests__/build-frontend.spec.ts index 74e1479..de77854 100644 --- a/playgrounds/build-frontend/__tests__/build-frontend.spec.ts +++ b/playgrounds/build-frontend/__tests__/build-frontend.spec.ts @@ -80,9 +80,9 @@ describe("build-frontend", () => { const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); const readmeContent = await getZipFileContent(zipPath, "README.md"); expect(readmeContent).toBeDefined(); - // HTML and should be transformed + // HTML and should be transformed expect(readmeContent).toMatch( - /src="https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt"/, + /src="https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.png"/, ); expect(readmeContent).toMatch( /href="https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt"/, From a0114307a4a05dcd28c95f60a8595fc0f4316058 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 28 Apr 2026 14:10:23 -0400 Subject: [PATCH 3/7] Add knip, fix knip issues, migrated to inline assets in README, added unit tests for image compression function --- .github/workflows/validate.yml | 3 + README.md | 4 + knip.json | 9 + package.json | 4 +- playgrounds/build-backend/README.md | 2 +- .../__tests__/build-backend.spec.ts | 8 +- playgrounds/build-backend/assets/test.png | Bin 0 -> 68 bytes playgrounds/build-backend/package.json | 1 - playgrounds/build-frontend/README.md | 6 +- .../__tests__/build-frontend.spec.ts | 31 +- playgrounds/build-frontend/assets/test.png | Bin 0 -> 68 bytes playgrounds/build-frontend/package.json | 1 - playgrounds/setup.ts | 4 + pnpm-lock.yaml | 956 +++++++++++++++++- src/bundle/index.ts | 21 +- src/bundle/readme-assets.spec.ts | 76 ++ src/bundle/readme-assets.ts | 254 ++--- src/types.ts | 19 +- vitest.config.ts | 2 +- 19 files changed, 1164 insertions(+), 237 deletions(-) create mode 100644 knip.json create mode 100644 playgrounds/build-backend/assets/test.png create mode 100644 playgrounds/build-frontend/assets/test.png create mode 100644 src/bundle/readme-assets.spec.ts diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b56dc56..d059623 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -44,3 +44,6 @@ jobs: - name: Typecheck run: pnpm typecheck + + - name: Knip + run: pnpm exec knip diff --git a/README.md b/README.md index 941a41f..38d3835 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,7 @@ caido-dev watch [path] [--config ] - **Description**: Start the development server and watch for changes. - **Options**: - `-c, --config `: Path to the `caido.config.ts` file. + +## README Assets + +Plugin packages always include the root `README.md`. Local README images are converted to compressed WebP data URIs during packaging, with each image limited to about 125 KiB and the final README limited to about 2 MiB. External `http` and `https` URLs are removed from README links and images, while `data:` URIs and fragment links are preserved. diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..d500aec --- /dev/null +++ b/knip.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "entry": ["src/**/*.spec.ts", "playgrounds/**/*.spec.ts"], + "project": ["src/**/*.ts", "playgrounds/**/*.ts", "*.config.ts"], + "ignore": [ + "playgrounds/**/caido.config.ts", + "playgrounds/**/packages/**" + ] +} diff --git a/package.json b/package.json index a48764b..aa13cf0 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,9 @@ "glob": "11.0.1", "jiti": "2.4.2", "jszip": "3.10.1", - "remark": "^15.0.1", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", + "sharp": "^0.34.5", "tsup": "8.3.5", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", @@ -58,7 +58,7 @@ "@types/node": "22.10.2", "@types/ws": "8.5.13", "eslint": "9.17.0", - "tsup": "8.3.5", + "knip": "^6.7.0", "tsx": "4.19.2", "typescript": "5.7.2", "vitest": "3.0.5" diff --git a/playgrounds/build-backend/README.md b/playgrounds/build-backend/README.md index 26ae209..f3d61ac 100644 --- a/playgrounds/build-backend/README.md +++ b/playgrounds/build-backend/README.md @@ -2,7 +2,7 @@ This is a test playground for building backend plugins. -![Test Asset](assets/test.txt) +![Test Asset](assets/test.png) ![External Image](https://example.com/image.png) The plugin includes a backend plugin with assets. diff --git a/playgrounds/build-backend/__tests__/build-backend.spec.ts b/playgrounds/build-backend/__tests__/build-backend.spec.ts index 5d7557f..f41999b 100644 --- a/playgrounds/build-backend/__tests__/build-backend.spec.ts +++ b/playgrounds/build-backend/__tests__/build-backend.spec.ts @@ -91,14 +91,12 @@ describe("build-backend", () => { expect(readmeContent).toContain("Backend Plugin Playground"); }); - it("should transform README image links to GitHub raw URLs", async () => { + it("should inline README image links as WebP data URIs", async () => { const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); const readmeContent = await getZipFileContent(zipPath, "README.md"); expect(readmeContent).toBeDefined(); - // Verify that the image link was transformed to a GitHub raw URL - expect(readmeContent).toMatch( - /https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt/, - ); + expect(readmeContent).toContain("data:image/webp;base64,"); + expect(readmeContent).not.toContain("assets/test.png"); }); it("should remove external image URLs (http, https, data)", async () => { diff --git a/playgrounds/build-backend/assets/test.png b/playgrounds/build-backend/assets/test.png new file mode 100644 index 0000000000000000000000000000000000000000..e0ccec79f1b36f25d0ea94a047499d8b63045bba GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$fBwreFf%hTyx+h4 QHc*(s)78&qol`;+0FQDG@c;k- literal 0 HcmV?d00001 diff --git a/playgrounds/build-backend/package.json b/playgrounds/build-backend/package.json index 88ba041..0997dbe 100644 --- a/playgrounds/build-backend/package.json +++ b/playgrounds/build-backend/package.json @@ -9,6 +9,5 @@ "email": "hello@caido.com", "url": "https://caido.com" }, - "repository": "https://github.com/caido-community/dev", "license": "ISC" } diff --git a/playgrounds/build-frontend/README.md b/playgrounds/build-frontend/README.md index 91b8dba..6a62399 100644 --- a/playgrounds/build-frontend/README.md +++ b/playgrounds/build-frontend/README.md @@ -14,11 +14,11 @@ It demonstrates a minimal frontend Caido plugin configuration and is used in int The following image reference is used to test automatic asset detection from README content: -![Test Asset](assets/test.txt) +![Test Asset](assets/test.png) ## Link Test Cases -Local link that should be transformed to a GitHub raw URL: +Local link that should be preserved: [Local Doc](assets/test.txt) @@ -37,7 +37,7 @@ Reference-style image and link using definitions: ![Ref Image][ref-image] [Ref Link][ref-link] -[ref-image]: assets/test.txt +[ref-image]: assets/test.png [ref-link]: https://example.com/docs ## HTML Test Cases diff --git a/playgrounds/build-frontend/__tests__/build-frontend.spec.ts b/playgrounds/build-frontend/__tests__/build-frontend.spec.ts index de77854..1cffa2c 100644 --- a/playgrounds/build-frontend/__tests__/build-frontend.spec.ts +++ b/playgrounds/build-frontend/__tests__/build-frontend.spec.ts @@ -12,12 +12,12 @@ describe("build-frontend", () => { expect(readmeContent).toContain("Frontend Build Playground"); }); - it("should transform README image links to GitHub raw URLs", async () => { + it("should inline README image links as WebP data URIs", async () => { const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); const readmeContent = await getZipFileContent(zipPath, "README.md"); expect(readmeContent).toBeDefined(); - expect(readmeContent).toContain("raw.githubusercontent.com"); - expect(readmeContent).toContain("assets/test.txt"); + expect(readmeContent).toContain("data:image/webp;base64,"); + expect(readmeContent).not.toContain("assets/test.png"); }); it("should remove external image URLs", async () => { @@ -37,14 +37,11 @@ describe("build-frontend", () => { ); }); - it("should transform local markdown links to GitHub raw URLs", async () => { + it("should preserve local markdown link URLs", async () => { const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); const readmeContent = await getZipFileContent(zipPath, "README.md"); expect(readmeContent).toBeDefined(); - // [Local Doc](assets/test.txt) should become a GitHub raw URL - expect(readmeContent).toMatch( - /\[Local Doc\]\(https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt\)/, - ); + expect(readmeContent).toContain("[Local Doc](assets/test.txt)"); }); it("should remove external markdown link URLs", async () => { @@ -64,29 +61,21 @@ describe("build-frontend", () => { expect(readmeContent).toContain("[Jump to section](#purpose)"); }); - it("should transform reference-style definition URLs", async () => { + it("should inline reference-style image definition URLs", async () => { const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); const readmeContent = await getZipFileContent(zipPath, "README.md"); expect(readmeContent).toBeDefined(); - // [ref-image]: assets/test.txt should be transformed to a GitHub raw URL - expect(readmeContent).toMatch( - /\[ref-image\]:\s*https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt/, - ); + expect(readmeContent).toMatch(/\[ref-image\]:\s*data:image\/webp;base64,/); // [ref-link]: https://example.com/docs is external and should be removed expect(readmeContent).not.toContain("https://example.com/docs"); }); - it("should transform local HTML src/href attributes", async () => { + it("should inline local HTML image src attributes and preserve href attributes", async () => { const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); const readmeContent = await getZipFileContent(zipPath, "README.md"); expect(readmeContent).toBeDefined(); - // HTML and should be transformed - expect(readmeContent).toMatch( - /src="https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.png"/, - ); - expect(readmeContent).toMatch( - /href="https:\/\/raw\.githubusercontent\.com\/.*\/assets\/test\.txt"/, - ); + expect(readmeContent).toMatch(/src="data:image\/webp;base64,/); + expect(readmeContent).toContain('href="assets/test.txt"'); }); it("should remove external URLs in HTML attributes", async () => { diff --git a/playgrounds/build-frontend/assets/test.png b/playgrounds/build-frontend/assets/test.png new file mode 100644 index 0000000000000000000000000000000000000000..e0ccec79f1b36f25d0ea94a047499d8b63045bba GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$fBwreFf%hTyx+h4 QHc*(s)78&qol`;+0FQDG@c;k- literal 0 HcmV?d00001 diff --git a/playgrounds/build-frontend/package.json b/playgrounds/build-frontend/package.json index 2507ec6..73c17d8 100644 --- a/playgrounds/build-frontend/package.json +++ b/playgrounds/build-frontend/package.json @@ -9,6 +9,5 @@ "email": "hello@caido.com", "url": "https://caido.com" }, - "repository": "https://github.com/caido-community/dev", "license": "ISC" } diff --git a/playgrounds/setup.ts b/playgrounds/setup.ts index c56f87d..35be3f8 100644 --- a/playgrounds/setup.ts +++ b/playgrounds/setup.ts @@ -7,6 +7,10 @@ beforeAll(({ file }) => { // Get the test file path from the current test file const testPath = file.filepath; + if (!testPath.split(path.sep).includes("playgrounds")) { + return; + } + // Find the playground directory (parent of __tests__ directory) const playgroundDir = path.dirname(path.dirname(testPath)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03e2d60..d6af83b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,18 +32,18 @@ importers: jszip: specifier: 3.10.1 version: 3.10.1 - remark: - specifier: ^15.0.1 - version: 15.0.1 remark-parse: specifier: ^11.0.0 version: 11.0.0 remark-stringify: specifier: ^11.0.0 version: 11.0.0 + sharp: + specifier: ^0.34.5 + version: 0.34.5 tsup: specifier: 8.3.5 - version: 8.3.5(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2) + version: 8.3.5(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.8.3) unified: specifier: ^11.0.5 version: 11.0.5 @@ -52,7 +52,7 @@ importers: version: 5.1.0 vite: specifier: 6.0.7 - version: 6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) + version: 6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3) ws: specifier: 8.18.0 version: 8.18.0 @@ -78,6 +78,9 @@ importers: eslint: specifier: 9.17.0 version: 9.17.0(jiti@2.4.2) + knip: + specifier: ^6.7.0 + version: 6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0) tsx: specifier: 4.19.2 version: 4.19.2 @@ -86,7 +89,7 @@ importers: version: 5.7.2 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.13)(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) + version: 3.0.5(@types/debug@4.1.13)(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3) packages: @@ -99,6 +102,18 @@ packages: '@caido/plugin-manifest@0.3.0': resolution: {integrity: sha512-HRGHf65K2sfSdaEUwkCNDlurJ4zL0bOUg/Db4u6CrwjTTzmg2gOzP6SzLRj+69gWmxOm5LUjhInNHaMIRmQkHw==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.23.1': resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} engines: {node: '>=18'} @@ -447,6 +462,159 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -472,6 +640,12 @@ packages: '@mdn/browser-compat-data@5.6.27': resolution: {integrity: sha512-72sl6Xbr9RVH5iUoQmb2Bv6zXt9qN7kXZmLYhYuQclC+ZaWfILiQ7ej0k8KZXRSAlgPinN67myIn9odCqvqu+w==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -484,6 +658,244 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.19.1': + resolution: {integrity: sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + resolution: {integrity: sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.19.1': + resolution: {integrity: sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + resolution: {integrity: sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + resolution: {integrity: sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + resolution: {integrity: sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.19.1': + resolution: {integrity: sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + resolution: {integrity: sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + resolution: {integrity: sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -606,6 +1018,9 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} @@ -1070,6 +1485,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -1339,6 +1758,9 @@ packages: fastq@1.18.0: resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fdir@6.4.2: resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} peerDependencies: @@ -1347,6 +1769,15 @@ packages: picomatch: optional: true + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1377,6 +1808,11 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1416,6 +1852,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} @@ -1657,6 +2096,10 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1687,6 +2130,11 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + knip@6.7.0: + resolution: {integrity: sha512-ckL51NDH1YJxnv1kNB0iUdDngB4f/e9Igz8uIqYfmNDoyOFmmk1V0WFv3LQ7/hzC63b2Z9X41gGUE9eOWrZpaA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1947,6 +2395,13 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.19.1: + resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2010,6 +2465,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -2110,9 +2569,6 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - remark@15.0.1: - resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2179,6 +2635,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@1.1.0: resolution: {integrity: sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==} engines: {node: '>= 18'} @@ -2205,6 +2666,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2236,6 +2701,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2293,6 +2762,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -2331,6 +2804,10 @@ packages: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2450,6 +2927,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + unbash@3.0.0: + resolution: {integrity: sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA==} + engines: {node: '>=14'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -2586,6 +3067,10 @@ packages: peerDependencies: eslint: '>=6.0.0' + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -2649,6 +3134,11 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2656,6 +3146,9 @@ packages: zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -2688,6 +3181,27 @@ snapshots: dependencies: ajv: 8.17.1 + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.23.1': optional: true @@ -2889,6 +3403,102 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2917,6 +3527,20 @@ snapshots: '@mdn/browser-compat-data@5.6.27': {} + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2929,6 +3553,137 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.18.0 + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + optional: true + '@pkgjs/parseargs@0.11.0': optional: true @@ -3005,6 +3760,11 @@ snapshots: - supports-color - typescript + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 @@ -3194,13 +3954,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.5(vite@6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2))': + '@vitest/mocker@3.0.5(vite@6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) + vite: 6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3) '@vitest/pretty-format@3.0.5': dependencies: @@ -3523,6 +4283,8 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.1.2: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -3963,10 +4725,18 @@ snapshots: dependencies: reusify: 1.0.4 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fdir@6.4.2(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -4008,6 +4778,10 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -4054,6 +4828,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.2.7 + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -4295,6 +5073,8 @@ snapshots: jiti@2.4.2: {} + jiti@2.6.1: {} + joycon@3.1.1: {} js-yaml@4.1.0: @@ -4324,6 +5104,26 @@ snapshots: dependencies: json-buffer: 3.0.1 + knip@6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0): + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + formatly: 0.3.0 + get-tsconfig: 4.14.0 + jiti: 2.6.1 + minimist: 1.2.8 + oxc-parser: 0.127.0 + oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0) + picomatch: 4.0.4 + smol-toml: 1.6.1 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.16 + unbash: 3.0.0 + yaml: 2.8.3 + zod: 4.3.6 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -4661,6 +5461,57 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0): + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.19.1 + '@oxc-resolver/binding-android-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-x64': 11.19.1 + '@oxc-resolver/binding-freebsd-x64': 11.19.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.19.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.19.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-musl': 11.19.1 + '@oxc-resolver/binding-openharmony-arm64': 11.19.1 + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0) + '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4707,17 +5558,20 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.4: {} + pirates@4.0.6: {} possible-typed-array-names@1.0.0: {} - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2): + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.4.2 postcss: 8.4.49 tsx: 4.19.2 + yaml: 2.8.3 postcss-selector-parser@6.1.2: dependencies: @@ -4817,15 +5671,6 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - remark@15.0.1: - dependencies: - '@types/mdast': 4.0.4 - remark-parse: 11.0.0 - remark-stringify: 11.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -4910,6 +5755,8 @@ snapshots: semver@7.6.3: {} + semver@7.7.4: {} + send@1.1.0: dependencies: debug: 4.4.0 @@ -4962,6 +5809,37 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5000,6 +5878,8 @@ snapshots: signal-exit@4.1.0: {} + smol-toml@1.6.1: {} + source-map-js@1.2.1: {} source-map@0.8.0-beta.0: @@ -5063,6 +5943,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -5103,6 +5985,11 @@ snapshots: fdir: 6.4.2(picomatch@4.0.2) picomatch: 4.0.2 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinypool@1.0.2: {} tinyrainbow@2.0.0: {} @@ -5142,7 +6029,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.3.5(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2): + tsup@8.3.5(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.24.2) cac: 6.7.14 @@ -5152,7 +6039,7 @@ snapshots: esbuild: 0.24.2 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2) + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.29.1 source-map: 0.8.0-beta.0 @@ -5238,6 +6125,8 @@ snapshots: typescript@5.7.2: {} + unbash@3.0.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.3 @@ -5304,13 +6193,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.0.5(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2): + vite-node@3.0.5(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) + vite: 6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -5325,7 +6214,7 @@ snapshots: - tsx - yaml - vite@6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2): + vite@6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3): dependencies: esbuild: 0.24.2 postcss: 8.4.49 @@ -5335,11 +6224,12 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 tsx: 4.19.2 + yaml: 2.8.3 - vitest@3.0.5(@types/debug@4.1.13)(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2): + vitest@3.0.5(@types/debug@4.1.13)(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3): dependencies: '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)) + '@vitest/mocker': 3.0.5(vite@6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3)) '@vitest/pretty-format': 3.0.5 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 @@ -5355,8 +6245,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) - vite-node: 3.0.5(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) + vite: 6.0.7(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3) + vite-node: 3.0.5(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 @@ -5393,6 +6283,8 @@ snapshots: transitivePeerDependencies: - supports-color + walk-up-path@4.0.0: {} + webidl-conversions@4.0.2: {} whatwg-url@7.1.0: @@ -5470,8 +6362,12 @@ snapshots: xml-name-validator@4.0.0: {} + yaml@2.8.3: {} + yocto-queue@0.1.0: {} zod@3.24.1: {} + zod@4.3.6: {} + zwitch@2.0.4: {} diff --git a/src/bundle/index.ts b/src/bundle/index.ts index 182d9f2..5ecd6b9 100644 --- a/src/bundle/index.ts +++ b/src/bundle/index.ts @@ -10,7 +10,7 @@ import { addDirectoryToZip, logInfo, logSuccess } from "../utils"; import { bundleBackendPlugin } from "./backend"; import { bundleFrontendPlugin } from "./frontend"; -import { parseGitHubRepoInfo, transformReadmeImages } from "./readme-assets"; +import { transformReadmeImages } from "./readme-assets"; /** * Creates the dist directories. @@ -60,23 +60,8 @@ export async function bundlePackage(options: { throw new Error("README.md is required but not found in project root"); } - // Read repository URL from project's package.json - const packageJsonPath = path.join(cwd, "package.json"); - const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); - const packageJson = JSON.parse(packageJsonContent); - const repoUrl = - typeof packageJson.repository === "string" - ? packageJson.repository - : packageJson.repository?.url; - if (!repoUrl) { - throw new Error("package.json must have a valid 'repository' field"); - } - - // Parse GitHub repo info (default to main branch) - const repoInfo = parseGitHubRepoInfo(repoUrl, "main"); - - // Transform README image links to GitHub raw URLs - const transformedReadme = await transformReadmeImages(readmePath, repoInfo); + // Inline local README images and remove external README URLs. + const transformedReadme = await transformReadmeImages(readmePath); // Write transformed README to package directory const readmeDest = path.join(pluginPackageDir, "README.md"); diff --git a/src/bundle/readme-assets.spec.ts b/src/bundle/readme-assets.spec.ts new file mode 100644 index 0000000..6d8bdde --- /dev/null +++ b/src/bundle/readme-assets.spec.ts @@ -0,0 +1,76 @@ +import fs from "fs/promises"; +import os from "os"; +import path from "path"; + +import sharp from "sharp"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { compressImageToWebpDataUri } from "./readme-assets"; + +const DATA_URI_PREFIX = "data:image/webp;base64,"; +const MAX_INLINED_IMAGE_BYTES = 125 * 1024; + +function decodeWebpDataUri(dataUri: string): Buffer { + expect(dataUri.startsWith(DATA_URI_PREFIX)).toBe(true); + return Buffer.from(dataUri.slice(DATA_URI_PREFIX.length), "base64"); +} + +async function writePng(filePath: string, width = 256, height = 256) { + await sharp({ + create: { + width, + height, + channels: 4, + background: { r: 255, g: 0, b: 0, alpha: 1 }, + }, + }) + .png() + .toFile(filePath); +} + +describe("compressImageToWebpDataUri", () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "readme-assets-")); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it("converts a local image to a WebP data URI under the per-image limit", async () => { + await writePng(path.join(tempDir, "image.png")); + + const dataUri = await compressImageToWebpDataUri(tempDir, "image.png"); + const webp = decodeWebpDataUri(dataUri); + const metadata = await sharp(webp).metadata(); + + expect(webp.byteLength).toBeLessThanOrEqual(MAX_INLINED_IMAGE_BYTES); + expect(metadata.format).toBe("webp"); + expect(metadata.width).toBe(256); + expect(metadata.height).toBe(256); + }); + + it("resolves encoded local paths and ignores query and fragment markers", async () => { + const assetDir = path.join(tempDir, "asset dir"); + await fs.mkdir(assetDir); + await writePng(path.join(assetDir, "test image.png"), 16, 16); + + const dataUri = await compressImageToWebpDataUri( + tempDir, + "asset%20dir/test%20image.png?raw=true#preview", + ); + const metadata = await sharp(decodeWebpDataUri(dataUri)).metadata(); + + expect(metadata.format).toBe("webp"); + expect(metadata.width).toBe(16); + expect(metadata.height).toBe(16); + }); + + it("rejects README image paths that escape the README directory", async () => { + await expect( + compressImageToWebpDataUri(tempDir, "../outside.png"), + ).rejects.toThrow("escapes project root"); + }); +}); diff --git a/src/bundle/readme-assets.ts b/src/bundle/readme-assets.ts index fd0ecf9..a1f5cf0 100644 --- a/src/bundle/readme-assets.ts +++ b/src/bundle/readme-assets.ts @@ -1,68 +1,32 @@ import fs from "fs/promises"; +import path from "path"; import type { Definition, Html, Image, Link } from "mdast"; import remarkParse from "remark-parse"; import remarkStringify from "remark-stringify"; +import sharp from "sharp"; import { unified } from "unified"; import { visit } from "unist-util-visit"; import { logInfo } from "../utils"; -export interface GitHubRepoInfo { - owner: string; - repo: string; - commitHash: string; -} +const MAX_INLINED_IMAGE_BYTES = 100 * 1024; // 133Kb base64 encoded +const MAX_README_BYTES = 2 * 1024 * 1024; // 2 Mb +const WEBP_MIME_TYPE = "image/webp"; +const IMAGE_EXTENSIONS = new Set([ + ".avif", + ".gif", + ".jpeg", + ".jpg", + ".png", + ".svg", + ".tif", + ".tiff", + ".webp", +]); type UrlNode = Image | Link | Definition; -/** - * Parses a GitHub repository URL to extract owner, repo, and branch info. - * Handles common URL formats including git+https:// and .git suffixes. - * @param repoUrl - The repository URL from package.json or config. - * @param branch - The branch to use for raw URLs (defaults to "main"). - * @returns Parsed GitHub repository information. - * @throws Error if the URL is not a valid GitHub repository URL. - */ -export function parseGitHubRepoInfo( - repoUrl: string, - branch = "main", -): GitHubRepoInfo { - // Clean common URL prefixes/suffixes - const cleanedUrl = repoUrl.replace(/^git\+/, "").replace(/\.git$/, ""); - const match = cleanedUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/); - - if (!match) { - throw new Error(`Invalid GitHub repository URL: ${repoUrl}`); - } - - const [, owner, repo] = match; - - // Check if running in GitHub Actions - const isGitHubActions = process.env.GITHUB_ACTIONS === "true"; - - let commitHash: string; - if (isGitHubActions) { - // Use GITHUB_SHA environment variable (automatically set in GitHub Actions) - const githubSha = process.env.GITHUB_SHA; - if (!githubSha) { - throw new Error( - "GITHUB_SHA environment variable is not set in GitHub Actions", - ); - } - commitHash = githubSha; - logInfo(`Using commit hash from GITHUB_SHA: ${commitHash}`); - } else { - // Not in GitHub Actions: use placeholder and log transformation - commitHash = "LOCAL_BUILD"; - logInfo( - `Not running in GitHub Actions, skipping commit hash fetch. README image links will use placeholder URL.`, - ); - } - - return { owner, repo, commitHash }; -} - /** * Checks if a URL is external (http or https). * data: URIs and fragment identifiers are not considered external. @@ -79,97 +43,117 @@ function isExternalUrl(url: string): boolean { } } -/** - * Builds a GitHub raw URL for a local asset path. - */ -function buildRawUrl(originalUrl: string, repoInfo: GitHubRepoInfo): string { - return `https://raw.githubusercontent.com/${repoInfo.owner}/${repoInfo.repo}/${repoInfo.commitHash}/${originalUrl}`; +function getLocalPathname(url: string): string { + const markerIndex = url.search(/[?#]/); + return markerIndex === -1 ? url : url.slice(0, markerIndex); } -/** - * Validates a GitHub raw URL by issuing a HEAD request. - * Only runs in GitHub Actions to avoid local rate limits. - * @returns true if URL is valid or validation was skipped, false if invalid. - */ -async function validateRawUrl(rawUrl: string): Promise { - if (process.env.GITHUB_ACTIONS !== "true") { - return true; +function decodeLocalPathname(url: string): string { + try { + return decodeURIComponent(getLocalPathname(url)); + } catch { + return getLocalPathname(url); } +} - try { - const response = await fetch(rawUrl, { method: "HEAD" }); - if (!response.ok) { - logInfo(`Warning: Asset not found at ${rawUrl}, removing reference`); - return false; +function isLocalImageUrl(url: string): boolean { + const pathname = decodeLocalPathname(url); + return IMAGE_EXTENSIONS.has(path.extname(pathname).toLowerCase()); +} + +function resolveReadmeAssetPath(readmeDir: string, url: string): string { + const assetPath = path.resolve(readmeDir, decodeLocalPathname(url)); + const relativeAssetPath = path.relative(readmeDir, assetPath); + + if ( + relativeAssetPath === "" || + relativeAssetPath.startsWith("..") || + path.isAbsolute(relativeAssetPath) + ) { + throw new Error(`README asset path escapes project root: ${url}`); + } + + return assetPath; +} + +export async function compressImageToWebpDataUri( + readmeDir: string, + originalUrl: string, +): Promise { + const assetPath = resolveReadmeAssetPath(readmeDir, originalUrl); + const input = await fs.readFile(assetPath); + + let bestBuffer: Buffer | undefined; + for (const quality of [80, 70, 60, 50, 40, 30, 20]) { + const output = await sharp(input, { + animated: true, + limitInputPixels: false, + }) + .webp({ quality, effort: 6 }) + .toBuffer(); + + bestBuffer = output; + + if (output.byteLength <= MAX_INLINED_IMAGE_BYTES) { + break; } - return true; - } catch { - logInfo( - `Warning: Failed to validate asset URL ${rawUrl}, removing reference`, + } + + if (bestBuffer === undefined) { + throw new Error(`Unable to process README image: ${originalUrl}`); + } + + if (bestBuffer.byteLength > MAX_INLINED_IMAGE_BYTES) { + throw new Error( + `README image ${originalUrl} is ${bestBuffer.byteLength} bytes after compression, which exceeds the ${MAX_INLINED_IMAGE_BYTES} byte limit`, ); - return false; } + + logInfo( + `Inlined README image as WebP: ${originalUrl} (${bestBuffer.byteLength} bytes)`, + ); + + return `data:${WEBP_MIME_TYPE};base64,${bestBuffer.toString("base64")}`; } /** - * Transforms a URL on a node (image, link, or definition) to a GitHub raw URL - * if it points to a local asset. Removes the URL if it's external or invalid. + * Transforms a URL on a markdown node. External URLs are removed. Local image + * URLs are inlined as compressed WebP data URIs. Other local links are left as-is. */ async function transformNodeUrl( node: UrlNode, - repoInfo: GitHubRepoInfo, + readmeDir: string, kind: string, ): Promise { const originalUrl = node.url; - // Skip empty URLs and fragment-only links (same-document anchors) - if (!originalUrl || originalUrl.startsWith("#")) { - return; - } - - // data: URIs are self-contained and should be kept as-is - if (originalUrl.startsWith("data:")) { + if ( + !originalUrl || + originalUrl.startsWith("#") || + originalUrl.startsWith("data:") + ) { return; } - // External URLs are removed to prevent loading external resources if (isExternalUrl(originalUrl)) { logInfo(`Warning: Skipping external ${kind} URL in README: ${originalUrl}`); node.url = ""; return; } - // Construct GitHub raw URL for the local asset - const rawUrl = buildRawUrl(originalUrl, repoInfo); - - // Validate URL reachability (only in GitHub Actions) - const isValid = await validateRawUrl(rawUrl); - if (!isValid) { - node.url = ""; + if (!isLocalImageUrl(originalUrl)) { return; } - node.url = rawUrl; - - if (repoInfo.commitHash === "LOCAL_BUILD") { - logInfo( - `Would transform README ${kind} link (not in GitHub Actions): ${originalUrl} → ${rawUrl}`, - ); - } else { - logInfo(`Transformed README ${kind} link: ${originalUrl} → ${rawUrl}`); - } + node.url = await compressImageToWebpDataUri(readmeDir, originalUrl); } /** * Transforms URLs found in raw HTML nodes (e.g., , ). - * External URLs are removed (attribute value emptied), local paths are rewritten - * to GitHub raw URLs. + * External URLs are removed, and local image src attributes are inlined as + * compressed WebP data URIs. */ -async function transformHtmlNode( - node: Html, - repoInfo: GitHubRepoInfo, -): Promise { - // Match src="..." and href="..." attributes (single or double quotes) +async function transformHtmlNode(node: Html, readmeDir: string): Promise { const attributeRegex = /\b(src|href)\s*=\s*(["'])([^"']*)\2/gi; const matches: { attr: string; @@ -192,42 +176,31 @@ async function transformHtmlNode( for (const { attr, quote, value, full } of matches) { const kind = attr === "src" ? "image" : "link"; - // Skip empty, fragment-only, and data: URIs if (!value || value.startsWith("#") || value.startsWith("data:")) { continue; } - let replacement: string; + let replacement: string | undefined; if (isExternalUrl(value)) { logInfo( `Warning: Skipping external ${kind} URL in README HTML: ${value}`, ); replacement = `${attr}=${quote}${quote}`; - } else { - const rawUrl = buildRawUrl(value, repoInfo); - const isValid = await validateRawUrl(rawUrl); - if (!isValid) { - replacement = `${attr}=${quote}${quote}`; - } else { - replacement = `${attr}=${quote}${rawUrl}${quote}`; - if (repoInfo.commitHash === "LOCAL_BUILD") { - logInfo( - `Would transform README HTML ${kind} (not in GitHub Actions): ${value} → ${rawUrl}`, - ); - } else { - logInfo(`Transformed README HTML ${kind}: ${value} → ${rawUrl}`); - } - } + } else if (attr === "src" && isLocalImageUrl(value)) { + const dataUri = await compressImageToWebpDataUri(readmeDir, value); + replacement = `${attr}=${quote}${dataUri}${quote}`; } - updatedValue = updatedValue.replace(full, replacement); + if (replacement !== undefined) { + updatedValue = updatedValue.replace(full, replacement); + } } node.value = updatedValue; } /** - * Transforms local asset references in README.md to GitHub raw URLs. + * Transforms local image references in README.md to compressed WebP data URIs. * Uses remark to parse the markdown AST and handles multiple node types: * - `image`: ![alt](path) * - `link`: [text](path) @@ -239,23 +212,17 @@ async function transformHtmlNode( * Fragment-only links (#anchor) are preserved as same-document anchors. * * @param readmePath - Absolute path to the project's README.md. - * @param repoInfo - Parsed GitHub repository information. * @returns Modified README content with transformed URLs. */ export async function transformReadmeImages( readmePath: string, - repoInfo: GitHubRepoInfo, ): Promise { const content = await fs.readFile(readmePath, "utf-8"); + const readmeDir = path.dirname(readmePath); - // Initialize unified processor with remark parse and stringify plugins const processor = unified().use(remarkParse).use(remarkStringify); - - // Parse markdown to AST const ast = processor.parse(content); - // Collect URL-bearing nodes by type. We collect first and process afterwards - // because unist-util-visit does not support async visitors. const imageNodes: Image[] = []; const linkNodes: Link[] = []; const definitionNodes: Definition[] = []; @@ -276,26 +243,29 @@ export async function transformReadmeImages( htmlNodes.push(node); break; default: - // Other node types do not carry URLs we need to transform. break; } }); - // Process each node type (async for URL validation via fetch) for (const node of imageNodes) { - await transformNodeUrl(node, repoInfo, "image"); + await transformNodeUrl(node, readmeDir, "image"); } for (const node of linkNodes) { - await transformNodeUrl(node, repoInfo, "link"); + await transformNodeUrl(node, readmeDir, "link"); } for (const node of definitionNodes) { - await transformNodeUrl(node, repoInfo, "definition"); + await transformNodeUrl(node, readmeDir, "definition"); } for (const node of htmlNodes) { - await transformHtmlNode(node, repoInfo); + await transformHtmlNode(node, readmeDir); } - // Stringify the modified AST back to markdown const modifiedContent = processor.stringify(ast); + if (Buffer.byteLength(modifiedContent, "utf-8") > MAX_README_BYTES) { + throw new Error( + `README.md is ${Buffer.byteLength(modifiedContent, "utf-8")} bytes after inlining images, which exceeds the ${MAX_README_BYTES} byte limit`, + ); + } + return modifiedContent; } diff --git a/src/types.ts b/src/types.ts index fab91cb..6764f81 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,13 +21,13 @@ export type BackendBuildOutput = { export type BuildOutput = FrontendBuildOutput | BackendBuildOutput; -export const backendReferenceConfigSchema = z.strictObject({ id: z.string() }); +const backendReferenceConfigSchema = z.strictObject({ id: z.string() }); const viteSchema: z.ZodType = z.record(z.string(), z.unknown()); -export const assetsConfigSchema = z.array(z.string()).optional(); +const assetsConfigSchema = z.array(z.string()).optional(); -export const frontendPluginConfigSchema = z.strictObject({ +const frontendPluginConfigSchema = z.strictObject({ kind: z.literal("frontend"), id: z.string(), name: z.string().optional(), @@ -37,7 +37,7 @@ export const frontendPluginConfigSchema = z.strictObject({ vite: viteSchema.optional(), }); -export const backendPluginConfigSchema = z.strictObject({ +const backendPluginConfigSchema = z.strictObject({ kind: z.literal("backend"), id: z.string(), name: z.string().optional(), @@ -45,7 +45,7 @@ export const backendPluginConfigSchema = z.strictObject({ assets: assetsConfigSchema, }); -export const workflowPluginConfigSchema = z.strictObject({ +const workflowPluginConfigSchema = z.strictObject({ kind: z.literal("workflow"), id: z.string(), name: z.string(), @@ -53,11 +53,11 @@ export const workflowPluginConfigSchema = z.strictObject({ definition: z.string(), }); -export const linksConfigSchema = z.strictObject({ +const linksConfigSchema = z.strictObject({ sponsor: z.string().url().optional(), }); -export const watchConfigSchema = z.strictObject({ +const watchConfigSchema = z.strictObject({ port: z.number().optional(), }); @@ -83,13 +83,8 @@ export const caidoConfigSchema = z.strictObject({ }); // Type inference -export type BackendReferenceConfig = z.infer< - typeof backendReferenceConfigSchema ->; export type FrontendPluginConfig = z.infer; export type BackendPluginConfig = z.infer; -export type WorkflowPluginConfig = z.infer; -export type WatchConfig = z.infer; export type CaidoConfig = z.infer; export type ConnectedMessage = { diff --git a/vitest.config.ts b/vitest.config.ts index 3de1164..c6b4144 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - include: ['./playgrounds/**/*.spec.ts'], + include: ['./src/**/*.spec.ts', './playgrounds/**/*.spec.ts'], setupFiles: ['./playgrounds/setup.ts'], }, }) From e366c7d5e7c826f7af5a17807243a9a6f91c4af6 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 28 Apr 2026 14:15:13 -0400 Subject: [PATCH 4/7] Removed unused assets --- assets/test.png | 1 - assets/test.txt | 1 - 2 files changed, 2 deletions(-) delete mode 100644 assets/test.png delete mode 100644 assets/test.txt diff --git a/assets/test.png b/assets/test.png deleted file mode 100644 index 407f899..0000000 --- a/assets/test.png +++ /dev/null @@ -1 +0,0 @@ -PNG_PLACEHOLDER_FOR_TESTS diff --git a/assets/test.txt b/assets/test.txt deleted file mode 100644 index 802992c..0000000 --- a/assets/test.txt +++ /dev/null @@ -1 +0,0 @@ -Hello world From 223d1ddea846c5ed3a3d7dd92f71ee5f87d91f1f Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 28 Apr 2026 14:33:35 -0400 Subject: [PATCH 5/7] Fix paths --- .../build-backend/__tests__/build-backend.spec.ts | 2 +- playgrounds/setup.ts | 12 ++++++++---- src/build/frontend.ts | 2 +- src/commands/watch.ts | 7 ++++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/playgrounds/build-backend/__tests__/build-backend.spec.ts b/playgrounds/build-backend/__tests__/build-backend.spec.ts index f41999b..8f1d07b 100644 --- a/playgrounds/build-backend/__tests__/build-backend.spec.ts +++ b/playgrounds/build-backend/__tests__/build-backend.spec.ts @@ -99,7 +99,7 @@ describe("build-backend", () => { expect(readmeContent).not.toContain("assets/test.png"); }); - it("should remove external image URLs (http, https, data)", async () => { + it("should remove external image URLs (http, https)", async () => { const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip"); const readmeContent = await getZipFileContent(zipPath, "README.md"); expect(readmeContent).toBeDefined(); diff --git a/playgrounds/setup.ts b/playgrounds/setup.ts index 35be3f8..f5f0d2c 100644 --- a/playgrounds/setup.ts +++ b/playgrounds/setup.ts @@ -1,4 +1,4 @@ -import { execSync } from "child_process"; +import { execFileSync, execSync } from "child_process"; import path from "path"; import { afterAll, beforeAll, expect } from "vitest"; @@ -22,9 +22,13 @@ beforeAll(({ file }) => { // Run pnpm build in the playground directory console.log(`Building playground in ${playgroundDir}...`); - execSync("node ../../dist/cli.js build", { - cwd: playgroundDir, - }); + execFileSync( + process.execPath, + [path.join("..", "..", "dist", "cli.js"), "build"], + { + cwd: playgroundDir, + }, + ); }); afterAll(() => { diff --git a/src/build/frontend.ts b/src/build/frontend.ts index 267b23b..fcee34f 100644 --- a/src/build/frontend.ts +++ b/src/build/frontend.ts @@ -49,7 +49,7 @@ export async function buildFrontendPlugin( const viteConfig = createViteConfig(cwd, pluginConfig); await build(viteConfig); - const hasCss = existsSync(`${pluginRoot}/dist/index.css`); + const hasCss = existsSync(path.join(pluginRoot, "dist", "index.css")); logInfo("Frontend plugin built successfully"); return { diff --git a/src/commands/watch.ts b/src/commands/watch.ts index f5272ca..16e5551 100644 --- a/src/commands/watch.ts +++ b/src/commands/watch.ts @@ -17,6 +17,11 @@ import { logError, logInfo, slash } from "../utils"; import { build } from "./build"; +function isIgnoredPath(filePath: string) { + const segments = slash(filePath).split("/"); + return segments.includes("dist") || segments.includes("node_modules"); +} + export async function watch(options: { path?: string; config?: string }) { const { path: cwd = process.cwd(), config: configPath } = options; @@ -112,7 +117,7 @@ export async function watch(options: { path?: string; config?: string }) { ]; const watcher = chokidarWatch(filesToWatch, { ignoreInitial: true, - ignored: (f) => f.includes("dist/") || f.includes("node_modules/"), + ignored: isIgnoredPath, }); watcher.on("all", async (event: string, filePath: string) => { From b5f0067fe8543fe1fd8992a3da37877fc566ade5 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 28 Apr 2026 14:47:59 -0400 Subject: [PATCH 6/7] Fix tests --- playgrounds/setup.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/playgrounds/setup.ts b/playgrounds/setup.ts index f5f0d2c..246765e 100644 --- a/playgrounds/setup.ts +++ b/playgrounds/setup.ts @@ -3,11 +3,15 @@ import path from "path"; import { afterAll, beforeAll, expect } from "vitest"; +function hasPathSegment(filePath: string, segment: string) { + return filePath.split(/[\\/]+/).includes(segment); +} + beforeAll(({ file }) => { // Get the test file path from the current test file const testPath = file.filepath; - if (!testPath.split(path.sep).includes("playgrounds")) { + if (!hasPathSegment(testPath, "playgrounds")) { return; } From 08c73f38c3b7722ee114cd69cb7643e7373439dc Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 28 Apr 2026 16:34:48 -0400 Subject: [PATCH 7/7] Rework html element parsing --- package.json | 1 + pnpm-lock.yaml | 16 ++++ src/bundle/readme-assets.spec.ts | 116 +++++++++++++++++++++++++- src/bundle/readme-assets.ts | 137 +++++++++++++++++++++---------- 4 files changed, 222 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index aa13cf0..27d3ff6 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "glob": "11.0.1", "jiti": "2.4.2", "jszip": "3.10.1", + "parse5": "^8.0.1", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "sharp": "^0.34.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6af83b..aff4812 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: jszip: specifier: 3.10.1 version: 3.10.1 + parse5: + specifier: ^8.0.1 + version: 8.0.1 remark-parse: specifier: ^11.0.0 version: 11.0.0 @@ -1527,6 +1530,10 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + es-abstract@1.23.9: resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} engines: {node: '>= 0.4'} @@ -2420,6 +2427,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4318,6 +4328,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + entities@8.0.0: {} + es-abstract@1.23.9: dependencies: array-buffer-byte-length: 1.0.2 @@ -5528,6 +5540,10 @@ snapshots: dependencies: callsites: 3.1.0 + parse5@8.0.1: + dependencies: + entities: 8.0.0 + parseurl@1.3.3: {} path-exists@4.0.0: {} diff --git a/src/bundle/readme-assets.spec.ts b/src/bundle/readme-assets.spec.ts index 6d8bdde..2592350 100644 --- a/src/bundle/readme-assets.spec.ts +++ b/src/bundle/readme-assets.spec.ts @@ -5,10 +5,15 @@ import path from "path"; import sharp from "sharp"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { compressImageToWebpDataUri } from "./readme-assets"; +import { + compressImageToWebpDataUri, + transformReadmeImages, +} from "./readme-assets"; const DATA_URI_PREFIX = "data:image/webp;base64,"; const MAX_INLINED_IMAGE_BYTES = 125 * 1024; +const itUnix = process.platform === "win32" ? it.skip : it; +const itWindows = process.platform === "win32" ? it : it.skip; function decodeWebpDataUri(dataUri: string): Buffer { expect(dataUri.startsWith(DATA_URI_PREFIX)).toBe(true); @@ -68,9 +73,114 @@ describe("compressImageToWebpDataUri", () => { expect(metadata.height).toBe(16); }); - it("rejects README image paths that escape the README directory", async () => { + it("transforms raw HTML URLs with HTML parsing", async () => { + await writePng(path.join(tempDir, "local-image.png"), 16, 16); + await fs.writeFile( + path.join(tempDir, "README.md"), + [ + "Local image", + "External docs", + "Data image", + "Section", + ].join("\n"), + ); + + const content = await transformReadmeImages( + path.join(tempDir, "README.md"), + ); + + expect(content).toContain('src="data:image/webp;base64,'); + expect(content).toContain('href=""'); + expect(content).toContain('src="data:image/png;base64,abc123"'); + expect(content).toContain('href="#section"'); + }); + + itWindows("resolves README image paths with Windows separators", async () => { + const assetDir = path.join(tempDir, "asset-dir"); + await fs.mkdir(assetDir); + await writePng(path.join(assetDir, "test-image.png"), 16, 16); + + const dataUri = await compressImageToWebpDataUri( + tempDir, + "asset-dir\\test-image.png", + ); + const metadata = await sharp(decodeWebpDataUri(dataUri)).metadata(); + + expect(metadata.format).toBe("webp"); + expect(metadata.width).toBe(16); + expect(metadata.height).toBe(16); + }); + + it("rejects README image paths that resolve outside the README directory", async () => { + const outsideDir = await fs.mkdtemp( + path.join(os.tmpdir(), "readme-assets-outside-"), + ); + + try { + await writePng(path.join(outsideDir, "outside.png"), 16, 16); + + await expect( + compressImageToWebpDataUri( + tempDir, + path.relative(tempDir, path.join(outsideDir, "outside.png")), + ), + ).rejects.toThrow("escapes project root"); + } finally { + await fs.rm(outsideDir, { recursive: true, force: true }); + } + }); + + itWindows( + "rejects README image paths with Windows separators that resolve outside the README directory", + async () => { + const outsideDir = await fs.mkdtemp( + path.join(os.tmpdir(), "readme-assets-outside-"), + ); + + try { + await writePng(path.join(outsideDir, "outside.png"), 16, 16); + + await expect( + compressImageToWebpDataUri( + tempDir, + path.win32.relative(tempDir, path.join(outsideDir, "outside.png")), + ), + ).rejects.toThrow("escapes project root"); + } finally { + await fs.rm(outsideDir, { recursive: true, force: true }); + } + }, + ); + + itUnix("rejects README image paths that are symlinks", async () => { + await writePng(path.join(tempDir, "image.png"), 16, 16); + await fs.symlink( + path.join(tempDir, "image.png"), + path.join(tempDir, "image-link.png"), + ); + await expect( - compressImageToWebpDataUri(tempDir, "../outside.png"), + compressImageToWebpDataUri(tempDir, "image-link.png"), ).rejects.toThrow("escapes project root"); }); + + itUnix( + "rejects README image paths that escape through parent symlinks", + async () => { + const outsideDir = await fs.mkdtemp( + path.join(os.tmpdir(), "readme-assets-outside-"), + ); + + try { + await writePng(path.join(outsideDir, "outside.png"), 16, 16); + await fs.symlink(outsideDir, path.join(tempDir, "linked-assets")); + + await expect( + compressImageToWebpDataUri(tempDir, "linked-assets/outside.png"), + ).rejects.toThrow("escapes project root"); + } finally { + await fs.rm(outsideDir, { recursive: true, force: true }); + } + }, + ); }); diff --git a/src/bundle/readme-assets.ts b/src/bundle/readme-assets.ts index a1f5cf0..c4b3a20 100644 --- a/src/bundle/readme-assets.ts +++ b/src/bundle/readme-assets.ts @@ -2,6 +2,7 @@ import fs from "fs/promises"; import path from "path"; import type { Definition, Html, Image, Link } from "mdast"; +import { type DefaultTreeAdapterMap, parseFragment, serialize } from "parse5"; import remarkParse from "remark-parse"; import remarkStringify from "remark-stringify"; import sharp from "sharp"; @@ -26,6 +27,10 @@ const IMAGE_EXTENSIONS = new Set([ ]); type UrlNode = Image | Link | Definition; +type HtmlNode = DefaultTreeAdapterMap["node"]; +type HtmlParentNode = DefaultTreeAdapterMap["parentNode"]; +type HtmlElement = DefaultTreeAdapterMap["element"]; +type HtmlTemplate = DefaultTreeAdapterMap["template"]; /** * Checks if a URL is external (http or https). @@ -61,26 +66,51 @@ function isLocalImageUrl(url: string): boolean { return IMAGE_EXTENSIONS.has(path.extname(pathname).toLowerCase()); } -function resolveReadmeAssetPath(readmeDir: string, url: string): string { +function normalizePathForComparison(filePath: string): string { + const normalizedPath = path.resolve(filePath); + return process.platform === "win32" + ? normalizedPath.toLowerCase() + : normalizedPath; +} + +function isPathInDirectory(directory: string, filePath: string): boolean { + const normalizedDirectory = normalizePathForComparison(directory); + const normalizedFilePath = normalizePathForComparison(filePath); + const directoryWithSeparator = normalizedDirectory.endsWith(path.sep) + ? normalizedDirectory + : `${normalizedDirectory}${path.sep}`; + + return normalizedFilePath.startsWith(directoryWithSeparator); +} + +async function resolveReadmeAssetPath( + readmeDir: string, + url: string, +): Promise { const assetPath = path.resolve(readmeDir, decodeLocalPathname(url)); - const relativeAssetPath = path.relative(readmeDir, assetPath); + const assetStats = await fs.lstat(assetPath); - if ( - relativeAssetPath === "" || - relativeAssetPath.startsWith("..") || - path.isAbsolute(relativeAssetPath) - ) { + if (assetStats.isSymbolicLink()) { + throw new Error(`README asset path escapes project root: ${url}`); + } + + const [realReadmeDir, realAssetPath] = await Promise.all([ + fs.realpath(readmeDir), + fs.realpath(assetPath), + ]); + + if (!isPathInDirectory(realReadmeDir, realAssetPath)) { throw new Error(`README asset path escapes project root: ${url}`); } - return assetPath; + return realAssetPath; } export async function compressImageToWebpDataUri( readmeDir: string, originalUrl: string, ): Promise { - const assetPath = resolveReadmeAssetPath(readmeDir, originalUrl); + const assetPath = await resolveReadmeAssetPath(readmeDir, originalUrl); const input = await fs.readFile(assetPath); let bestBuffer: Buffer | undefined; @@ -148,55 +178,72 @@ async function transformNodeUrl( node.url = await compressImageToWebpDataUri(readmeDir, originalUrl); } +function isHtmlElement(node: HtmlNode): node is HtmlElement { + return "attrs" in node; +} + +function isHtmlParentNode(node: HtmlNode): node is HtmlParentNode { + return "childNodes" in node; +} + +function isHtmlTemplate(node: HtmlNode): node is HtmlTemplate { + return "content" in node; +} + /** * Transforms URLs found in raw HTML nodes (e.g., , ). * External URLs are removed, and local image src attributes are inlined as * compressed WebP data URIs. */ async function transformHtmlNode(node: Html, readmeDir: string): Promise { - const attributeRegex = /\b(src|href)\s*=\s*(["'])([^"']*)\2/gi; - const matches: { - attr: string; - quote: string; - value: string; - full: string; - }[] = []; - - let match: RegExpExecArray | undefined; - while ((match = attributeRegex.exec(node.value) ?? undefined) !== undefined) { - matches.push({ - attr: match[1], - quote: match[2], - value: match[3], - full: match[0], - }); + const fragment = parseFragment(node.value); + + async function transformElementAttributes(element: HtmlElement) { + for (const attr of element.attrs) { + const attrName = attr.name.toLowerCase(); + if (attrName !== "src" && attrName !== "href") { + continue; + } + + const kind = attrName === "src" ? "image" : "link"; + if ( + !attr.value || + attr.value.startsWith("#") || + attr.value.startsWith("data:") + ) { + continue; + } + + if (isExternalUrl(attr.value)) { + logInfo( + `Warning: Skipping external ${kind} URL in README HTML: ${attr.value}`, + ); + attr.value = ""; + } else if (attrName === "src" && isLocalImageUrl(attr.value)) { + attr.value = await compressImageToWebpDataUri(readmeDir, attr.value); + } + } } - let updatedValue = node.value; - for (const { attr, quote, value, full } of matches) { - const kind = attr === "src" ? "image" : "link"; - - if (!value || value.startsWith("#") || value.startsWith("data:")) { - continue; - } + async function walkHtmlNodes(parentNode: HtmlParentNode) { + for (const childNode of parentNode.childNodes) { + if (isHtmlElement(childNode)) { + await transformElementAttributes(childNode); + } - let replacement: string | undefined; - if (isExternalUrl(value)) { - logInfo( - `Warning: Skipping external ${kind} URL in README HTML: ${value}`, - ); - replacement = `${attr}=${quote}${quote}`; - } else if (attr === "src" && isLocalImageUrl(value)) { - const dataUri = await compressImageToWebpDataUri(readmeDir, value); - replacement = `${attr}=${quote}${dataUri}${quote}`; - } + if (isHtmlParentNode(childNode)) { + await walkHtmlNodes(childNode); + } - if (replacement !== undefined) { - updatedValue = updatedValue.replace(full, replacement); + if (isHtmlTemplate(childNode)) { + await walkHtmlNodes(childNode.content); + } } } - node.value = updatedValue; + await walkHtmlNodes(fragment); + + node.value = serialize(fragment); } /**