mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-11-06 12:50:54 -04:00
chore(deps): replace cli-highlight with a highlight.js ANSI renderer (D-5) (#2396)
* chore(deps): replace cli-highlight with a highlight.js ANSI renderer * fix(highlight): key fallback highlight cache by Chalk level Also correct the CONTRIBUTING highlighting note and move the qrcode retention test and note out of this change.
This commit is contained in:
+8
-2
@@ -230,9 +230,15 @@ color detection tests must import in a fresh process.
|
||||
|
||||
### Dependency pins and overrides
|
||||
|
||||
Terminal syntax highlighting uses the exact `highlight.js` pin directly, with
|
||||
the shared Chalk instance for ANSI colors. Keep the lazy loader and terminal
|
||||
palette in `src/utils/cliHighlight.ts` for terminal code and Markdown highlighting
|
||||
only. Structured diff rendering in `src/native-ts/color-diff/index.ts` loads
|
||||
`highlight.js` separately. Run the highlighting and Markdown regression tests
|
||||
when changing this dependency or its ANSI renderer.
|
||||
|
||||
Keep all direct dependencies and devDependencies pinned to exact versions in the
|
||||
root and web manifests. The existing `sharp` range is the temporary exception
|
||||
while its separate upgrade is in progress. Regenerate the relevant Bun lockfile
|
||||
root and web manifests. Regenerate the relevant Bun lockfile
|
||||
when changing a pin.
|
||||
|
||||
The root `overrides` block also controls transitive resolutions:
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"chalk": "6.0.1",
|
||||
"chokidar": "4.0.3",
|
||||
"cli-boxes": "4.0.1",
|
||||
"cli-highlight": "2.1.11",
|
||||
"commander": "12.1.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "9.0.0",
|
||||
@@ -54,6 +53,7 @@
|
||||
"google-auth-library": "11.3.1",
|
||||
"graphology": "0.26.0",
|
||||
"graphology-metrics": "2.4.2",
|
||||
"highlight.js": "10.7.3",
|
||||
"https-proxy-agent": "9.1.0",
|
||||
"ignore": "7.0.13",
|
||||
"indent-string": "5.0.0",
|
||||
@@ -613,8 +613,6 @@
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||
@@ -663,8 +661,6 @@
|
||||
|
||||
"cli-boxes": ["cli-boxes@4.0.1", "", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="],
|
||||
|
||||
"cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
@@ -1019,8 +1015,6 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"needle": ["needle@3.5.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, "bin": { "needle": "bin/needle" } }, "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w=="],
|
||||
@@ -1067,10 +1061,6 @@
|
||||
|
||||
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||
|
||||
"parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="],
|
||||
|
||||
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
@@ -1199,10 +1189,6 @@
|
||||
|
||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||
|
||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
@@ -1503,10 +1489,6 @@
|
||||
|
||||
"bun-types/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
||||
|
||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
@@ -1537,8 +1519,6 @@
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="],
|
||||
|
||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
||||
@@ -1581,16 +1561,6 @@
|
||||
|
||||
"axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="],
|
||||
|
||||
"cli-highlight/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
@@ -1633,18 +1603,6 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"cli-highlight/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cli-highlight/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"cli-highlight/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cli-highlight/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"cli-highlight/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"eslint/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
@@ -1661,12 +1619,6 @@
|
||||
|
||||
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"cli-highlight/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"cli-highlight/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"qrcode/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"qrcode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
+1
-1
@@ -118,7 +118,6 @@
|
||||
"chalk": "6.0.1",
|
||||
"chokidar": "4.0.3",
|
||||
"cli-boxes": "4.0.1",
|
||||
"cli-highlight": "2.1.11",
|
||||
"commander": "12.1.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "9.0.0",
|
||||
@@ -134,6 +133,7 @@
|
||||
"google-auth-library": "11.3.1",
|
||||
"graphology": "0.26.0",
|
||||
"graphology-metrics": "2.4.2",
|
||||
"highlight.js": "10.7.3",
|
||||
"https-proxy-agent": "9.1.0",
|
||||
"ignore": "7.0.13",
|
||||
"indent-string": "5.0.0",
|
||||
|
||||
@@ -141,7 +141,7 @@ export const INTENTIONALLY_BUNDLED: string[] = [
|
||||
'bidi-js',
|
||||
'chalk',
|
||||
'cli-boxes',
|
||||
'cli-highlight',
|
||||
'highlight.js',
|
||||
'commander',
|
||||
'emoji-regex',
|
||||
'env-paths',
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { expect, spyOn, test } from 'bun:test'
|
||||
import chalk from 'chalk'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import React from 'react'
|
||||
import { Ansi, createRoot } from '../../ink.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { getCliHighlightPromise } from '../../utils/cliHighlight.js'
|
||||
import { HighlightedCodeFallback } from './Fallback.js'
|
||||
|
||||
test('remounting with colors disabled does not reuse colored highlights', async () => {
|
||||
await acquireSharedMutationLock('HighlightedCode/Fallback.test.tsx')
|
||||
const previousLevel = chalk.level
|
||||
const stdout = new PassThrough()
|
||||
const stdin = new PassThrough()
|
||||
const code = 'const cachedColorLevel = 42;'
|
||||
const renderAnsi = Ansi.type
|
||||
// Inspect the highlighted string before Ink can strip ANSI colors itself.
|
||||
const ansi = spyOn(Ansi, 'type').mockImplementation(props => renderAnsi(props))
|
||||
const root = await createRoot({
|
||||
stdout: stdout as unknown as NodeJS.WriteStream,
|
||||
stdin: stdin as unknown as NodeJS.ReadStream,
|
||||
patchConsole: false,
|
||||
})
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
try {
|
||||
expect(await getCliHighlightPromise()).not.toBeNull()
|
||||
chalk.level = 1
|
||||
const highlighted = Promise.withResolvers<void>()
|
||||
ansi.mockImplementation(props => {
|
||||
if (props.children.includes('\x1b')) highlighted.resolve()
|
||||
return renderAnsi(props)
|
||||
})
|
||||
root.render(<HighlightedCodeFallback key="colored" code={code} filePath="cache.ts" />)
|
||||
await Promise.race([
|
||||
highlighted.promise,
|
||||
new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error('Highlighting did not complete')), 2500)
|
||||
}),
|
||||
])
|
||||
expect(ansi.mock.calls.at(-1)![0].children).toContain('\x1b')
|
||||
|
||||
chalk.level = 0
|
||||
ansi.mockClear()
|
||||
root.render(<HighlightedCodeFallback key="plain" code={code} filePath="cache.ts" />)
|
||||
const output = ansi.mock.calls.at(-1)![0].children
|
||||
expect(output).toBe(code)
|
||||
expect(output).not.toContain('\x1b')
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
root.unmount()
|
||||
ansi.mockRestore()
|
||||
stdout.end()
|
||||
stdin.end()
|
||||
chalk.level = previousLevel
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import { c as _c } from "react-compiler-runtime";
|
||||
import chalk from 'chalk';
|
||||
import { extname } from 'path';
|
||||
import React, { Suspense, use, useMemo } from 'react';
|
||||
import { Ansi, Text } from '../../ink.js';
|
||||
@@ -15,11 +16,11 @@ type Props = {
|
||||
|
||||
// Module-level highlight cache — hl.highlight() is the hot cost on virtual-
|
||||
// scroll remounts. useMemo doesn't survive unmount→remount. Keyed by hash
|
||||
// of code+language to avoid retaining full source strings (#24180 RSS fix).
|
||||
// of code+language and Chalk level to avoid retaining full source strings (#24180 RSS fix).
|
||||
const HL_CACHE_MAX = 500;
|
||||
const hlCache = new Map<string, string>();
|
||||
function cachedHighlight(hl: NonNullable<Awaited<ReturnType<typeof getCliHighlightPromise>>>, code: string, language: string): string {
|
||||
const key = hashPair(language, code);
|
||||
const key = `${chalk.level}:${hashPair(language, code)}`;
|
||||
const hit = hlCache.get(key);
|
||||
if (hit !== undefined) {
|
||||
hlCache.delete(key);
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ type SpanProps = {
|
||||
* Component that parses ANSI escape codes and renders them using Text components.
|
||||
*
|
||||
* Use this as an escape hatch when you have pre-formatted ANSI strings from
|
||||
* external tools (like cli-highlight) that need to be rendered in Ink.
|
||||
* syntax highlighting that need to be rendered in Ink.
|
||||
*
|
||||
* Memoized to prevent re-renders when parent changes but children string is the same.
|
||||
*/
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Pure TypeScript port of vendor/color-diff-src.
|
||||
*
|
||||
* The Rust version uses syntect+bat for syntax highlighting and the similar
|
||||
* crate for word diffing. This port uses highlight.js (already a dep via
|
||||
* cli-highlight) and the diff npm package's diffArrays.
|
||||
* crate for word diffing. This port uses highlight.js and the diff npm
|
||||
* package's diffArrays.
|
||||
*
|
||||
* API matches vendor/color-diff-src/index.d.ts exactly so callers don't change.
|
||||
*
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
[
|
||||
{
|
||||
"name": "javascript",
|
||||
"language": "javascript",
|
||||
"code": "const value = true;\nconsole.log(\"<&> \\\"quotes\\\"\", 42); // comment\nfunction sum(a) { return /ab+/g.test(a); }",
|
||||
"output": "\u001b[34mconst\u001b[39m value = \u001b[34mtrue\u001b[39m;\n\u001b[36mconsole\u001b[39m.log(\u001b[31m\"<&> \\\"quotes\\\"\"\u001b[39m, \u001b[32m42\u001b[39m); \u001b[32m// comment\u001b[39m\n\u001b[33m\u001b[34mfunction\u001b[39m\u001b[33m sum(a) \u001b[39m{ \u001b[34mreturn\u001b[39m \u001b[31m/ab+/g\u001b[39m.test(a); }"
|
||||
},
|
||||
{
|
||||
"name": "typescript",
|
||||
"language": "ts",
|
||||
"code": "interface Result { name: string; count: number }\nclass Example { value: Result | null = null; }",
|
||||
"output": "\u001b[34minterface\u001b[39m Result { name: \u001b[36mstring\u001b[39m; count: \u001b[36mnumber\u001b[39m }\n\u001b[34m\u001b[34mclass\u001b[39m\u001b[34m Example \u001b[39m{ value: Result | \u001b[34mnull\u001b[39m = \u001b[34mnull\u001b[39m; }"
|
||||
},
|
||||
{
|
||||
"name": "json",
|
||||
"language": "json",
|
||||
"code": "{\"key\": \"value\", \"count\": 42, \"ok\": true}",
|
||||
"output": "{\u001b[36m\"key\"\u001b[39m: \u001b[31m\"value\"\u001b[39m, \u001b[36m\"count\"\u001b[39m: \u001b[32m42\u001b[39m, \u001b[36m\"ok\"\u001b[39m: \u001b[34mtrue\u001b[39m}"
|
||||
},
|
||||
{
|
||||
"name": "markup",
|
||||
"language": "xml",
|
||||
"code": "<section title=\"<&>\">hello & world</section>",
|
||||
"output": "\u001b[90m<\u001b[34msection\u001b[39m\u001b[90m \u001b[36mtitle\u001b[39m\u001b[90m=\u001b[31m\"<&>\"\u001b[39m\u001b[90m>\u001b[39mhello & world\u001b[90m</\u001b[34msection\u001b[39m\u001b[90m>\u001b[39m"
|
||||
},
|
||||
{
|
||||
"name": "sublanguages",
|
||||
"language": "xml",
|
||||
"code": "<script>const n = 2;</script>\n<style>.item { color: red; }</style>",
|
||||
"output": "\u001b[90m<\u001b[34mscript\u001b[39m\u001b[90m>\u001b[39m\u001b[34mconst\u001b[39m n = \u001b[32m2\u001b[39m;\u001b[90m</\u001b[34mscript\u001b[39m\u001b[90m>\u001b[39m\n\u001b[90m<\u001b[34mstyle\u001b[39m\u001b[90m>\u001b[39m.item { color: red; }\u001b[90m</\u001b[34mstyle\u001b[39m\u001b[90m>\u001b[39m"
|
||||
},
|
||||
{
|
||||
"name": "templates",
|
||||
"language": "javascript",
|
||||
"code": "const value = `hello ${Math.max(1, 2)}`;",
|
||||
"output": "\u001b[34mconst\u001b[39m value = \u001b[31m`hello ${\u001b[36mMath\u001b[39m\u001b[31m.max(\u001b[32m1\u001b[39m\u001b[31m, \u001b[32m2\u001b[39m\u001b[31m)}`\u001b[39m;"
|
||||
},
|
||||
{
|
||||
"name": "markdown styles",
|
||||
"language": "markdown",
|
||||
"code": "# Heading\n**bold** *italic* [label](target)\n> quote\n- list\n`code`",
|
||||
"output": "# Heading\n\u001b[1m**bold**\u001b[22m \u001b[3m*italic*\u001b[23m [\u001b[31mlabel\u001b[39m](\u001b[4mtarget\u001b[24m)\n> quote\n- list\n`code`"
|
||||
},
|
||||
{
|
||||
"name": "diff colors",
|
||||
"language": "diff",
|
||||
"code": "@@ -1 +1 @@\n-removed\n+added",
|
||||
"output": "@@ -1 +1 @@\n\u001b[31m-removed\u001b[39m\n\u001b[32m+added\u001b[39m"
|
||||
},
|
||||
{
|
||||
"name": "meta",
|
||||
"language": "cpp",
|
||||
"code": "#include <vector>\n#define COUNT 12\nint main() { return 0; }",
|
||||
"output": "\u001b[90m#\u001b[90minclude\u001b[39m\u001b[90m \u001b[90m<vector>\u001b[39m\u001b[90m\u001b[39m\n\u001b[90m#\u001b[90mdefine\u001b[39m\u001b[90m COUNT 12\u001b[39m\n\u001b[33m\u001b[34mint\u001b[39m\u001b[33m main() \u001b[39m{ \u001b[34mreturn\u001b[39m \u001b[32m0\u001b[39m; }"
|
||||
},
|
||||
{
|
||||
"name": "type style",
|
||||
"language": "haskell",
|
||||
"code": "value :: Int\nvalue = 42",
|
||||
"output": "value :: \u001b[36m\u001b[2mInt\u001b[22m\u001b[39m\nvalue = \u001b[32m42\u001b[39m"
|
||||
},
|
||||
{
|
||||
"name": "plaintext escapes",
|
||||
"language": "plaintext",
|
||||
"code": "<span class=\"hljs-keyword\"> & ' > ' 😀\n",
|
||||
"output": "<span class=\"hljs-keyword\"> & ' > ' 😀\n"
|
||||
},
|
||||
{
|
||||
"name": "line endings",
|
||||
"language": "plaintext",
|
||||
"code": "first\r\nsecond\rthird\u0000last\n",
|
||||
"output": "first\nsecond\nthirdlast\n"
|
||||
},
|
||||
{
|
||||
"name": "empty",
|
||||
"language": "plaintext",
|
||||
"code": "",
|
||||
"output": ""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import chalk from 'chalk'
|
||||
import { stripVTControlCharacters } from 'node:util'
|
||||
import baseline from '../test/fixtures/cliHighlight.baseline.json'
|
||||
import { getCliHighlightPromise, getLanguageName, type CliHighlight } from './cliHighlight.js'
|
||||
import { applyMarkdown } from './markdown.js'
|
||||
|
||||
let previousLevel: typeof chalk.level
|
||||
let highlighter: CliHighlight
|
||||
|
||||
beforeEach(async () => {
|
||||
previousLevel = chalk.level
|
||||
chalk.level = 1
|
||||
const loaded = await getCliHighlightPromise()
|
||||
expect(loaded).not.toBeNull()
|
||||
highlighter = loaded!
|
||||
})
|
||||
|
||||
afterEach(() => { chalk.level = previousLevel })
|
||||
|
||||
// Captured from the previous renderer at color level 1. Compare ANSI bytes,
|
||||
// including nested color restoration, rather than only the visible text.
|
||||
describe('terminal syntax output compatibility', () => {
|
||||
for (const fixture of baseline) {
|
||||
test(fixture.name, () => {
|
||||
expect(highlighter.highlight(fixture.code, { language: fixture.language }))
|
||||
.toBe(fixture.output)
|
||||
})
|
||||
}
|
||||
|
||||
test('disabled colors preserve visible output', () => {
|
||||
chalk.level = 0
|
||||
for (const fixture of baseline) {
|
||||
expect(highlighter.highlight(fixture.code, { language: fixture.language }))
|
||||
.toBe(stripVTControlCharacters(fixture.output))
|
||||
}
|
||||
})
|
||||
|
||||
test('aliases and telemetry use the shared language registry', async () => {
|
||||
expect(getCliHighlightPromise()).toBe(getCliHighlightPromise())
|
||||
expect(highlighter.supportsLanguage('ts')).toBe(true)
|
||||
expect(highlighter.supportsLanguage('unsupported-language')).toBe(false)
|
||||
expect(await getLanguageName('example.ts')).toBe('TypeScript')
|
||||
expect(await getLanguageName('example.unsupported-language')).toBe('unknown')
|
||||
expect(await getLanguageName('README')).toBe('unknown')
|
||||
})
|
||||
|
||||
test('Markdown keeps the syntax palette in dark and light themes', () => {
|
||||
const fixture = baseline.find(item => item.name === 'json')!
|
||||
for (const theme of ['dark', 'light'] as const) {
|
||||
expect(applyMarkdown('```json\n' + fixture.code + '\n```', theme, highlighter))
|
||||
.toBe(fixture.output)
|
||||
}
|
||||
})
|
||||
|
||||
test('unknown and missing fenced languages fall back to plaintext', () => {
|
||||
const code = 'const value = "<&>"; // keep plain'
|
||||
for (const language of ['unsupported-language', '']) {
|
||||
expect(applyMarkdown('```' + language + '\n' + code + '\n```', 'dark', highlighter))
|
||||
.toBe(code)
|
||||
}
|
||||
})
|
||||
})
|
||||
+48
-18
@@ -1,34 +1,64 @@
|
||||
// highlight.js's type defs carry `/// <reference lib="dom" />`. SSETransport,
|
||||
// mcp/client, ssh, dumpPrompts use DOM types (TextDecodeOptions, RequestInfo)
|
||||
// that only typecheck because this file's `typeof import('highlight.js')` pulls
|
||||
// lib.dom in. tsconfig has lib: ["ESNext"] only — fixing the actual DOM-type
|
||||
// deps is a separate sweep; this ref preserves the status quo.
|
||||
/// <reference lib="dom" />
|
||||
|
||||
import chalk from 'chalk'
|
||||
import { extname } from 'path'
|
||||
|
||||
export type CliHighlight = {
|
||||
highlight: typeof import('cli-highlight').highlight
|
||||
supportsLanguage: typeof import('cli-highlight').supportsLanguage
|
||||
highlight: (code: string, options: { language: string }) => string
|
||||
supportsLanguage: (name: string) => boolean
|
||||
}
|
||||
|
||||
// One promise shared by Fallback.tsx, markdown.ts, events.ts, getLanguageName.
|
||||
// The highlight.js import piggybacks: cli-highlight has already pulled it into
|
||||
// the module cache, so the second import() is a cache hit — no extra bytes
|
||||
// faulted in.
|
||||
// Preserve the terminal palette used by the code and Markdown renderers.
|
||||
// Unlisted tokens, including titles, parameters and substitutions, are plain.
|
||||
const tokenStyles = new Map<string, (text: string) => string>([
|
||||
['keyword', chalk.blue], ['literal', chalk.blue], ['class', chalk.blue],
|
||||
['built_in', chalk.cyan], ['type', chalk.cyan.dim], ['attr', chalk.cyan],
|
||||
['number', chalk.green], ['comment', chalk.green], ['doctag', chalk.green],
|
||||
['addition', chalk.green], ['regexp', chalk.red], ['string', chalk.red],
|
||||
['deletion', chalk.red], ['function', chalk.yellow], ['meta', chalk.gray],
|
||||
['tag', chalk.gray], ['name', chalk.blue], ['emphasis', chalk.italic],
|
||||
['strong', chalk.bold], ['link', chalk.underline],
|
||||
])
|
||||
|
||||
const entities: Record<string, string> = {
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', ''': "'",
|
||||
}
|
||||
|
||||
// Only consume highlight.js's generated spans and its five escaped entities.
|
||||
// Source markup is escaped by highlight.js, so it cannot become a span here.
|
||||
// A stack preserves nested styles and unprefixed sublanguage spans without a
|
||||
// general HTML parser. Keep the previous renderer's CR/LF and NUL handling.
|
||||
function terminalHighlight(html: string): string {
|
||||
const stack: { token?: string; text: string }[] = [{ text: '' }]
|
||||
for (const part of html.replace(/\r\n?/g, '\n').replace(/\0/g, '')
|
||||
.split(/(<span class="[^"]*">|<\/span>)/g)) {
|
||||
if (part.startsWith('<span class="')) {
|
||||
stack.push({ token: /hljs-(\w+)/.exec(part)?.[1], text: '' })
|
||||
} else if (part === '</span>') {
|
||||
const frame = stack.pop()!
|
||||
const style = frame.token ? tokenStyles.get(frame.token) : undefined
|
||||
stack[stack.length - 1]!.text += style ? style(frame.text) : frame.text
|
||||
} else {
|
||||
stack[stack.length - 1]!.text += part.replace(
|
||||
/&(?:amp|lt|gt|quot|#x27);/g, entity => entities[entity]!,
|
||||
)
|
||||
}
|
||||
}
|
||||
return stack[0]!.text
|
||||
}
|
||||
|
||||
// Share one lazy registry load across code, Markdown and telemetry consumers.
|
||||
let cliHighlightPromise: Promise<CliHighlight | null> | undefined
|
||||
|
||||
let loadedGetLanguage: typeof import('highlight.js').getLanguage | undefined
|
||||
|
||||
async function loadCliHighlight(): Promise<CliHighlight | null> {
|
||||
try {
|
||||
const cliHighlight = await import('cli-highlight')
|
||||
// cache hit — cli-highlight already loaded highlight.js
|
||||
const highlightJs = await import('highlight.js')
|
||||
loadedGetLanguage = highlightJs.getLanguage
|
||||
return {
|
||||
highlight: cliHighlight.highlight,
|
||||
supportsLanguage: cliHighlight.supportsLanguage,
|
||||
highlight: (code, options) => terminalHighlight(
|
||||
highlightJs.highlight(code, options).value,
|
||||
),
|
||||
supportsLanguage: name => Boolean(highlightJs.getLanguage(name)),
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
@@ -41,7 +71,7 @@ export function getCliHighlightPromise(): Promise<CliHighlight | null> {
|
||||
}
|
||||
|
||||
/**
|
||||
* eg. "foo/bar.ts" → "TypeScript". Awaits the shared cli-highlight load,
|
||||
* Resolves a filename's extension to a language name after the shared load,
|
||||
* then reads highlight.js's language registry. All callers are telemetry
|
||||
* (OTel counter attributes, permission-dialog unary events) — none block
|
||||
* on this, they fire-and-forget or the consumer already handles Promise<string>.
|
||||
|
||||
Reference in New Issue
Block a user