mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 14:00:28 +00:00
Compare commits
855 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a878dd451 | |||
| c08d05ade5 | |||
| b9453a92f8 | |||
| 2c4d40ad97 | |||
| 688515efcf | |||
| 931cbe717e | |||
| 3ef1802e7e | |||
| 8a0e0df84e | |||
| a9041a309a | |||
| 625c108196 | |||
| 8e87856501 | |||
| 9b822abbb6 | |||
| 81dba6455b | |||
| 1faa57b812 | |||
| 40150f0a6c | |||
| bf29570bd5 | |||
| e8e979b6c0 | |||
| 1f250d1fa8 | |||
| 46b3a18b25 | |||
| 2ff77e15b5 | |||
| d7c867aa63 | |||
| bcd1c41aa1 | |||
| 77058e3f17 | |||
| 8ad7465801 | |||
| 1b7ac0e41b | |||
| 5f88f4e680 | |||
| 7009bb9311 | |||
| e460ec381e | |||
| 0a82628298 | |||
| 18ff11b68b | |||
| 3bf55cef10 | |||
| e041e49f11 | |||
| c1b5be05eb | |||
| 7543eb83ad | |||
| 523b7cd4b6 | |||
| af698a49f5 | |||
| db67585ee2 | |||
| a566153d98 | |||
| 530f98a9c4 | |||
| 39391259c8 | |||
| 35be3835f3 | |||
| 4452fd793c | |||
| 90566879d2 | |||
| 40a879ad5f | |||
| cc441c99c6 | |||
| 64e143a15b | |||
| 8c10d92f77 | |||
| 0a22eb5340 | |||
| d9fc4608e3 | |||
| ded2f560e3 | |||
| a9c5d1b546 | |||
| 0ff21b7ae2 | |||
| 94a496c561 | |||
| 148a43ac01 | |||
| 5da854f09c | |||
| b772f74c2b | |||
| e0bd04f641 | |||
| 8d7c3edda8 | |||
| 80951e9889 | |||
| 185dd8337e | |||
| da7fb13d83 | |||
| 49c993f181 | |||
| 90b54682e5 | |||
| 1dccf8f06c | |||
| 810244e8a9 | |||
| 0dd2f4fbcd | |||
| 8a700222e6 | |||
| b13fd91afc | |||
| 8a6baaba6a | |||
| 48fba7cb49 | |||
| 6928a9b00a | |||
| 7bab574c63 | |||
| 75bdff3ff3 | |||
| 56c25587db | |||
| cf4efc7213 | |||
| 1ef71609e7 | |||
| 195c9e6b84 | |||
| 09f39f8300 | |||
| 9002f6598a | |||
| 01c9b059c6 | |||
| 6d02b8ab72 | |||
| 177ef8582a | |||
| 78c589ffba | |||
| e4b214e6bd | |||
| 64ce9e180d | |||
| c64d3a50e8 | |||
| fd070ce9ca | |||
| 048c4e4c70 | |||
| fb01dc9ecb | |||
| d33e570bc7 | |||
| 87fd234d55 | |||
| 0f32eb17d1 | |||
| 3e29680792 | |||
| b3db1c58c8 | |||
| 0f994016f4 | |||
| 419094b79d | |||
| 95d23775ce | |||
| aa98859560 | |||
| ea19801cfa | |||
| 13059b843c | |||
| 3db14ad950 | |||
| 5fac9e37ee | |||
| 1424c566a0 | |||
| 65f99eafe5 | |||
| 7594a334d8 | |||
| 827e4f9856 | |||
| a9a1a609c5 | |||
| 61405a3036 | |||
| 634a24ff03 | |||
| 2ccd43ee5e | |||
| cd506e3f79 | |||
| a6644074cb | |||
| 4be043d96e | |||
| 673f4b6076 | |||
| d0a63bab91 | |||
| ca9a1dd396 | |||
| 996d7ec021 | |||
| f9ad9b5356 | |||
| 332089eca9 | |||
| 6ac4cf4b76 | |||
| 5c74eef602 | |||
| 1ee1853995 | |||
| fa9c9b38b7 | |||
| b4d9c6b95e | |||
| 588964279e | |||
| a80d41200a | |||
| ccb9d7bda4 | |||
| 14562e9e5b | |||
| 580d186ddd | |||
| 038008c853 | |||
| 8c890522ed | |||
| e9558a6df7 | |||
| b5f3c1416a | |||
| b9d383bed3 | |||
| b6e7f2d2db | |||
| 94ef1769ec | |||
| ae9367dafe | |||
| cef18701be | |||
| bf674a47d9 | |||
| 1eb763f9c5 | |||
| db176ba0ea | |||
| 03bbacf319 | |||
| 484cc11627 | |||
| 0dee4155a2 | |||
| 4d7c01587e | |||
| 6083a88845 | |||
| bf6a7af72b | |||
| 703e654a37 | |||
| 1b571aeaa9 | |||
| c41cf9c12a | |||
| 3d376845d7 | |||
| bcd4d4e980 | |||
| 88b1919a45 | |||
| cf94386c16 | |||
| e77bf32d51 | |||
| a69a307de2 | |||
| 39156ee9c3 | |||
| 47ac3e2c81 | |||
| e73b182925 | |||
| a9a52fee72 | |||
| cb92754575 | |||
| 49d35aa026 | |||
| 98b2df42ef | |||
| 6cf4278f14 | |||
| 86c44299eb | |||
| 89c4a0f808 | |||
| 8e9a49de1a | |||
| 4e9071d1b5 | |||
| 597fe34d3b | |||
| e14609cb3e | |||
| 57f3077006 | |||
| e24632c80b | |||
| ad3399b42e | |||
| f3408ed495 | |||
| 8676ad19e2 | |||
| 3a31100efe | |||
| 3fedaaa08c | |||
| bcea23e886 | |||
| fdea8f1c47 | |||
| 139b897521 | |||
| 37c2dd8819 | |||
| 62ca6d3ce5 | |||
| 6102413d6b | |||
| eaef259a80 | |||
| 578a3a829b | |||
| c923c654bd | |||
| 65fd3cc101 | |||
| bb30db105d | |||
| 6146f8e183 | |||
| 7e55cd5e4c | |||
| cb2dfa76af | |||
| 77efd449ae | |||
| af259fbe86 | |||
| 2b31b0213c | |||
| f60afe1b25 | |||
| b7c3b847e8 | |||
| 2fa65e6949 | |||
| c76b4fdccc | |||
| 9f651339f0 | |||
| 5ce3b1e578 | |||
| d172684b88 | |||
| 937c8b8eb2 | |||
| 36dbdb48cc | |||
| 2dc76a2c3c | |||
| 0edc378c57 | |||
| 2eb313194a | |||
| 882dfcda83 | |||
| 3ff88ba22f | |||
| 51bd768042 | |||
| daadd91eba | |||
| f4469b2df3 | |||
| 91b325ca08 | |||
| 1616028426 | |||
| 6c15d8314a | |||
| 6c967c9bcc | |||
| c44d822fe1 | |||
| af3cda1c53 | |||
| a0ad403a91 | |||
| 0ca49523a2 | |||
| 76550415ac | |||
| 509389c7e5 | |||
| 88b94b5d36 | |||
| 251630c49f | |||
| acf2719d06 | |||
| ddba8031ec | |||
| 72a1596ae8 | |||
| 5b36c59061 | |||
| 77c717c251 | |||
| 60310b4c83 | |||
| 6eabaecbad | |||
| 423bbd06d9 | |||
| 36f19c8018 | |||
| 2ecbdd8f62 | |||
| c727cc8b9d | |||
| 2c729d461e | |||
| d83ea4ce14 | |||
| bac8608e78 | |||
| 7a1236a3a5 | |||
| ed80bd9e7a | |||
| 7036b67307 | |||
| 6bd522b322 | |||
| 74fc3f74d7 | |||
| 3e384c679e | |||
| f1fa1061db | |||
| 4f754a73a3 | |||
| 62d6d28888 | |||
| fd73a9912b | |||
| 595435ac4a | |||
| aa9d41a1b6 | |||
| 30089b404c | |||
| 92b68560e1 | |||
| f158064817 | |||
| 2885a137f8 | |||
| 0ffb74a6f8 | |||
| b116a0a2a4 | |||
| b9bb535d52 | |||
| 257ca09a5e | |||
| f405fffa06 | |||
| 16a54b6bb9 | |||
| 526a928420 | |||
| c201abab5f | |||
| 3697dfefed | |||
| 287203d197 | |||
| 0b38f6c93b | |||
| 8ce64f4862 | |||
| 127ef54b0f | |||
| 67f31aaa7a | |||
| 51fef84d39 | |||
| 0c577a00de | |||
| 8b6dc6dbbd | |||
| 2e7ba40a25 | |||
| b70b8fd2ca | |||
| a9ba996953 | |||
| 47c8ccba6f | |||
| e9842feeef | |||
| f4b4fe15c1 | |||
| 06b9fd542a | |||
| 76077f9f6d | |||
| 7b65f7c0d5 | |||
| 37a9453917 | |||
| d994875bea | |||
| a9b5521f3a | |||
| 91fadb08ed | |||
| b55605f25e | |||
| 6ba5e54d19 | |||
| 56aeb19e49 | |||
| ebab8188f6 | |||
| 8064c95267 | |||
| 2cef2e02c8 | |||
| 8bfced2e0d | |||
| 61ee4e3637 | |||
| 927aa89213 | |||
| f29d1e2f16 | |||
| 3a9efb8aed | |||
| f71ab8426c | |||
| a3d1558f99 | |||
| ca53869e2d | |||
| 33598e665b | |||
| 9045ba5248 | |||
| 7061fc2223 | |||
| 0c9e3952b7 | |||
| 4b78672135 | |||
| 10289e4881 | |||
| 8b6a6df1d6 | |||
| 83df84ac28 | |||
| 5e5075a137 | |||
| 90c1f68e19 | |||
| 0e10124626 | |||
| 01e1c2ae6a | |||
| 65558cfbb1 | |||
| 10d894ce19 | |||
| d1a2958212 | |||
| 894b6adc4c | |||
| b3b819f8e5 | |||
| 2b98af0cc0 | |||
| 54a56ed772 | |||
| 071627d4f0 | |||
| 7595706b92 | |||
| be080248b1 | |||
| d3d51507e5 | |||
| a17ab65a6d | |||
| e3aca9e4f6 | |||
| 045f603849 | |||
| b2854500a1 | |||
| ead88591c2 | |||
| aa1fc246e2 | |||
| e792cffeb3 | |||
| 119e3bc369 | |||
| 823e7d5c4b | |||
| f5f659e7fc | |||
| 723e579958 | |||
| f27901c453 | |||
| 789c2fab60 | |||
| 3fa727c80b | |||
| 88c38fa505 | |||
| bda9fe2d76 | |||
| 2bdade3b57 | |||
| 8d2a95b168 | |||
| 68c842bd4c | |||
| ecb4292551 | |||
| 7fd381c0b1 | |||
| 709964fe3e | |||
| afc7a38bef | |||
| bbcfd72a6d | |||
| 8242f18b8a | |||
| 4f7fba08df | |||
| f17e292ba4 | |||
| 6e1074ec4d | |||
| c5e5857623 | |||
| 11df1fd395 | |||
| 569d4046be | |||
| 50653abd08 | |||
| 84082225af | |||
| 7d882cef5f | |||
| 126d4b78b6 | |||
| e8224bad6c | |||
| 6063437513 | |||
| ab17ff47b0 | |||
| d97b404c7a | |||
| 3c141f0231 | |||
| e3d391e01b | |||
| 62cfec874a | |||
| ccd21e4943 | |||
| 696d9d0d01 | |||
| b77c4fc5b9 | |||
| 7e1ea48080 | |||
| b5d6f2e908 | |||
| 832b44f0ac | |||
| 4909044831 | |||
| 4ed84c209a | |||
| ca01888fe2 | |||
| 6855c05781 | |||
| ab867eb69c | |||
| deabce98ae | |||
| f884477a65 | |||
| c56b569a05 | |||
| 1ba412ff6c | |||
| 7c2823cd26 | |||
| 16bfafe32b | |||
| da648cabd8 | |||
| a36e437580 | |||
| d5dd5e7ba1 | |||
| c4a2604b4d | |||
| 7f4e644733 | |||
| 5d3f78b112 | |||
| 60bcf43304 | |||
| b40465e7b7 | |||
| 3151864f81 | |||
| 80803c92f2 | |||
| 31e77d4e17 | |||
| 8e5a2aa6e6 | |||
| fe6f6420e3 | |||
| aff6285eb9 | |||
| 5f132bc979 | |||
| 4297de3571 | |||
| 6455f2e3ff | |||
| dee52fa80a | |||
| 4ac29817a2 | |||
| f09ca60930 | |||
| a7c0f7251b | |||
| efc6117665 | |||
| 6c613d0df3 | |||
| 45d32b5084 | |||
| 0f62a629b4 | |||
| 7ab63516bd | |||
| 9dd2db6678 | |||
| 4787b7e910 | |||
| be02e33ada | |||
| 3ce7e529f7 | |||
| 0f162a45a7 | |||
| 02ba0ef601 | |||
| bdc6cd9aba | |||
| b3875a19d7 | |||
| 884d900e38 | |||
| f68755695e | |||
| 9ffde72ced | |||
| 9e3629b12b | |||
| 48f81c80f0 | |||
| 0baa8e5022 | |||
| 59b9663ce3 | |||
| 9be66e3e8a | |||
| 9a0f35c26d | |||
| f1826807a7 | |||
| 887c3f8eac | |||
| d666212674 | |||
| 8cb9131d83 | |||
| 98b36616cb | |||
| eceec6f641 | |||
| 9f4d93502e | |||
| 8ab419a47e | |||
| 56ce5dfd74 | |||
| 370b731972 | |||
| b7fb9e57ca | |||
| 5b9f5fa991 | |||
| bf1f11794e | |||
| 8621c7f571 | |||
| 4a6f91a19d | |||
| 38a1c85fee | |||
| cb71511cda | |||
| afa476c51f | |||
| 1e9ed753a1 | |||
| 22da7961d1 | |||
| e074283bbf | |||
| afa868e0e1 | |||
| c5bdde4401 | |||
| 5efe13e622 | |||
| 41ef3435bc | |||
| 157d5fb9e7 | |||
| ec28a7b686 | |||
| b3e9ff9839 | |||
| aa2292036c | |||
| f7c4538b16 | |||
| b9b444c24e | |||
| 6f1b795d80 | |||
| 0803db07ef | |||
| a9adbc6370 | |||
| f49b0af3fe | |||
| 3d8e89cbe9 | |||
| f0a54bcb27 | |||
| c348583a18 | |||
| a4b238dc0c | |||
| 529afa3fac | |||
| 577bf26d1e | |||
| 4808f36718 | |||
| a100c5a6c8 | |||
| 4962662fb1 | |||
| f7c251e2d2 | |||
| 40524d4d4a | |||
| 1a7ef78fe2 | |||
| 5f3f4da2d2 | |||
| 93eea0b6d9 | |||
| 9dfa9c2db0 | |||
| d9ff25d28e | |||
| ae30a576e8 | |||
| 2e495cb28a | |||
| f517035e38 | |||
| 83bd0d74cd | |||
| 3cfefa40fd | |||
| 59ffb95417 | |||
| 2ebb18dc5a | |||
| 1deb897de1 | |||
| 11d9b80e77 | |||
| e73d9afd86 | |||
| 729501c977 | |||
| 414866cbec | |||
| 9d5e397574 | |||
| 92c9b8b056 | |||
| a73caf69ff | |||
| f72f250b16 | |||
| bd330ac133 | |||
| e31a8f4275 | |||
| cc38ec2098 | |||
| eab11e2d6f | |||
| 152490cd67 | |||
| 833b07b284 | |||
| 5f668605c2 | |||
| 9c762065f2 | |||
| 4af7df8902 | |||
| a3f854e1a8 | |||
| 4c54820055 | |||
| 22f574da9c | |||
| 426aabe39a | |||
| 64de8d2cf6 | |||
| b4b471fab6 | |||
| 17b32994f6 | |||
| bcc0ef8666 | |||
| 6ede6ee7c6 | |||
| 7a11861d36 | |||
| 10754fbaf4 | |||
| 8f32f4fb1e | |||
| 717ad0b300 | |||
| a3eed52f24 | |||
| 27a34b7ee2 | |||
| bf3ad28c7f | |||
| 70c2e90e4b | |||
| d0a378cdb6 | |||
| 81f8ddcf27 | |||
| 9d6457f814 | |||
| 07c02ebcd2 | |||
| 4d3172d13f | |||
| 0aa34cc9a8 | |||
| 551d30cfa4 | |||
| 844a0a8ea7 | |||
| eb8ba66a66 | |||
| d738e118a0 | |||
| c16d08f5d5 | |||
| 61ec9b25b9 | |||
| b57561d395 | |||
| e00d1c5bbd | |||
| 9063b56c89 | |||
| 020d3668d7 | |||
| 4d30cbf058 | |||
| d3f6c38f05 | |||
| 6f1db32682 | |||
| 4ad6b2c067 | |||
| 07449443a0 | |||
| b09cc64fec | |||
| 1eed2004f6 | |||
| 91539b8187 | |||
| 16cfde87ee | |||
| 58b4a978ff | |||
| e34be0e89b | |||
| e60c754343 | |||
| 48ca21e471 | |||
| 3ba9e46f2b | |||
| 58f0cc8287 | |||
| 29e2b22b63 | |||
| 3c7cb09c1b | |||
| 0f1cd6745d | |||
| e90f57b138 | |||
| 7bd5939247 | |||
| 89de966e7a | |||
| 4e2d86318e | |||
| f647af2003 | |||
| 96e24a1f84 | |||
| 2a0f37f4b3 | |||
| 17d935dd4c | |||
| df07fbe242 | |||
| 1e53376151 | |||
| f294818775 | |||
| eed386a133 | |||
| e172b25ba8 | |||
| f7f6f9d06d | |||
| 9e01676fe9 | |||
| f6cf62a686 | |||
| a5ef44ab2a | |||
| b53cb1d2b3 | |||
| 7eeff73080 | |||
| bf63045319 | |||
| 71c2e7f8b4 | |||
| 985c1b45b4 | |||
| 35efc9d6d8 | |||
| 6b1fd43179 | |||
| 7517e987a5 | |||
| 21851d3340 | |||
| ca682362dd | |||
| 56fb754e22 | |||
| 99d788ed9e | |||
| ebf3b2beb7 | |||
| 7b7231602f | |||
| adfb118285 | |||
| a7f58ab829 | |||
| 360b525b4b | |||
| 695e99f466 | |||
| aa87699915 | |||
| d2eb3d9f5a | |||
| 77bc339fca | |||
| e7fb77116e | |||
| 050ca23ac4 | |||
| 9063f97dba | |||
| ed9e8fe93a | |||
| f478ddc2ed | |||
| bd11c0d21b | |||
| 381728970e | |||
| 4690e2087a | |||
| 224a44daf8 | |||
| 4766092ff9 | |||
| 3e4e0b0bf0 | |||
| 6b985051f3 | |||
| 2d08a6e6de | |||
| d539409fc7 | |||
| c30ed46021 | |||
| fc98a4a881 | |||
| 6278a1844e | |||
| 86e7aa6c5e | |||
| 8761cee1bb | |||
| 44feb3b140 | |||
| 6b43773752 | |||
| 62438f447e | |||
| 87d68ea59e | |||
| 5f9c80e4f2 | |||
| 9c69cc413f | |||
| b77623b9f3 | |||
| 1430ceebea | |||
| 3fbd6f0d50 | |||
| 8f0b97906b | |||
| d460dc4b39 | |||
| 27b9f7817b | |||
| 9e062b10d3 | |||
| 74f89316d9 | |||
| 9e1d7b296b | |||
| b49caafa9c | |||
| f372eec393 | |||
| 65ae941687 | |||
| 75b8efa155 | |||
| 34b0c67c91 | |||
| e0ace99707 | |||
| 3553db0989 | |||
| 5c14948cf3 | |||
| 8e4e2a2333 | |||
| 0f4c53cfbd | |||
| c156e946b0 | |||
| 461e15fd00 | |||
| 2989f79708 | |||
| 344842f5ce | |||
| c51076efc4 | |||
| 38e34dcce3 | |||
| 04a4175678 | |||
| 3e40b4db50 | |||
| 854e612e5d | |||
| 27a461be9d | |||
| 3d83dadb12 | |||
| 8dc5c18b9f | |||
| e4756c23a7 | |||
| 79ad7bcb59 | |||
| 5b0917a13a | |||
| 6d6f4ef6dd | |||
| fa69a64a2b | |||
| d1a85d1f91 | |||
| 05bf9d7a59 | |||
| 2e0b3370c8 | |||
| 10995e09ca | |||
| e7cc3ff61f | |||
| 59d259074b | |||
| 96c4ae4f2d | |||
| 64f3596df2 | |||
| 27d6c3af39 | |||
| 1de4eb7973 | |||
| df1966df25 | |||
| 1001aaf94e | |||
| 155e448c8f | |||
| ca2a94ee1a | |||
| a512c90bda | |||
| 9cbefa89c7 | |||
| c8bba7bfe1 | |||
| 4e04db8a1b | |||
| b45feafeff | |||
| 3876f7081e | |||
| b4a021a192 | |||
| 1d92dc1350 | |||
| e4ba35fdae | |||
| 35dcaca971 | |||
| 2aa4f2e0b8 | |||
| 6d56df7c10 | |||
| 4534ea83c3 | |||
| 9022d87ec7 | |||
| a49781e200 | |||
| 065a5e6e55 | |||
| e27986b17a | |||
| 8606f222c9 | |||
| 2c3066e3ae | |||
| 55c0b03a41 | |||
| 3e548e4186 | |||
| 3aea302648 | |||
| 1d1f0a7c6a | |||
| 590729cce5 | |||
| b554d7d13f | |||
| 9568b8a7c8 | |||
| 683bb8f634 | |||
| 7d6a31627f | |||
| c31e5648f5 | |||
| 0c01832684 | |||
| 6b1c9fef86 | |||
| cc976995cc | |||
| ebbb476589 | |||
| 89655cb428 | |||
| 3676d64bdc | |||
| f682df1e01 | |||
| 6c9893945d | |||
| a2fb627ae1 | |||
| f9f80117f0 | |||
| 63bf4dbb16 | |||
| b3fa503aff | |||
| 0ae7cfd790 | |||
| 8fdc5c89dc | |||
| 758b34d46b | |||
| 1b0613e482 | |||
| 3164f1a99d | |||
| 6b588af07d | |||
| d436b04ff4 | |||
| d159788d0b | |||
| 3355960d95 | |||
| c32d4494f2 | |||
| 1dcf4df51f | |||
| 3c0e49d56b | |||
| c35d0e8954 | |||
| c0d73d2f76 | |||
| 454175ab86 | |||
| 6feb8fcd6a | |||
| b8d3cb7ee5 | |||
| 8ce73229c3 | |||
| 8eae6e970b | |||
| a48b886dfd | |||
| 17de0838c5 | |||
| baf3496980 | |||
| ae33c09aa7 | |||
| 7ff69c3b72 | |||
| 6142d6bf80 | |||
| 75543b140f | |||
| 511f1cfbe5 | |||
| 46fa9d954c | |||
| 5e1ed5358e | |||
| 295f03db49 | |||
| bdde3929fc | |||
| 4f4210d24a | |||
| c038ec8f81 | |||
| 75b52eac28 | |||
| ad4d07dc06 | |||
| 75086740f8 | |||
| eea8f1e00f | |||
| 4f9abfb6f2 | |||
| 252afceab0 | |||
| bef191660a | |||
| 145625142d | |||
| f70a2aa310 | |||
| 56e46ce8ab | |||
| 754cc11e0b | |||
| 3551d147e9 | |||
| 162cfe4163 | |||
| 6815dd197e | |||
| d74f8a7098 | |||
| 1e53f46521 | |||
| 1a6216df4f | |||
| a629900324 | |||
| a4c33997fe | |||
| badf3a92f6 | |||
| e0fb9ac3ca | |||
| f3e40410c4 | |||
| 8e99871004 | |||
| 1e4e54e752 | |||
| 50c1be3447 | |||
| 319a1e536c | |||
| 50cb0c6e15 | |||
| ceb32b1a2e | |||
| f8c54b4d34 | |||
| c06ad70f8a | |||
| 0ef5dc8538 | |||
| 28adc9b5f1 | |||
| 912d1693f5 | |||
| c23d4ef331 | |||
| c21693fe50 | |||
| c06b3dd60d | |||
| f173e014db | |||
| 1b458c436e | |||
| b42b0b3910 | |||
| 2fc21272ff | |||
| 379d486488 | |||
| cba583a31c | |||
| 7a504b7cc5 | |||
| 8ec5fd0986 | |||
| b9ad997fcb | |||
| 529f940a45 | |||
| 8a3c15911f | |||
| d7948e34b2 | |||
| 9f42211993 | |||
| 65e62307bf | |||
| 469b332da2 | |||
| e434442f1e | |||
| 9344431bfc | |||
| 622ecbec1d | |||
| fa1382b289 | |||
| b8388e5a2a | |||
| 2bce12798f | |||
| f89e889d18 | |||
| 2b8a2d24b6 | |||
| b85eccdff5 | |||
| d924e655c0 | |||
| b932e4e2e9 | |||
| ca4a3e8c9a | |||
| b498521196 | |||
| 8a8cf2025d | |||
| ed7d47d9b8 | |||
| b5494cc04b | |||
| 229f2ba9d5 | |||
| 6da36558f6 | |||
| ca71c1fad6 | |||
| 262afa357b | |||
| 7375d3c597 | |||
| 3548fa6dff | |||
| cb73040e0f | |||
| 872d36b64f | |||
| e1ff17e655 | |||
| f3010bd28a | |||
| 8ff1e3b619 | |||
| d7596bf454 | |||
| aea5eb10ae | |||
| 1165a81cf5 | |||
| 61d1a0192e | |||
| f9a2977621 | |||
| 24f601d594 | |||
| 7b34537bbe | |||
| 145f385fdb | |||
| 360aa1c6d6 | |||
| ccc0975240 | |||
| 791135040a | |||
| 12c9d765ff | |||
| 45207288a1 | |||
| 177afa5828 | |||
| d51b79edbb | |||
| a2720ac49d | |||
| 5f6853d812 | |||
| 17dd382dc3 | |||
| 98e42cfe79 | |||
| c890e01004 | |||
| 659be551bb | |||
| 78a632b7fe | |||
| 4927e964b7 | |||
| 262a9b198b | |||
| 4f3588240c | |||
| abbae3ec4d | |||
| 1b8609ac60 | |||
| 193d591e9f | |||
| 1edc5d622a | |||
| 61b8507996 | |||
| 6f368b5cf3 | |||
| 9201e2cf52 | |||
| 2e0d706d43 | |||
| 7fefe4919c | |||
| 729921f338 | |||
| 1839ea2678 | |||
| f08dd29364 | |||
| ca2dfa3065 | |||
| 10ded437f0 | |||
| d1fdc13441 | |||
| a8fca0e033 |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "🚀 Dev server",
|
||||
"runtimeExecutable": "sh",
|
||||
"runtimeArgs": ["-c", "bun install && bun dev"],
|
||||
"port": 3000,
|
||||
"autoPort": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Stop editors from looking for .editorconfig files in parent directories.
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
@@ -1,3 +0,0 @@
|
||||
# Changes to the API data cache functions can invalidate all existing data cache
|
||||
# causing a massive amount of revalidation, impacting our API.
|
||||
packages/gitbook/src/lib/data/api.ts @SamyPesse
|
||||
@@ -54,10 +54,9 @@ After forking this repository, you'll want to [create a branch](https://docs.git
|
||||
#### 3. Install dependencies and run the project locally
|
||||
|
||||
##### Prerequisites:
|
||||
- Node.js (Version: >=20.6)
|
||||
- Use `nvm` for easy Node management
|
||||
- [Bun](https://bun.sh/) (Version: >=1.2.15)
|
||||
- We use a text-based lockfile which isn't supported below 1.2.15
|
||||
|
||||
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
|
||||
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
|
||||
|
||||
##### Setup steps:
|
||||
|
||||
@@ -77,7 +76,10 @@ bun dev
|
||||
```
|
||||
|
||||
Additional development commands:
|
||||
- `bun format`: Format the code using Biome
|
||||
- `bun lint`: Lint the code using Oxlint
|
||||
- `bun lint:fix`: Automatically fix lint issues using Oxlint
|
||||
- `bun format`: Format the code using Oxfmt
|
||||
- `bun format:check`: Check formatting without changing files
|
||||
- `bun typecheck`: Run TypeScript type checking
|
||||
- `bun unit`: Run unit tests
|
||||
- `bun e2e`: Run end-to-end tests
|
||||
|
||||
@@ -1,83 +1,83 @@
|
||||
name: Gradual Deploy to Cloudflare
|
||||
description: Use gradual deployment to deploy to Cloudflare. This action will upload the middleware and server versions to Cloudflare and kept them bound together
|
||||
inputs:
|
||||
apiToken:
|
||||
description: 'Cloudflare API token'
|
||||
required: true
|
||||
accountId:
|
||||
description: 'Cloudflare account ID'
|
||||
required: true
|
||||
environment:
|
||||
description: 'Cloudflare environment to deploy to (staging, production, preview)'
|
||||
required: true
|
||||
middlewareVersionId:
|
||||
description: 'Middleware version ID to deploy'
|
||||
required: true
|
||||
serverVersionId:
|
||||
description: 'Server version ID to deploy'
|
||||
required: true
|
||||
apiToken:
|
||||
description: 'Cloudflare API token'
|
||||
required: true
|
||||
accountId:
|
||||
description: 'Cloudflare account ID'
|
||||
required: true
|
||||
environment:
|
||||
description: 'Cloudflare environment to deploy to (staging, production, preview)'
|
||||
required: true
|
||||
middlewareVersionId:
|
||||
description: 'Middleware version ID to deploy'
|
||||
required: true
|
||||
serverVersionId:
|
||||
description: 'Server version ID to deploy'
|
||||
required: true
|
||||
outputs:
|
||||
deployment-url:
|
||||
description: "Deployment URL"
|
||||
value: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||
deployment-url:
|
||||
description: 'Deployment URL'
|
||||
value: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- id: wrangler_status
|
||||
name: Check wrangler deployment status
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
using: 'composite'
|
||||
steps:
|
||||
- id: wrangler_status
|
||||
name: Check wrangler deployment status
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# This step is used to get the version ID that is currently deployed to Cloudflare.
|
||||
- id: extract_current_version
|
||||
name: Extract current version
|
||||
shell: bash
|
||||
run: |
|
||||
version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
# This step is used to get the version ID that is currently deployed to Cloudflare.
|
||||
- id: extract_current_version
|
||||
name: Extract current version
|
||||
shell: bash
|
||||
run: |
|
||||
version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: deploy_server
|
||||
name: Deploy server to Cloudflare at 0%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
- id: deploy_server
|
||||
name: Deploy server to Cloudflare at 0%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||
- id: deploy_middleware
|
||||
name: Deploy middleware to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||
- id: deploy_middleware
|
||||
name: Deploy middleware to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Deploy server to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
- name: Deploy server to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
name: 'Deploy cloudflare'
|
||||
description: 'Deploy GitBook to Cloudflare'
|
||||
inputs:
|
||||
opItem:
|
||||
description: '1Password item to load secrets from'
|
||||
required: true
|
||||
opServiceAccount:
|
||||
description: '1Password service account token'
|
||||
required: true
|
||||
apiToken:
|
||||
description: 'Cloudflare API token'
|
||||
required: true
|
||||
accountId:
|
||||
description: 'Cloudflare account ID'
|
||||
required: true
|
||||
environment:
|
||||
description: 'Cloudflare environment to deploy to (staging, production, preview)'
|
||||
required: true
|
||||
deploy:
|
||||
description: 'Deploy as main version for all traffic instead of uploading versions'
|
||||
required: true
|
||||
commitTag:
|
||||
description: 'Commit branch to associate with the deployment'
|
||||
required: true
|
||||
commitMessage:
|
||||
description: 'Commit message to associate with the deployment'
|
||||
required: true
|
||||
opItem:
|
||||
description: '1Password item to load secrets from'
|
||||
required: true
|
||||
opServiceAccount:
|
||||
description: '1Password service account token'
|
||||
required: true
|
||||
apiToken:
|
||||
description: 'Cloudflare API token'
|
||||
required: true
|
||||
accountId:
|
||||
description: 'Cloudflare account ID'
|
||||
required: true
|
||||
environment:
|
||||
description: 'Cloudflare environment to deploy to (staging, production, preview)'
|
||||
required: true
|
||||
deploy:
|
||||
description: 'Deploy as main version for all traffic instead of uploading versions'
|
||||
required: true
|
||||
commitTag:
|
||||
description: 'Commit branch to associate with the deployment'
|
||||
required: true
|
||||
commitMessage:
|
||||
description: 'Commit message to associate with the deployment'
|
||||
required: true
|
||||
outputs:
|
||||
deployment-url:
|
||||
description: "Deployment URL"
|
||||
value: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
deployment-url:
|
||||
description: 'Deployment URL'
|
||||
value: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@@ -42,26 +42,33 @@ runs:
|
||||
- name: Load secret
|
||||
uses: 1password/load-secrets-action@v2
|
||||
env:
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
|
||||
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
|
||||
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
|
||||
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ inputs.opItem }}/NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
|
||||
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
|
||||
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
|
||||
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
|
||||
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
|
||||
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
|
||||
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
|
||||
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
|
||||
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
|
||||
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
|
||||
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
|
||||
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ inputs.opItem }}/NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
|
||||
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
|
||||
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
|
||||
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
|
||||
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
|
||||
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
|
||||
GITBOOK_OAUTH_SERVER_URL: ${{ inputs.opItem }}/GITBOOK_OAUTH_SERVER_URL
|
||||
GITBOOK_SITE_OAUTH_SIGNING_SECRET: ${{ inputs.opItem }}/GITBOOK_SITE_OAUTH_SIGNING_SECRET
|
||||
GITBOOK_PREVIEW_BASE_URL: ${{ inputs.opItem }}/GITBOOK_PREVIEW_BASE_URL
|
||||
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
|
||||
GITBOOK_INTEGRATIONS_CONTENT_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_CONTENT_HOST
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
|
||||
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL_2
|
||||
GITBOOK_IMAGE_RESIZE_SALT: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SALT
|
||||
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
- name: Build worker
|
||||
run: bun run turbo build:cloudflare
|
||||
env:
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
shell: bash
|
||||
|
||||
- name: Upload the DO worker
|
||||
@@ -70,7 +77,7 @@ runs:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
|
||||
|
||||
@@ -81,7 +88,7 @@ runs:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
@@ -104,10 +111,10 @@ runs:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
|
||||
- name: Extract middleware version worker ID
|
||||
shell: bash
|
||||
id: extract_middleware_version_id
|
||||
@@ -128,11 +135,10 @@ runs:
|
||||
middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
|
||||
deploy: ${{ inputs.deploy }}
|
||||
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
|
||||
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
|
||||
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
name: 'Deploy vercel'
|
||||
description: 'Deploy GitBook to Vercel'
|
||||
inputs:
|
||||
vercelOrg:
|
||||
description: 'Vercel organization'
|
||||
required: true
|
||||
vercelProject:
|
||||
description: 'Vercel project'
|
||||
required: true
|
||||
vercelToken:
|
||||
description: 'Vercel token'
|
||||
required: true
|
||||
opItem:
|
||||
description: '1Password item to load secrets from'
|
||||
required: true
|
||||
opServiceAccount:
|
||||
description: '1Password service account token'
|
||||
required: true
|
||||
environment:
|
||||
description: 'Environment to deploy to'
|
||||
required: true
|
||||
vercelOrg:
|
||||
description: 'Vercel organization'
|
||||
required: true
|
||||
vercelProject:
|
||||
description: 'Vercel project'
|
||||
required: true
|
||||
vercelToken:
|
||||
description: 'Vercel token'
|
||||
required: true
|
||||
opItem:
|
||||
description: '1Password item to load secrets from'
|
||||
required: true
|
||||
opServiceAccount:
|
||||
description: '1Password service account token'
|
||||
required: true
|
||||
environment:
|
||||
description: 'Environment to deploy to'
|
||||
required: true
|
||||
headSha:
|
||||
description: 'Git ref to deploy, used for the deploymentId'
|
||||
required: false
|
||||
outputs:
|
||||
deployment-url:
|
||||
description: "Deployment URL"
|
||||
value: ${{ steps.deploy.outputs.deployment-url }}
|
||||
deployment-url:
|
||||
description: 'Deployment URL'
|
||||
value: ${{ steps.deploy.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@@ -37,33 +40,51 @@ runs:
|
||||
run: bun run vercel pull --yes --environment=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
|
||||
shell: bash
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
- name: Load secret
|
||||
uses: 1password/load-secrets-action@v2
|
||||
env:
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
|
||||
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
|
||||
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
|
||||
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
|
||||
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
|
||||
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
|
||||
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
|
||||
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
|
||||
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
|
||||
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
|
||||
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
|
||||
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
|
||||
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
|
||||
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
|
||||
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
|
||||
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
|
||||
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
|
||||
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
|
||||
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
|
||||
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
|
||||
GITBOOK_OAUTH_SERVER_URL: ${{ inputs.opItem }}/GITBOOK_OAUTH_SERVER_URL
|
||||
GITBOOK_SITE_OAUTH_SIGNING_SECRET: ${{ inputs.opItem }}/GITBOOK_SITE_OAUTH_SIGNING_SECRET
|
||||
GITBOOK_PREVIEW_BASE_URL: ${{ inputs.opItem }}/GITBOOK_PREVIEW_BASE_URL
|
||||
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
|
||||
GITBOOK_INTEGRATIONS_CONTENT_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_CONTENT_HOST
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
|
||||
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL_2
|
||||
GITBOOK_IMAGE_RESIZE_SALT: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SALT
|
||||
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
- name: Inject build env vars
|
||||
if: ${{ inputs.environment == 'preview' }}
|
||||
shell: bash
|
||||
run: |
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
echo "resolved HEAD_SHA: $HEAD_SHA"
|
||||
echo "GITBOOK_HEAD_SHA=$HEAD_SHA" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "GITBOOK_RUNTIME=vercel" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "GITBOOK_BLOCK_SEARCH_INDEXATION=true" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE=true" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
|
||||
cat .vercel/.env.${{ inputs.environment }}.local
|
||||
- name: Build Project Artifacts
|
||||
run: bun run vercel build --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
|
||||
shell: bash
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
GITBOOK_RUNTIME: vercel
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
GITBOOK_RUNTIME: vercel
|
||||
GITBOOK_HEAD_SHA: ${{ inputs.headSha }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
id: deploy
|
||||
shell: bash
|
||||
@@ -71,10 +92,9 @@ runs:
|
||||
DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }})
|
||||
echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
- changeset-release/main
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -21,6 +20,20 @@ jobs:
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun format:check
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: Lint
|
||||
timeout-minutes: 6
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun lint
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test
|
||||
@@ -35,13 +48,10 @@ jobs:
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun unit
|
||||
build-oss:
|
||||
# CI to check that the repository builds correctly on a machine without the credentials
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build (Open Source)
|
||||
name: Build
|
||||
timeout-minutes: 6
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ''
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CSS browser compatibility
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: css-browser-compatibility-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check newly added CSS declarations
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 6
|
||||
steps:
|
||||
# pull_request_target checks out the trusted base branch by default. Never use the PR ref here.
|
||||
- name: Checkout trusted checker
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Check CSS browser compatibility
|
||||
working-directory: packages/gitbook
|
||||
run: bun run check:css-browser-compatibility
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
@@ -1,23 +1,35 @@
|
||||
name: Preview
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
approval:
|
||||
# Ensure that external contributors' PRs are manually approved before deployment
|
||||
name: Approve Preview Deployment
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.fork }}
|
||||
environment:
|
||||
name: preview-approval
|
||||
steps:
|
||||
- name: Approval gate
|
||||
run: echo "Preview deployment approved."
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (preview)
|
||||
runs-on: ubuntu-latest
|
||||
needs: approval
|
||||
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
|
||||
environment:
|
||||
name: 2v-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
name: 2v-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Deploy to Vercel
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-vercel
|
||||
@@ -28,17 +40,22 @@ jobs:
|
||||
vercelToken: ${{ secrets.VERCEL_TOKEN }}
|
||||
opItem: op://gitbook-open/2v-preview
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
headSha: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
deploy-v2-cloudflare:
|
||||
name: Deploy v2 to Cloudflare Worker (preview)
|
||||
runs-on: ubuntu-latest
|
||||
needs: approval
|
||||
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
|
||||
environment:
|
||||
name: 2c-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
name: 2c-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Deploy to Cloudflare
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-cloudflare
|
||||
@@ -55,17 +72,17 @@ jobs:
|
||||
id: extract-worker-id
|
||||
if: ${{ !steps.deploy.outputs.deployment-url }}
|
||||
run: |
|
||||
if [[ "${{ steps.deploy.outputs.command-output }}" =~ Worker\ Version\ ID:\ ([0-9a-f]{8})-([0-9a-f-]+) ]]; then
|
||||
WORKER_ID_FIRST_PART="${BASH_REMATCH[1]}"
|
||||
echo "worker-url=https://${WORKER_ID_FIRST_PART}-gitbook-open-v2-preview.gitbook.workers.dev/" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [[ "${{ steps.deploy.outputs.command-output }}" =~ Worker\ Version\ ID:\ ([0-9a-f]{8})-([0-9a-f-]+) ]]; then
|
||||
WORKER_ID_FIRST_PART="${BASH_REMATCH[1]}"
|
||||
echo "worker-url=https://${WORKER_ID_FIRST_PART}-gitbook-open-v2-preview.gitbook.workers.dev/" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}"
|
||||
comment-deployments:
|
||||
runs-on: ubuntu-latest
|
||||
name: Comment Deployments (preview)
|
||||
if: always() && !startsWith(github.ref, 'refs/heads/main')
|
||||
if: always() && github.event_name != 'push'
|
||||
needs:
|
||||
- deploy-v2-vercel
|
||||
- deploy-v2-cloudflare
|
||||
@@ -102,10 +119,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2
|
||||
needs: deploy-v2-vercel
|
||||
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
@@ -118,15 +138,18 @@ jobs:
|
||||
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
ARGOS_BUILD_NAME: 'v2-vercel'
|
||||
ARGOS_BUILD_NAME: v2-vercel
|
||||
visual-testing-v2-cloudflare:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
@@ -139,15 +162,18 @@ jobs:
|
||||
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
ARGOS_BUILD_NAME: 'v2-cloudflare'
|
||||
ARGOS_BUILD_NAME: v2-cloudflare
|
||||
visual-testing-customers-v2:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2
|
||||
needs: deploy-v2-vercel
|
||||
timeout-minutes: 15
|
||||
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
@@ -160,15 +186,66 @@ jobs:
|
||||
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
ARGOS_BUILD_NAME: 'customers-v2'
|
||||
ARGOS_BUILD_NAME: customers-v2-vercel
|
||||
|
||||
# Style recalc depends on the client bundle, not the host, so this runs here only and
|
||||
# not in the Cloudflare job. Kept out of `e2e-customers`, which is a visual suite.
|
||||
- name: Run style invalidation tests
|
||||
if: always()
|
||||
working-directory: packages/gitbook
|
||||
run: bun run e2e-style-perf
|
||||
env:
|
||||
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
|
||||
|
||||
# Runs on failure too: a blown style budget is exactly when the numbers are worth seeing.
|
||||
- name: Build style invalidation report
|
||||
if: always() && github.event_name != 'push'
|
||||
id: style-perf
|
||||
working-directory: packages/gitbook
|
||||
run: |
|
||||
rows=$(cat test-results/style-perf.md 2>/dev/null || true)
|
||||
if [ -z "$rows" ]; then exit 0; fi
|
||||
{
|
||||
echo 'body<<STYLE_PERF_EOF'
|
||||
echo '### Style invalidation on a large API reference'
|
||||
echo
|
||||
echo 'Elements restyled by opening one popup on [the Snyk API reference](https://docs.snyk.io/snyk-api/reference/apps). A share near or above 100% means the insertion restyles the whole document.'
|
||||
echo
|
||||
echo '| interaction | restyled | page | share | budget | |'
|
||||
echo '| --- | ---: | ---: | ---: | ---: | :-: |'
|
||||
echo "$rows"
|
||||
echo 'STYLE_PERF_EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find style invalidation comment
|
||||
if: always() && github.event_name != 'push' && steps.style-perf.outputs.body != ''
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc-style-perf
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: 'Style invalidation on a large API reference'
|
||||
|
||||
- name: Create or update style invalidation comment
|
||||
if: always() && github.event_name != 'push' && steps.style-perf.outputs.body != ''
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.fc-style-perf.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: ${{ steps.style-perf.outputs.body }}
|
||||
edit-mode: replace
|
||||
visual-testing-customers-v2-cloudflare:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
timeout-minutes: 15
|
||||
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
@@ -181,22 +258,43 @@ jobs:
|
||||
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
ARGOS_BUILD_NAME: 'customers-v2'
|
||||
pagespeed-testing-v2:
|
||||
ARGOS_BUILD_NAME: customers-v2-cloudflare
|
||||
browserless-testing-v2-vercel:
|
||||
runs-on: ubuntu-latest
|
||||
name: PageSpeed Testing v1
|
||||
name: Browserless Testing v2 (Vercel)
|
||||
needs: deploy-v2-vercel
|
||||
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Run pagespeed tests
|
||||
run: bun ./packages/gitbook/tests/pagespeed-testing.ts
|
||||
- name: Run tests
|
||||
run: cd ./packages/gitbook && bun e2e-browserless
|
||||
env:
|
||||
BASE_URL: ${{needs.deploy-v2-vercel.outputs.deployment-url}}
|
||||
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
|
||||
# browserless-testing-v2-cloudflare:
|
||||
# runs-on: ubuntu-latest
|
||||
# name: Browserless Testing v2 (Cloudflare)
|
||||
# needs: deploy-v2-cloudflare
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# - name: Setup Bun
|
||||
# uses: ./.github/composite/setup-bun
|
||||
# - name: Install dependencies
|
||||
# run: bun install --frozen-lockfile
|
||||
# env:
|
||||
# PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
# - name: Run tests
|
||||
# run: cd ./packages/gitbook && bun e2e-browserless
|
||||
# env:
|
||||
# BASE_URL: ${{needs.deploy-v2-cloudflare.outputs.deployment-url}}
|
||||
# SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
|
||||
|
||||
@@ -3,13 +3,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (production)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
environment:
|
||||
name: 2v-production
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
@@ -21,16 +19,16 @@ jobs:
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-vercel
|
||||
with:
|
||||
environment: production
|
||||
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
|
||||
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
vercelToken: ${{ secrets.VERCEL_TOKEN }}
|
||||
opItem: op://gitbook-open/2v-production
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
environment: production
|
||||
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
|
||||
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
vercelToken: ${{ secrets.VERCEL_TOKEN }}
|
||||
opItem: op://gitbook-open/2v-production
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
deploy-v2-cloudflare:
|
||||
name: Deploy v2 to Cloudflare Worker (production)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
environment:
|
||||
name: 2c-production
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
@@ -52,4 +50,4 @@ jobs:
|
||||
commitMessage: ${{ github.sha }}
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
|
||||
@@ -3,13 +3,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (staging)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
environment:
|
||||
name: 2v-staging
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
@@ -21,16 +19,16 @@ jobs:
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-vercel
|
||||
with:
|
||||
environment: staging
|
||||
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
|
||||
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
vercelToken: ${{ secrets.VERCEL_TOKEN }}
|
||||
opItem: op://gitbook-open/2v-staging
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
environment: staging
|
||||
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
|
||||
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
vercelToken: ${{ secrets.VERCEL_TOKEN }}
|
||||
opItem: op://gitbook-open/2v-staging
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
deploy-v2-cloudflare:
|
||||
name: Deploy v2 to Cloudflare Worker (staging)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
environment:
|
||||
name: 2c-staging
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
@@ -52,4 +50,4 @@ jobs:
|
||||
commitMessage: ${{ github.sha }}
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
|
||||
@@ -5,38 +5,48 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
env:
|
||||
# The repo is managed by Bun, but publishing uses npm for provenance.
|
||||
# Keep Corepack from rejecting packageManager: bun@... when npm runs.
|
||||
COREPACK_ENABLE_PROJECT_SPEC: 0
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
||||
fetch-depth: 0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Update npm
|
||||
run: |
|
||||
corepack enable npm
|
||||
corepack install -g npm@latest
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
- name: Create Release Pull Request
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
publish: npm run release
|
||||
version: npm run changeset-version
|
||||
publish: bun run publish-all-packages
|
||||
version: bun run changeset-version
|
||||
env:
|
||||
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
|
||||
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
|
||||
@@ -25,3 +25,7 @@ yarn-error.log*
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
.npmrc
|
||||
|
||||
# Bun pack artifacts
|
||||
packages/*/*.tgz
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"sortImports": {
|
||||
"customGroups": [
|
||||
{
|
||||
"groupName": "workspace",
|
||||
"elementNamePattern": ["@gitbook/**"]
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
["value-builtin", "value-external", "type-builtin", "type-external"],
|
||||
{ "newlinesBetween": true },
|
||||
"workspace",
|
||||
{ "newlinesBetween": true },
|
||||
[
|
||||
"value-internal",
|
||||
"type-internal",
|
||||
"value-parent",
|
||||
"type-parent",
|
||||
"value-sibling",
|
||||
"type-sibling",
|
||||
"value-index",
|
||||
"type-index"
|
||||
],
|
||||
"type-import",
|
||||
"unknown"
|
||||
]
|
||||
},
|
||||
"sortPackageJson": false,
|
||||
"sortTailwindcss": {
|
||||
"config": "./packages/gitbook/tailwind.config.ts",
|
||||
"attributes": ["class", "className", "style"],
|
||||
"functions": ["clsx", "tw"]
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/public/**",
|
||||
"**/.next/**",
|
||||
"**/.open-next/**",
|
||||
"**/.turbo/**",
|
||||
"**/.vercel/**",
|
||||
"**/.cache/**",
|
||||
"**/.wrangler/**",
|
||||
"**/*.log",
|
||||
"**/*.MD",
|
||||
"**/*.md",
|
||||
"**/*.mdx",
|
||||
"**/*.html",
|
||||
"**/*.css",
|
||||
"packages/embed/standalone/**",
|
||||
"packages/openapi-parser/src/fixtures/**",
|
||||
"packages/emoji-codepoints/index.ts",
|
||||
"packages/icons/src/data/*.json",
|
||||
"packages/gitbook/worker-configuration.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"categories": {
|
||||
"correctness": "warn",
|
||||
"suspicious": "warn",
|
||||
"perf": "warn"
|
||||
},
|
||||
"plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "jsx-a11y", "vitest"],
|
||||
"env": {
|
||||
"builtin": true,
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"shared-node-browser": true,
|
||||
"worker": true
|
||||
},
|
||||
"globals": {
|
||||
"Bun": "readonly",
|
||||
"GitBookIntegrationEvent": "readonly",
|
||||
"React": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
"eslint/no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": ["assert", "error", "warn"]
|
||||
}
|
||||
],
|
||||
"eslint/no-control-regex": "warn",
|
||||
"eslint/no-prototype-builtins": "warn",
|
||||
"eslint/no-cond-assign": "warn",
|
||||
"eslint/no-undef": "error",
|
||||
"eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true,
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"react/button-has-type": "warn",
|
||||
"react/exhaustive-deps": "warn",
|
||||
"react/jsx-key": "warn",
|
||||
"react/jsx-no-useless-fragment": "warn",
|
||||
"react/no-array-index-key": "warn",
|
||||
"react/void-dom-elements-no-children": "warn",
|
||||
"react/rules-of-hooks": "error",
|
||||
"jsx-a11y/alt-text": "warn",
|
||||
"jsx-a11y/anchor-is-valid": "warn",
|
||||
"jsx-a11y/click-events-have-key-events": "warn",
|
||||
"jsx-a11y/iframe-has-title": "warn",
|
||||
"jsx-a11y/interactive-supports-focus": "warn",
|
||||
"jsx-a11y/label-has-associated-control": "warn",
|
||||
"jsx-a11y/no-noninteractive-tabindex": "warn",
|
||||
"jsx-a11y/role-has-required-aria-props": "warn",
|
||||
"jsx-a11y/tabindex-no-positive": "warn",
|
||||
"typescript/array-type": "error",
|
||||
"typescript/no-explicit-any": "warn",
|
||||
"typescript/no-non-null-assertion": "warn",
|
||||
"typescript/no-confusing-void-expression": "warn",
|
||||
"typescript/only-throw-error": "error",
|
||||
"vitest/no-focused-tests": "error",
|
||||
"no-delete-var": "warn",
|
||||
"no-debugger": "error",
|
||||
"no-throw-literal": "error",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/public/**",
|
||||
"**/.next/**",
|
||||
"**/.open-next/**",
|
||||
"**/.turbo/**",
|
||||
"**/.vercel/**",
|
||||
"**/.cache/**",
|
||||
"**/.wrangler/**",
|
||||
"packages/embed/standalone/**",
|
||||
"packages/openapi-parser/src/fixtures/**",
|
||||
"packages/emoji-codepoints/index.ts",
|
||||
"packages/icons/src/data/*.json",
|
||||
"packages/gitbook/worker-configuration.d.ts",
|
||||
"**/*.css"
|
||||
]
|
||||
}
|
||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"recommendations": ["biomejs.biome"]
|
||||
"recommendations": ["oxc.oxc-vscode"]
|
||||
}
|
||||
|
||||
Vendored
+3
-5
@@ -8,14 +8,12 @@
|
||||
["style \\=([^;]*);", "\\`([^\\`]*)\\`"]
|
||||
],
|
||||
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit"
|
||||
"source.fixAll.oxc": "always"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# AGENTS Instructions
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
bun install # Install dependencies
|
||||
bun dev # Start dev server (all packages)
|
||||
bun run build # Build all packages
|
||||
bun run lint # Lint with Oxlint
|
||||
bun run format # Format with Oxfmt (run after every change)
|
||||
bun run typecheck # Type-check all packages
|
||||
bun run unit # Run unit tests
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
The dev server proxies published GitBook sites locally. After `bun dev`, access any published site at:
|
||||
|
||||
```
|
||||
http://localhost:3000/url/<published-gitbook-url>
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `http://localhost:3000/url/gitbook.com/docs`
|
||||
- `http://localhost:3000/url/open-source.gitbook.io/midjourney`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
packages/
|
||||
gitbook/ # Main Next.js app
|
||||
src/
|
||||
app/ # Next.js App Router (sites/)
|
||||
components/ # React components
|
||||
lib/ # Server utilities, data fetching
|
||||
intl/ # Internationalization (translations/)
|
||||
openapi-parser/ # OpenAPI 3.0/3.1/Swagger parser
|
||||
react-openapi/ # OpenAPI rendering components
|
||||
react-contentkit/ # ContentKit component rendering
|
||||
embed/ # Embeddable GitBook components
|
||||
shared/ # Shared utilities
|
||||
icons/ # Icon assets
|
||||
fonts/ # Font assets
|
||||
colors/ # Color tokens
|
||||
expr/ # GitBook expression evaluator
|
||||
cache-do/ # Cloudflare DO cache
|
||||
cache-tags/ # Cache tag utilities
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
bun run unit # Unit tests via bun test (not vitest)
|
||||
bun run e2e # Playwright e2e tests (requires built app)
|
||||
```
|
||||
|
||||
Run a specific test file:
|
||||
```bash
|
||||
cd packages/gitbook && bun test src/lib/cache.test.ts
|
||||
```
|
||||
|
||||
## Changesets
|
||||
|
||||
After committing code changes, create a changeset for the affected package:
|
||||
|
||||
```md
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Provide a short description of the change.
|
||||
```
|
||||
|
||||
Save as `.changeset/<name>.md`, then commit it separately with message: `changeset`
|
||||
|
||||
## Formatting
|
||||
|
||||
Linting uses Oxlint and formatting uses Oxfmt. Always run `bun run format` before committing.
|
||||
|
||||
## Comments
|
||||
|
||||
Comment to explain *why*, not *what* — the code already shows what it does. Keep comments short, ideally a single line. Avoid multi-line block comments that narrate mechanics a reader can follow from the code; they add noise and go stale. Reserve longer comments for genuinely non-obvious rationale: a subtle invariant, or a workaround and the reason it exists.
|
||||
@@ -1,3 +1,7 @@
|
||||
<p align="center">
|
||||
<img src="./assets/gitbook_icon_dark.svg" alt="GitBook" width="48" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">GitBook</h1>
|
||||
|
||||
<p align="center">
|
||||
@@ -35,7 +39,7 @@ To run a local version of this project, please follow these simple steps.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (Version: >=20.6)
|
||||
- Node.js (Version: >= 22.3)
|
||||
- Use nvm for easy Node management
|
||||
- [Bun](https://bun.sh/) (Version: >=1.2.15)
|
||||
- We use a text-based lockfile which isn't supported below 1.2.15
|
||||
@@ -77,23 +81,7 @@ All pull-requests will be tested against both visual and performances testing to
|
||||
|
||||
## Fonts and Icons
|
||||
|
||||
GitBook Open uses fontawesome. During development, your local environment will use the free version. However, only the pro version will be accepted by CI. If you see the following error:
|
||||
|
||||
```
|
||||
The GitBook icon is missing. It indicates that the dependencies were installed without the correct font-awesome package. These changes have probably been persisted in the Bun lockfile. Read the README for more information.
|
||||
```
|
||||
|
||||
It means that you've changed the GBO dependencies and bundled in the free version. Only GitBook staff can help with this - if you're not on the GitBook team, please ping us in the PR and we'll help get things moving.
|
||||
|
||||
If you are GitBook staff, you'll need our NPM token in your local environment.
|
||||
|
||||
```
|
||||
.env.local
|
||||
|
||||
NPM_TOKEN_READONLY=xxx
|
||||
```
|
||||
|
||||
and then reinstall dependencies.
|
||||
GitBook Open uses fontawesome. For self-hosting and local development, for licensing reasons, only the icons from the free version should be used.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="65" height="65" viewBox="0 0 65 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M27.3963 34.2195C30.5255 36.0255 32.09 36.9285 33.8082 36.93C35.5265 36.9315 37.0926 36.0313 40.2249 34.2308L60.1913 22.7534C61.0927 22.2353 61.6484 21.2749 61.6484 20.2352C61.6484 19.1955 61.0927 18.2351 60.1913 17.717L40.2177 6.23554C37.0888 4.43695 35.5243 3.53766 33.8078 3.53833C32.0912 3.539 30.5275 4.43951 27.4 6.24053L10.2292 16.1286C10.102 16.2019 10.0383 16.2385 9.97905 16.2732C4.11368 19.7068 0.489862 25.9754 0.441408 32.7717C0.440918 32.8404 0.440918 32.9138 0.440918 33.0607C0.440918 33.2074 0.440918 33.2807 0.441407 33.3494C0.489754 40.138 4.10549 46.4008 9.96041 49.837C10.0196 49.8718 10.0831 49.9085 10.2101 49.9818L20.9658 56.1918C27.2331 59.8104 30.3668 61.6197 33.808 61.6208C37.2493 61.622 40.3842 59.8148 46.6539 56.2005L58.008 49.6551C61.1474 47.8454 62.7171 46.9405 63.579 45.4487C64.4409 43.957 64.4409 42.1451 64.4409 38.5215V31.5212C64.4409 30.5159 63.8965 29.5895 63.0182 29.1004C62.1683 28.627 61.1325 28.6341 60.2891 29.1189L37.0074 42.5019C35.4453 43.3998 34.6643 43.8487 33.8072 43.849C32.9501 43.8493 32.1688 43.4008 30.6062 42.5038L14.8487 33.4586C14.0593 33.0055 13.6647 32.7789 13.3477 32.738C12.625 32.6448 11.9301 33.0497 11.6548 33.7244C11.534 34.0203 11.5365 34.4753 11.5414 35.3855C11.545 36.0555 11.5468 36.3905 11.6094 36.6987C11.7496 37.3887 12.1127 38.0136 12.6428 38.4771C12.8795 38.6842 13.1696 38.8516 13.7499 39.1866L30.5974 48.9103C32.164 49.8145 32.9473 50.2666 33.8075 50.2668C34.6677 50.267 35.4512 49.8154 37.0184 48.912L57.6684 37.0085C58.2037 36.7 58.4713 36.5457 58.672 36.6616C58.8727 36.7776 58.8727 37.0865 58.8727 37.7044V40.8796C58.8727 41.7855 58.8727 42.2385 58.6572 42.6114C58.4417 42.9844 58.0493 43.2106 57.2644 43.663L40.2322 53.4811C37.0966 55.2885 35.5288 56.1923 33.8078 56.1915C32.0869 56.1907 30.5199 55.2855 27.386 53.4752L11.4509 44.2701C11.4003 44.2409 11.375 44.2262 11.3514 44.2125C8.0102 42.26 5.94856 38.6882 5.92922 34.8185C5.92909 34.7911 5.92909 34.7619 5.92909 34.7035V31.7889C5.92909 29.6526 7.06686 27.678 8.9151 26.6067C10.5483 25.66 12.5628 25.6582 14.1977 26.6018L27.3963 34.2195Z" fill="#181C1F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 622 KiB After Width: | Height: | Size: 491 KiB |
-177
@@ -1,177 +0,0 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": [
|
||||
"**/node_modules/**/*",
|
||||
"**/dist/**/*",
|
||||
"**/build/**/*",
|
||||
"**/public/**/*",
|
||||
"**/.next/**/*",
|
||||
"**/.open-next/**/*",
|
||||
"**/.turbo/**/*",
|
||||
"**/.vercel/**/*",
|
||||
"**/.cache/**/*",
|
||||
"**/.wrangler/**/*",
|
||||
"packages/embed/standalone/**/*",
|
||||
"packages/openapi-parser/src/fixtures/**/*",
|
||||
"packages/emoji-codepoints/index.ts",
|
||||
"packages/icons/src/data/*.json",
|
||||
"packages/gitbook/worker-configuration.d.ts",
|
||||
"**/*.css"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"useEditorconfig": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 100,
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"performance": {
|
||||
"noDelete": "warn"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"noUselessFragments": "warn",
|
||||
"noBannedTypes": "warn"
|
||||
},
|
||||
"correctness": {
|
||||
"noUndeclaredVariables": "error",
|
||||
"noUnusedVariables": "error",
|
||||
"useArrayLiterals": "error",
|
||||
"useHookAtTopLevel": "error",
|
||||
"noUnusedImports": "error",
|
||||
"noVoidElementsWithChildren": "warn",
|
||||
"useJsxKeyInIterable": "warn",
|
||||
"useExhaustiveDependencies": "warn",
|
||||
"noUnknownFunction": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "warn",
|
||||
"noParameterAssign": "off",
|
||||
"useThrowOnlyError": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noConsole": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"allow": ["assert", "error", "warn"]
|
||||
}
|
||||
},
|
||||
"noExplicitAny": "warn",
|
||||
"noImplicitAnyLet": "warn",
|
||||
"noConfusingVoidType": "warn",
|
||||
"noControlCharactersInRegex": "warn",
|
||||
"noPrototypeBuiltins": "warn",
|
||||
"noAssignInExpressions": "warn",
|
||||
"noArrayIndexKey": "warn"
|
||||
},
|
||||
"a11y": {
|
||||
"useSemanticElements": "warn",
|
||||
"useKeyWithClickEvents": "warn",
|
||||
"noSvgWithoutTitle": "warn",
|
||||
"useButtonType": "warn",
|
||||
"useIframeTitle": "warn",
|
||||
"useAltText": "warn",
|
||||
"noPositiveTabindex": "warn",
|
||||
"useFocusableInteractive": "warn",
|
||||
"useAriaPropsForRole": "warn",
|
||||
"useValidAnchor": "warn",
|
||||
"noLabelWithoutControl": "warn",
|
||||
"noNoninteractiveTabindex": "warn"
|
||||
},
|
||||
"nursery": {
|
||||
"useSortedClasses": {
|
||||
"level": "error",
|
||||
"fix": "safe",
|
||||
"options": {
|
||||
"attributes": ["class", "className", "style"],
|
||||
"functions": ["clsx", "tw"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "es5",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": [
|
||||
"packages/gitbook/**/*",
|
||||
"packages/react-openapi/**/*",
|
||||
"packages/react-math/**/*",
|
||||
"packages/react-contentkit/**/*",
|
||||
"packages/icons/**/*"
|
||||
],
|
||||
"javascript": {
|
||||
"globals": ["React"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["packages/gitbook/**/*"],
|
||||
"javascript": {
|
||||
"globals": ["React", "GitBookIntegrationEvent"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.css"],
|
||||
"javascript": {
|
||||
"globals": ["theme"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.test.ts", "packages/gitbook/tests/**/*"],
|
||||
"javascript": {
|
||||
"globals": ["Bun"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"packages/cache-do/**/*",
|
||||
"packages/gitbook/cf-env.d.ts",
|
||||
"packages/gitbook/src/cloudflare-entrypoint.ts"
|
||||
],
|
||||
"javascript": {
|
||||
"globals": [
|
||||
"DurableObjectLocationHint",
|
||||
"DurableObjectNamespace",
|
||||
"DurableObjectStub",
|
||||
"ContinentCode",
|
||||
"Fetcher",
|
||||
"ExportedHandler"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
[install.scopes]
|
||||
"gitbook" = { token = "$NPM_TOKEN_READONLY", url = "https://registry.npmjs.org" }
|
||||
+45
-18
@@ -1,45 +1,72 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"turbo": "^2.5.0",
|
||||
"vercel": "^39.3.0"
|
||||
"engines": {
|
||||
"node": "^22.3.0"
|
||||
},
|
||||
"packageManager": "bun@1.2.15",
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.31.1",
|
||||
"oxfmt": "^0.62.0",
|
||||
"oxlint": "^1.82.0",
|
||||
"turbo": "^2.10.12",
|
||||
"vercel": "50.37.3"
|
||||
},
|
||||
"packageManager": "bun@1.3.7",
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"esbuild": "0.24.2"
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"esbuild": "0.27.3",
|
||||
"axios": "1.8.4"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run dev",
|
||||
"dev": "turbo run dev --concurrency 20",
|
||||
"build": "turbo run build",
|
||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"format": "biome check --write ./",
|
||||
"format:check": "biome check --diagnostic-level=error ./",
|
||||
"lint": "oxlint --quiet",
|
||||
"lint:fix": "oxlint --fix --quiet",
|
||||
"format": "oxfmt",
|
||||
"format:check": "oxfmt --check",
|
||||
"unit": "turbo run unit",
|
||||
"e2e": "turbo run e2e",
|
||||
"e2e-customers": "turbo run e2e-customers",
|
||||
"changeset": "changeset",
|
||||
"changeset-version": "changeset version && bun run format",
|
||||
"release": "turbo run release && changeset publish",
|
||||
"changeset-version": "changeset version && bun run format && bun update",
|
||||
"publish-all-packages": "turbo run publish-to-npm --continue=dependencies-successful",
|
||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
||||
"clean": "turbo run clean"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.136.0",
|
||||
"bidc": "^0.0.2"
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@gitbook/api": "0.201.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"assert-never": "^1.4.0",
|
||||
"bidc": "^0.0.2",
|
||||
"bun-types": "^1.1.20",
|
||||
"classnames": "^2.5.1",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"tsdown": "^0.15.6",
|
||||
"typescript": "^5.5.3",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"vercel": "^50.26.1"
|
||||
}
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
|
||||
"next@16.3.3": "patches/next@16.3.3.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
# @gitbook/browser-types
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bf674a4: Add an optional `context` property (string, up to 512 characters) to the `confirmation` of custom AI tools, shown above the confirmation dialog to help the user understand what they are approving or rejecting. The `confirmation` can now also be a function that receives the AI-provided input and returns the confirmation, so the context can be derived dynamically from the arguments the tool is about to run with. Available both to integrations (`GitBookIntegrationTool`) and to embed consumers (`GitBookToolDefinition`).
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5f66860: Use isCookiesTrackingDisabled for cookie consent integrations
|
||||
- 2e495cb: Add Global Privacy Control (GPC) support
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7a11861: Add support for custom cookie banner registration
|
||||
- Updated dependencies [6f1db32]
|
||||
- @gitbook/icons@0.4.3
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 10995e0: Use NPM Trusted publishing for publishing the package.
|
||||
- Updated dependencies [10995e0]
|
||||
- @gitbook/icons@0.3.4
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
- Updated dependencies [6142d6b]
|
||||
- @gitbook/icons@0.3.3
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
- Updated dependencies [295f03d]
|
||||
- @gitbook/icons@0.3.2
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -8,17 +8,33 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"sideEffects": false,
|
||||
"version": "0.1.6",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
"bun-types": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"]
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/GitbookIO/gitbook"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,28 @@ export type GitBookIntegrationEvent = 'load' | 'unload';
|
||||
|
||||
export type GitBookIntegrationEventCallback = (...args: any[]) => void;
|
||||
|
||||
export type GitBookIntegrationToolConfirmation = {
|
||||
icon?: IconName;
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* Supporting context displayed to the user above the confirmation dialog,
|
||||
* to help them understand what they are approving or rejecting.
|
||||
* Limited to 512 characters.
|
||||
*/
|
||||
context?: string;
|
||||
};
|
||||
|
||||
export type GitBookIntegrationTool = AIToolDefinition & {
|
||||
/**
|
||||
* Confirmation action to be displayed to the user before executing the tool.
|
||||
* Provide a static object, or a function that receives the input provided by
|
||||
* the AI assistant and returns the confirmation — useful to display dynamic
|
||||
* context based on the arguments the tool is about to be executed with.
|
||||
*/
|
||||
confirmation?: {
|
||||
icon?: IconName;
|
||||
label: string;
|
||||
};
|
||||
confirmation?:
|
||||
| GitBookIntegrationToolConfirmation
|
||||
| ((input: object) => GitBookIntegrationToolConfirmation);
|
||||
|
||||
/**
|
||||
* Callback when the tool is executed.
|
||||
@@ -46,6 +60,11 @@ export type GitBookAssistant = {
|
||||
ui?: boolean;
|
||||
};
|
||||
|
||||
export type GitBookCookieBannerHandler = (options: {
|
||||
onApprove: () => void;
|
||||
onReject: () => void;
|
||||
}) => void;
|
||||
|
||||
export type GitBookGlobal = {
|
||||
/**
|
||||
* Register an event listener.
|
||||
@@ -72,6 +91,25 @@ export type GitBookGlobal = {
|
||||
* Register a custom assistant to be available on the site.
|
||||
*/
|
||||
registerAssistant: (assistant: GitBookAssistant) => () => void;
|
||||
|
||||
/**
|
||||
* Register a custom cookie banner handler.
|
||||
* When registered, the built-in cookie banner will not be displayed.
|
||||
*/
|
||||
registerCookieBanner: (handler: GitBookCookieBannerHandler) => void;
|
||||
|
||||
/**
|
||||
* Indicates whether cookies tracking is disabled for the current user.
|
||||
* Returns `true` when tracking is disabled, `false` when tracking is enabled,
|
||||
* and `undefined` when the tracking preference is unknown or not yet determined.
|
||||
*/
|
||||
isCookiesTrackingDisabled: () => boolean | undefined;
|
||||
|
||||
/**
|
||||
* Indicates whether global privacy control is enabled for the current user.
|
||||
* Returns `true` when global privacy control is enabled, `false` when it is disabled.
|
||||
*/
|
||||
isGlobalPrivacyControlEnabled: () => boolean;
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @gitbook/cache-tags
|
||||
|
||||
## 0.3.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 10995e0: Use NPM Trusted publishing for publishing the package.
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -4,22 +4,36 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.3.1",
|
||||
"sideEffects": false,
|
||||
"version": "0.3.4",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"assert-never": "^1.2.1"
|
||||
"assert-never": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
"bun-types": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w"
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/GitbookIO/gitbook"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import type { ComputedContentSource } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
|
||||
import type { ComputedContentSource } from '@gitbook/api';
|
||||
|
||||
/**
|
||||
* Get a stringified cache tag for a given object.
|
||||
*/
|
||||
export function getCacheTag(
|
||||
spec: /**
|
||||
* All data related to a user
|
||||
* @deprecated - in v2, no tag as this is an immutable data
|
||||
*/
|
||||
* All data related to a user
|
||||
* @deprecated - in v2, no tag as this is an immutable data
|
||||
*/
|
||||
| {
|
||||
tag: 'user';
|
||||
user: string;
|
||||
@@ -121,17 +122,6 @@ export function getCacheTag(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache tag for a given URL.
|
||||
*/
|
||||
export function getCacheTagForURL(url: string | URL) {
|
||||
const parsedURL = url instanceof URL ? url : new URL(url);
|
||||
return getCacheTag({
|
||||
tag: 'url',
|
||||
hostname: parsedURL.hostname,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tags for a computed content source.
|
||||
*/
|
||||
@@ -182,7 +172,7 @@ export function getComputedContentSourceCacheTags(
|
||||
break;
|
||||
default:
|
||||
// Do not throw for unknown dependency types
|
||||
// as it might mean we are lacking behind the API version
|
||||
// as it might mean we are lagging behind the API version
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -198,9 +188,8 @@ export function getComputedContentSourceCacheTags(
|
||||
}
|
||||
|
||||
// We invalidate the computed content when a new version of the integration is deployed.
|
||||
|
||||
if (source.type.startsWith('integration:')) {
|
||||
const integration = source.type.split(':')[1];
|
||||
const integration = source.type.split(':')[1]!;
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# @gitbook/colors
|
||||
|
||||
## 0.4.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a69a307: A near-white tint color (e.g. a warm `#F5F3EF`) is now taken as the exact page background, mirroring the existing behavior for near-black tints. The tint's exact lightness, hue and chroma are preserved, and the color is anchored to whichever scale step the active theme renders as the background — so it matches exactly on `muted` (which uses the second step) as well as `clean`. This applies only to near-neutral tints that are light enough to read as a background; saturated or merely light-ish colors keep their normal accent scale. The `bold` theme is unaffected: it already uses the tint for the header and stays intentionally two-tone.
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 10995e0: Use NPM Trusted publishing for publishing the package.
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 17dd382: Add `original` background color step
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 193d591: Fix return type for `colorContrast`
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -4,18 +4,33 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.3.3",
|
||||
"sideEffects": false,
|
||||
"version": "0.4.4",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
"bun-types": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w"
|
||||
"unit": "bun test",
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/GitbookIO/gitbook"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { colorScale } from './transformations';
|
||||
|
||||
describe('colorScale exact base', () => {
|
||||
it('takes a very light tint as the exact background on step 1 (clean/bold)', () => {
|
||||
const scale = colorScale('#F5F3EF', { baseStep: 1 });
|
||||
expect(scale[0]).toBe('#F5F3EF');
|
||||
});
|
||||
|
||||
it('anchors a very light tint to step 2 when the theme uses the subtle step (muted)', () => {
|
||||
const scale = colorScale('#F5F3EF', { baseStep: 2 });
|
||||
expect(scale[1]).toBe('#F5F3EF');
|
||||
// Step 1 sits just above the exact base, toward white.
|
||||
expect(scale[0]).not.toBe('#F5F3EF');
|
||||
expect(scale[0]).not.toBe('#ffffff');
|
||||
});
|
||||
|
||||
it('takes a darker-than-dark tint as the exact background, preserving hue and chroma', () => {
|
||||
const scale = colorScale('#0B0F19', { darkMode: true, baseStep: 1 });
|
||||
expect(scale[0]).toBe('#0B0F19');
|
||||
});
|
||||
|
||||
it('does not trigger for a normal mid-lightness tint', () => {
|
||||
const scale = colorScale('#787878', { baseStep: 2 });
|
||||
// The default white background is kept; the tint only colors the scale.
|
||||
expect(scale[0]).toBe('#ffffff');
|
||||
expect(scale[1]).not.toBe('#787878');
|
||||
});
|
||||
|
||||
it('does not trigger for a saturated light color, keeping the normal accent ramp', () => {
|
||||
// Light enough (L≈0.93) to pass the lightness bound, but too chromatic to read as a
|
||||
// background — emitting it verbatim would leave a vivid step 1 above a near-gray scale.
|
||||
const scale = colorScale('#FFEB3B', { baseStep: 1 });
|
||||
expect(scale[0]).toBe('#ffffff');
|
||||
expect(scale[0]).not.toBe('#FFEB3B');
|
||||
});
|
||||
|
||||
it('respects a custom light background instead of overriding it with the tint', () => {
|
||||
// The color is darker than the requested background, so it is not the extreme end and the
|
||||
// supplied base must be preserved rather than overwritten.
|
||||
const scale = colorScale('#eeeeee', { baseStep: 1, background: '#f8f8f8' });
|
||||
expect(scale[0]).not.toBe('#eeeeee');
|
||||
});
|
||||
|
||||
it('anchors an exact base even when a neutral mix is supplied (tint === primary)', () => {
|
||||
// getTintMixColor blends neutral into the tint when it equals the primary color; that must
|
||||
// not darken a near-white tint out of the exact-base path.
|
||||
const scale = colorScale('#F5F3EF', {
|
||||
baseStep: 1,
|
||||
mix: { color: '#787878', ratio: 0.4 },
|
||||
});
|
||||
expect(scale[0]).toBe('#F5F3EF');
|
||||
});
|
||||
|
||||
it('does not trigger for a light accent color below the near-white threshold', () => {
|
||||
// #D8DEEC (light blue-gray, L≈0.90) is a UI accent, not a background, so it must not anchor.
|
||||
const scale = colorScale('#D8DEEC', { baseStep: 1 });
|
||||
expect(scale[0]).toBe('#ffffff');
|
||||
expect(scale[0]).not.toBe('#D8DEEC');
|
||||
});
|
||||
|
||||
it('never anchors when no baseStep is given (accent scales and the bold theme)', () => {
|
||||
// A scale that does not define the page background opts out of the exact base entirely.
|
||||
const scale = colorScale('#F5F3EF', {});
|
||||
expect(scale[0]).toBe('#ffffff');
|
||||
expect(scale[0]).not.toBe('#F5F3EF');
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ type RGBColor = [number, number, number];
|
||||
type OKLABColor = { L: number; A: number; B: number };
|
||||
type OKLCHColor = { L: number; C: number; H: number };
|
||||
|
||||
const D65 = [95.047, 100.0, 108.883]; // Reference white (D65)
|
||||
const D65 = [95.047, 100.0, 108.883] as const; // Reference white (D65)
|
||||
|
||||
export enum ColorCategory {
|
||||
backgrounds = 'backgrounds',
|
||||
@@ -19,7 +19,7 @@ export enum ColorCategory {
|
||||
}
|
||||
|
||||
type ColorSubScale = {
|
||||
[key: string]: number;
|
||||
[key: string]: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -56,6 +56,8 @@ export const scale: Record<ColorCategory, ColorSubScale> = {
|
||||
solid: 9,
|
||||
/** Hovered solid backgrounds */
|
||||
'solid-hover': 10,
|
||||
/** Original color */
|
||||
original: 'original',
|
||||
},
|
||||
[ColorCategory.text]: {
|
||||
/** Very low-contrast text
|
||||
@@ -80,6 +82,22 @@ export const colorMixMapping = {
|
||||
dark: [0, 0.03, 0.08, 0.1, 0.13, 0.15, 0.2, 0.25, 0.5, 0.55, 0.75, 1],
|
||||
};
|
||||
|
||||
/**
|
||||
* Light mode has no equivalent to the dark base bound (nothing is lighter than white), so a tint
|
||||
* at or above this lightness is treated as an explicit, near-white background (e.g. a warm `#F5F3EF`
|
||||
* at L≈0.96). Kept high so light UI accent colors (around L≈0.90) aren't mistaken for backgrounds.
|
||||
*/
|
||||
const EXACT_BASE_LIGHT_THRESHOLD = 0.95;
|
||||
|
||||
/**
|
||||
* Only a near-neutral tint reads as a background. A saturated color would keep the exact hue at the
|
||||
* anchored step while the rest of the low scale stays ~gray, so those keep the normal accent ramp.
|
||||
*/
|
||||
const EXACT_BASE_NEUTRAL_CHROMA = 0.05;
|
||||
|
||||
/** Lightness of the default white light background (≈0.99999, not exactly 1). */
|
||||
const LIGHT_BASE_L = rgbToOklch(hexToRgbArray(LIGHT_BASE)).L;
|
||||
|
||||
/**
|
||||
* Convert a hex color to an RGB color.
|
||||
*/
|
||||
@@ -157,6 +175,14 @@ export type ColorScaleOptions = {
|
||||
/** Define a custom foreground color to use. If left undefined, the global `light`/`dark` values (in `colors.ts`) will be used. */
|
||||
foreground?: string;
|
||||
|
||||
/**
|
||||
* The 1-indexed scale step this scale renders as the page background (1 = `tint-base` for
|
||||
* `clean`, 2 = `tint-subtle` for `muted`). When set, an extreme near-neutral tint is taken as
|
||||
* the exact background, anchored to this step so it matches exactly. Omit for scales that don't
|
||||
* define the page background (accents, or the two-tone `bold` theme) — they never anchor.
|
||||
*/
|
||||
baseStep?: number;
|
||||
|
||||
mix?: {
|
||||
/** If set to a hex code, this color will be additionally mixed into the generated scale according to `mix.ratio`. */
|
||||
color: string;
|
||||
@@ -177,6 +203,7 @@ export function colorScale(
|
||||
darkMode = false,
|
||||
background = darkMode ? DARK_BASE : LIGHT_BASE,
|
||||
foreground = darkMode ? LIGHT_BASE : DARK_BASE,
|
||||
baseStep,
|
||||
mix,
|
||||
}: ColorScaleOptions = {}
|
||||
) {
|
||||
@@ -186,33 +213,53 @@ export function colorScale(
|
||||
const backgroundColor = rgbToOklch(hexToRgbArray(background));
|
||||
let mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
|
||||
|
||||
if (mixColor && mix?.ratio && mix.ratio > 0) {
|
||||
// If defined, we mix in a (tiny) bit of the mix color with the base color.
|
||||
// A near-neutral tint at the extreme end of the scale is taken as the exact page background
|
||||
// rather than tinting pure black/white with it — letting brands set an exact background such as
|
||||
// a warm `#F5F3EF`. Only scales that define the page background opt in (via `baseStep`). In light
|
||||
// mode the base is pure white by default, so a near-white tint also qualifies (nothing is lighter
|
||||
// than white); a custom, lower background is respected instead. Decided on the raw color so a
|
||||
// neutral mix (below) can't darken a tint out of the exact base.
|
||||
const isExtremeBase = darkMode
|
||||
? baseColor.L < backgroundColor.L
|
||||
: backgroundColor.L >= LIGHT_BASE_L
|
||||
? baseColor.L > EXACT_BASE_LIGHT_THRESHOLD
|
||||
: baseColor.L > backgroundColor.L;
|
||||
const isExactBase =
|
||||
baseStep !== undefined && isExtremeBase && baseColor.C < EXACT_BASE_NEUTRAL_CHROMA;
|
||||
const exactBaseIndex = (baseStep ?? 1) - 1;
|
||||
|
||||
if (mixColor && mix?.ratio && mix.ratio > 0 && !isExactBase) {
|
||||
// Mix a little of the mix color into the base — but not when the tint is the exact base,
|
||||
// where it must stay true to the supplied color (and match `--header-background`).
|
||||
baseColor.L = mixColor.L * mix.ratio + baseColor.L * (1 - mix.ratio);
|
||||
baseColor.C = mixColor.C * mix.ratio + baseColor.C * (1 - mix.ratio);
|
||||
baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H;
|
||||
}
|
||||
|
||||
if (
|
||||
(darkMode && baseColor.L < backgroundColor.L) ||
|
||||
(!darkMode && baseColor.L > backgroundColor.L)
|
||||
) {
|
||||
// If the supplied color is outside of our lightness bounds, use the supplied color's lightness.
|
||||
// This is mostly used to allow darker-than-dark backgrounds for brands that specifically want that look.
|
||||
if (isExactBase) {
|
||||
const difference = (backgroundColor.L - baseColor.L) / backgroundColor.L;
|
||||
backgroundColor.L = baseColor.L;
|
||||
// At the edges of the scale, the subtle lightness changes stop being perceptible. We need to amp up our mapping to still stand out.
|
||||
const amplifier = 1;
|
||||
mapping = mapping.map((step, index) =>
|
||||
index < 9 ? step + step * amplifier * difference : step
|
||||
);
|
||||
|
||||
// Anchor the supplied color to the step the theme renders as the background, solving the
|
||||
// background lightness so neighbouring steps stay continuous with it.
|
||||
const baseMix = mapping[exactBaseIndex]!;
|
||||
backgroundColor.L = (baseColor.L - foregroundColor.L * baseMix) / (1 - baseMix);
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
for (let index = 0; index < mapping.length; index++) {
|
||||
const targetL =
|
||||
foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
|
||||
if (isExactBase && index === exactBaseIndex) {
|
||||
result.push(hex);
|
||||
continue;
|
||||
}
|
||||
|
||||
const step = mapping[index]!;
|
||||
const targetL = foregroundColor.L * step + backgroundColor.L * (1 - step);
|
||||
|
||||
if (
|
||||
index === 8 &&
|
||||
@@ -236,7 +283,10 @@ export function colorScale(
|
||||
case 11:
|
||||
return 0.1;
|
||||
default:
|
||||
return index * 0.05;
|
||||
// When the tint is the exact base, hold the steps from the base toward the
|
||||
// accents at its chroma so the background stays tinted; steps lighter than the
|
||||
// base (e.g. cards in `muted`) keep desaturating toward white.
|
||||
return isExactBase && index >= exactBaseIndex ? 1 : index * 0.05;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -295,7 +345,7 @@ export function rgbArrayToHex(rgb: RGBColor): string {
|
||||
|
||||
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
|
||||
const rgb = end.map((channel, index) => {
|
||||
return Math.round(channel + percentage * (start[index] - channel));
|
||||
return Math.round(channel + percentage * (start[index]! - channel));
|
||||
});
|
||||
|
||||
return rgbArrayToHex(rgb as RGBColor);
|
||||
@@ -392,14 +442,14 @@ export function xyzToLab65(xyz: [number, number, number]): {
|
||||
B: number;
|
||||
} {
|
||||
const [x, y, z] = xyz.map((v, i) => {
|
||||
const scaled = v / D65[i];
|
||||
const scaled = v / D65[i]!;
|
||||
return scaled > 0.008856 ? Math.cbrt(scaled) : 7.787 * scaled + 16 / 116;
|
||||
});
|
||||
|
||||
return {
|
||||
L: 116 * y - 16,
|
||||
A: 500 * (x - y),
|
||||
B: 200 * (y - z),
|
||||
L: 116 * y! - 16,
|
||||
A: 500 * (x! - y!),
|
||||
B: 200 * (y! - z!),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -419,7 +469,10 @@ export function dpsContrast(a: RGBColor, b: RGBColor) {
|
||||
return contrast < 7.5 ? 0 : contrast;
|
||||
}
|
||||
|
||||
export function colorContrast(background: string, foreground: string[] = [LIGHT_BASE, DARK_BASE]) {
|
||||
export function colorContrast(
|
||||
background: string,
|
||||
foreground: string[] = [LIGHT_BASE, DARK_BASE]
|
||||
): string {
|
||||
const bg = hexToRgbArray(background);
|
||||
|
||||
const best: { color?: RGBColor; contrast: number } = {
|
||||
@@ -436,5 +489,5 @@ export function colorContrast(background: string, foreground: string[] = [LIGHT_
|
||||
}
|
||||
}
|
||||
|
||||
return best.color ? rgbArrayToHex(best.color) : foreground[0];
|
||||
return best.color ? rgbArrayToHex(best.color) : foreground[0] || LIGHT_BASE;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -1,5 +1,104 @@
|
||||
# @gitbook/embed
|
||||
|
||||
## 0.5.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 94a496c: Make the Docs Embed widget match the page it is embedded in rather than the visitor's OS: a widget on a light page stays light even when the visitor's system is in dark mode, and the widget's own chrome and the docs inside it always render in the same scheme. Sites published with a single theme impose it on the widget too, since they render in it regardless. The standalone script takes `?theme=light` on its URL, and calling `init` twice now updates the options instead of throwing.
|
||||
- bf674a4: Add an optional `context` property (string, up to 512 characters) to the `confirmation` of custom AI tools, shown above the confirmation dialog to help the user understand what they are approving or rejecting. The `confirmation` can now also be a function that receives the AI-provided input and returns the confirmation, so the context can be derived dynamically from the arguments the tool is about to run with. Available both to integrations (`GitBookIntegrationTool`) and to embed consumers (`GitBookToolDefinition`).
|
||||
|
||||
## 0.5.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 37a9453: Allow copying in embed
|
||||
- Updated dependencies [47c8ccb]
|
||||
- @gitbook/icons@0.5.0
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 88c38fa: Add Search tab to Docs Embed, refactor search
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8d2a95b: Docs Embed: Better support light/dark mode overrides
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- b3875a1: Support `assistantName` property to override Assistant name
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 370b731: Docs Embed: Make trademark optional
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b4b471f: Fix unexpected error thrown when using the embed in standalone mode
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 96e24a1: Fix signed JWT token usage
|
||||
- 0aa34cc: Fix Embed options in React
|
||||
- Updated dependencies [6f1db32]
|
||||
- @gitbook/icons@0.4.3
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 1430cee: Improve Docs Embed with separate Assistant and Docs tabs
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [62438f4]
|
||||
- @gitbook/icons@0.4.0
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 10995e0: Use NPM Trusted publishing for publishing the package.
|
||||
- Updated dependencies [10995e0]
|
||||
- @gitbook/icons@0.3.4
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
- Updated dependencies [6142d6b]
|
||||
- @gitbook/icons@0.3.3
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
- Updated dependencies [295f03d]
|
||||
- @gitbook/icons@0.3.2
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7508674: Fix bundling of packages
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6f368b5: Fix embed assistant window width on small screens
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
+474
-9
@@ -1,24 +1,76 @@
|
||||
# `@gitbook/embed`
|
||||
# GitBook Docs Embed (`@gitbook/embed`)
|
||||
|
||||
Embed the GitBook Docs Assistant in your product or website.
|
||||
Embed your GitBook docs in your product or website.
|
||||
|
||||
The Docs Embed can contain three tabs:
|
||||
- **Assistant**: The [GitBook Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) - an AI-powered chat interface to help users find answers
|
||||
- **Search**: A search-focused surface for quickly finding pages and asking scoped questions
|
||||
- **Docs**: A browser for navigating your documentation site
|
||||
|
||||
The embed is set up automatically based on your site's configuration. You can optionally customize and override the configuration with custom actions, tools, suggested questions, [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access), and more. See the [Configuration](#configuration) section for all available options.
|
||||
|
||||
# Usage
|
||||
|
||||
## As a script from your docs site
|
||||
## As a standalone script from your docs site
|
||||
|
||||
All GitBook docs site includes a script to easily embed the docs assistant as a widget on your website.
|
||||
All GitBook docs sites include a script to easily add the Docs Embed as a widget on your website.
|
||||
|
||||
The script is served at `https://docs.company.com/~gitbook/embed/script.js`.
|
||||
|
||||
You can find the embed script from your docs site settings, or you can copy the following and replace the `docs.company.com` by your docs site hostname.
|
||||
You can find the embed script from your docs site settings, or you can copy the following and replace `docs.company.com` with your docs site hostname.
|
||||
|
||||
```html
|
||||
<script src="https://docs.company.com/~gitbook/embed/script.js"></script>
|
||||
```
|
||||
|
||||
The script initializes the widget itself, so there is nothing to call. To pin the embed to one color
|
||||
scheme, put it on the script URL — it has to be known before the widget renders:
|
||||
|
||||
```html
|
||||
<script src="https://docs.company.com/~gitbook/embed/script.js?theme=light"></script>
|
||||
```
|
||||
|
||||
To authenticate the visitor, call `init` with their token. Keep tokens out of the script URL: it is
|
||||
publicly cacheable and ends up in server logs.
|
||||
|
||||
```html
|
||||
<script src="https://docs.company.com/~gitbook/embed/script.js"></script>
|
||||
<script>
|
||||
window.GitBook('show');
|
||||
window.GitBook('init',
|
||||
{ siteURL: 'https://docs.company.com' },
|
||||
{ visitor: { token: 'your-jwt-token' } }
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
The standalone script provides a global `GitBook` function. See the [API Reference](#api-reference) section for all available methods.
|
||||
|
||||
### Example: Configuring the widget
|
||||
|
||||
```javascript
|
||||
GitBook('configure', {
|
||||
button: {
|
||||
label: 'Ask',
|
||||
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
|
||||
},
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
actions: [
|
||||
{
|
||||
icon: 'circle-question',
|
||||
label: 'Contact Support',
|
||||
onClick: () => window.open('https://support.example.com', '_blank')
|
||||
}
|
||||
],
|
||||
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
|
||||
assistantName: 'Support Assistant',
|
||||
suggestions: ['What is GitBook?', 'How do I get started?'],
|
||||
tools: [/* ... */],
|
||||
closeButton: true,
|
||||
});
|
||||
```
|
||||
|
||||
See the [Configuration](#configuration) section for all available options.
|
||||
|
||||
## As a package from NPM
|
||||
|
||||
Install the package: `npm install @gitbook/embed` and import it in your web application:
|
||||
@@ -30,10 +82,49 @@ const gitbook = createGitBook({
|
||||
siteURL: 'https://docs.company.com'
|
||||
});
|
||||
|
||||
// Create an iframe and get its URL
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = gitbook.getFrameURL();
|
||||
iframe.src = gitbook.getFrameURL({
|
||||
colorScheme: 'dark', // Optional: force the embed to render in dark mode
|
||||
visitor: {
|
||||
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
|
||||
unsignedClaims: { // Optional: custom claims for dynamic expressions
|
||||
userId: '123',
|
||||
plan: 'premium'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create a frame client to communicate with the iframe
|
||||
const frame = gitbook.createFrame(iframe);
|
||||
|
||||
// Use the frame client methods
|
||||
frame.navigateToPage('/getting-started'); // Navigate to a page in the docs tab
|
||||
frame.navigateToAssistant(); // Switch to the assistant tab
|
||||
frame.postUserMessage('How do I get started?');
|
||||
frame.clearChat();
|
||||
|
||||
// Configure the embed (see Configuration section for all options)
|
||||
frame.configure({
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
actions: [
|
||||
{
|
||||
icon: 'circle-question',
|
||||
label: 'Contact Support',
|
||||
onClick: () => window.open('https://support.example.com', '_blank')
|
||||
}
|
||||
],
|
||||
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
|
||||
assistantName: 'Support Assistant',
|
||||
suggestions: ['What is GitBook?', 'How do I get started?'],
|
||||
tools: [/* ... */],
|
||||
closeButton: true
|
||||
});
|
||||
|
||||
// Listen to events
|
||||
frame.on('close', () => {
|
||||
console.log('Frame closed');
|
||||
});
|
||||
```
|
||||
|
||||
## As React components
|
||||
@@ -41,9 +132,383 @@ const frame = gitbook.createFrame(iframe);
|
||||
After installing the NPM package, you can import prebuilt React components:
|
||||
|
||||
```tsx
|
||||
import { GitBookProvider, GitBookAssistantFrame } from '@gitbook/embed/react';
|
||||
import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
|
||||
|
||||
<GitBookProvider siteURL="https://docs.company.com">
|
||||
<GitBookAssistantFrame />
|
||||
<GitBookFrame
|
||||
colorScheme="dark"
|
||||
visitor={{
|
||||
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
|
||||
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
|
||||
}}
|
||||
tabs={['assistant', 'search', 'docs']}
|
||||
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
|
||||
assistantName="Support Assistant"
|
||||
suggestions={['What is GitBook?', 'How do I get started?']}
|
||||
actions={[
|
||||
{
|
||||
icon: 'circle-question',
|
||||
label: 'Contact Support',
|
||||
onClick: () => window.open('https://support.example.com', '_blank')
|
||||
}
|
||||
]}
|
||||
tools={[/* ... */]}
|
||||
closeButton
|
||||
/>
|
||||
</GitBookProvider>
|
||||
```
|
||||
|
||||
You can also use the `useGitBook` hook to access the client:
|
||||
|
||||
```tsx
|
||||
import { useGitBook } from '@gitbook/embed/react';
|
||||
|
||||
function MyComponent() {
|
||||
const gitbook = useGitBook();
|
||||
const frameURL = gitbook.getFrameURL({ colorScheme: 'dark', visitor: { token: '...' } });
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
# API Reference
|
||||
|
||||
## Method Comparison
|
||||
|
||||
| Method | Standalone Script | NPM Package | React Components |
|
||||
|--------|------------------|-------------|------------------|
|
||||
| **Initialize** | `GitBook('init', options, frameOptions)` | `createGitBook(options)` | `<GitBookProvider siteURL="...">` |
|
||||
| **Get frame URL** | ❌ (handled internally) | `client.getFrameURL(options)` | `useGitBook().getFrameURL(options)` |
|
||||
| **Create frame client** | ❌ (handled internally) | `client.createFrame(iframe)` | `useGitBook().createFrame(iframe)` |
|
||||
| **Show/Hide widget** | `GitBook('show')` / `GitBook('hide')` | ❌ | ❌ |
|
||||
| **Open/Close window** | `GitBook('open')` / `GitBook('close')` / `GitBook('toggle')` | ❌ | ❌ |
|
||||
| **Navigate to page** | `GitBook('navigateToPage', path)` | `frame.navigateToPage(path)` | Via frame client |
|
||||
| **Navigate to assistant** | `GitBook('navigateToAssistant')` | `frame.navigateToAssistant()` | Via frame client |
|
||||
| **Post message** | `GitBook('postUserMessage', message)` | `frame.postUserMessage(message)` | Via frame client |
|
||||
| **Clear chat** | `GitBook('clearChat')` | `frame.clearChat()` | Via frame client |
|
||||
| **Configure** | `GitBook('configure', settings)` | `frame.configure(settings)` | Props on `<GitBookFrame>` |
|
||||
| **Event listeners** | ❌ | `frame.on(event, listener)` | Via frame client |
|
||||
| **Unload** | `GitBook('unload')` | ❌ | ❌ |
|
||||
|
||||
## Method Signatures
|
||||
|
||||
### Standalone Script
|
||||
|
||||
- `GitBook('init', options: { siteURL: string }, frameOptions?: { colorScheme?: 'light' | 'dark', visitor?: {...} })` - Initialize widget
|
||||
- `GitBook('show')` - Show widget button
|
||||
- `GitBook('hide')` - Hide widget button
|
||||
- `GitBook('open')` - Open widget window
|
||||
- `GitBook('close')` - Close widget window
|
||||
- `GitBook('toggle')` - Toggle widget window
|
||||
- `GitBook('navigateToPage', path: string)` - Navigate to page (accepts the page path within the site, an absolute path, or the full published URL; resolves pages in any space/section)
|
||||
- `GitBook('navigateToAssistant')` - Navigate to assistant tab
|
||||
- `GitBook('postUserMessage', message: string)` - Post message to chat
|
||||
- `GitBook('clearChat')` - Clear chat history
|
||||
- `GitBook('configure', settings: {...})` - Configure widget
|
||||
- `GitBook('unload')` - Unload widget
|
||||
|
||||
### NPM Package
|
||||
|
||||
**Client Factory:**
|
||||
- `createGitBook(options: { siteURL: string })` → `GitBookClient`
|
||||
- `client.getFrameURL(options?: { colorScheme?: 'light' | 'dark', visitor?: {...} })` → `string`
|
||||
- `client.createFrame(iframe: HTMLIFrameElement)` → `GitBookFrameClient`
|
||||
|
||||
**Frame Client:**
|
||||
- `frame.navigateToPage(path: string)` → `void` (accepts the page path within the site, an absolute path, or the full published URL; resolves pages in any space/section)
|
||||
- `frame.navigateToAssistant()` → `void`
|
||||
- `frame.postUserMessage(message: string)` → `void`
|
||||
- `frame.clearChat()` → `void`
|
||||
- `frame.configure(settings: Partial<GitBookEmbeddableConfiguration>)` → `void`
|
||||
- `frame.on(event: string, listener: Function)` → `() => void` (unsubscribe)
|
||||
|
||||
### React Components
|
||||
|
||||
**Components:**
|
||||
- `<GitBookProvider siteURL: string>` - Provider component
|
||||
- `<GitBookFrame {...props}>` - Frame component (accepts all config options as props)
|
||||
|
||||
**Hooks:**
|
||||
- `useGitBook()` → `GitBookClient` (must be used within `<GitBookProvider>`)
|
||||
|
||||
# Configuration
|
||||
|
||||
Configuration options are available across usage methods as follows:
|
||||
- **Standalone script**: via `GitBook('configure', {...})`
|
||||
- **NPM package**: via `frame.configure({...})`
|
||||
- **React components**: via props on `<GitBookFrame>`
|
||||
|
||||
### `tabs`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
Override which tabs are displayed. Defaults to your site's configuration.
|
||||
|
||||
- **Type**: `('assistant' | 'search' | 'docs')[]`
|
||||
|
||||
```javascript
|
||||
tabs: ['assistant', 'search', 'docs']
|
||||
```
|
||||
|
||||
### `closeButton`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
Display a close (`x`) button in the embed sidebar.
|
||||
|
||||
- **Type**: `boolean`
|
||||
- **Default**: `false`
|
||||
|
||||
```javascript
|
||||
closeButton: true
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- When clicked, the embed sends a `close` event to the parent page.
|
||||
- In the standalone script, this event is handled automatically and the widget closes.
|
||||
- In custom iframe integrations (NPM package), you must listen for the `close` event and decide how to hide/collapse your UI.
|
||||
- In React, this works automatically when using the standard widget flow. If you build custom frame wiring, handle `close` the same way as the NPM package.
|
||||
|
||||
NPM package example:
|
||||
|
||||
```typescript
|
||||
const frame = gitbook.createFrame(iframe);
|
||||
|
||||
frame.configure({
|
||||
closeButton: true
|
||||
});
|
||||
|
||||
const unsubscribe = frame.on('close', () => {
|
||||
// Hide your modal/drawer/container
|
||||
container.classList.add('hidden');
|
||||
});
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The close button is rendered in the same sidebar area as tabs/actions.
|
||||
- If your UI hides that sidebar or doesn't render it, the button will not be visible.
|
||||
|
||||
### `trademark`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
Display GitBook branding in the embed. Defaults to true.
|
||||
|
||||
- **Type**: `boolean`
|
||||
- **Default**: `true`
|
||||
|
||||
```javascript
|
||||
trademark: true
|
||||
```
|
||||
|
||||
### `actions`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
Custom action buttons rendered in the sidebar alongside tabs. Each action button triggers a callback when clicked.
|
||||
|
||||
**Note**: This prop was previously named `buttons`. Use `actions` instead, it has the same functionality.
|
||||
|
||||
- **Type**: `GitBookEmbeddableActionDefinition[]`
|
||||
- **Properties**:
|
||||
- `icon`: `string` - Icon name. Any [FontAwesome icon](https://fontawesome.com/search) is supported. (e.g., `'rocket'`, `'comments'`, `'user-circle'`, ...)
|
||||
- `label`: `string` - Button label text
|
||||
- `onClick`: `() => void | Promise<void>` - Callback function when clicked
|
||||
|
||||
```javascript
|
||||
actions: [
|
||||
{
|
||||
icon: 'comments',
|
||||
label: 'Contact Support',
|
||||
onClick: () => window.open('https://support.example.com', '_blank')
|
||||
},
|
||||
{
|
||||
icon: 'rocket',
|
||||
label: 'Get started',
|
||||
onClick: () => {
|
||||
GitBook('navigateToPage', '/getting-started');
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `greeting`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
Welcome message displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) tab.
|
||||
|
||||
- **Type**: `{ title: string, subtitle: string }`
|
||||
|
||||
```javascript
|
||||
greeting: {
|
||||
title: 'Welcome!',
|
||||
subtitle: 'How can I help you today?'
|
||||
}
|
||||
```
|
||||
|
||||
### `assistantName`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
Override the assistant name displayed in the chat header and assistant entry points (for example, sidebar tabs and action labels). This name will be limited to 32 characters to prevent text overflow.
|
||||
|
||||
- **Type**: `string`
|
||||
|
||||
```javascript
|
||||
assistantName: 'Support Assistant'
|
||||
```
|
||||
|
||||
### `suggestions`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
Suggested questions displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) welcome screen.
|
||||
|
||||
- **Type**: `string[]`
|
||||
|
||||
```javascript
|
||||
suggestions: [
|
||||
'What is GitBook?',
|
||||
'How do I get started?',
|
||||
'What can you do?'
|
||||
]
|
||||
```
|
||||
|
||||
### `tools`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
Custom AI tools to extend the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant). Tools allow the assistant to execute functions and integrate with your own systems.
|
||||
|
||||
**Note**: In addition to custom tools, the Assistant will always have access to any [MCP servers you define](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant#extend-gitbook-assistant-with-mcp-servers) in your site's AI settings.
|
||||
|
||||
- **Type**: `GitBookToolDefinition[]`
|
||||
- **Properties**:
|
||||
- `name`: `string` - Unique tool identifier
|
||||
- `description`: `string` - Description of what the tool does (used by the AI to decide when and how to use it).
|
||||
- `inputSchema`: `object` - JSON schema defining the tool's input parameters
|
||||
- `execute`: `(input: object) => Promise<{ output: any, summary: string }>` - Async function that executes the tool.
|
||||
- `output`: The result of the tool execution, provided to the AI to continue working with. Not shown to the user.
|
||||
- `summary`: The visual summary of the tool execution, shown in the user's chat window.
|
||||
- `confirmation`: `{ icon?: string, label: string }` (optional) - Confirmation button shown before execution, useful for actions that require the user's express approval.
|
||||
|
||||
```javascript
|
||||
tools: [
|
||||
{
|
||||
name: 'get_user_info',
|
||||
description: 'Get information about the current user',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: {
|
||||
type: 'string',
|
||||
description: 'The user ID to look up'
|
||||
}
|
||||
},
|
||||
required: ['userId']
|
||||
},
|
||||
execute: async (input) => {
|
||||
const user = await fetch(`/api/users/${input.userId}`).then(r => r.json());
|
||||
return {
|
||||
output: { name: user.name, plan: user.plan },
|
||||
summary: `Retrieved info for user ${user.name}`
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'create_ticket',
|
||||
description: 'Create a support ticket',
|
||||
confirmation: {
|
||||
icon: 'circle-question',
|
||||
label: 'Create support ticket?'
|
||||
},
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
subject: { type: 'string' },
|
||||
description: { type: 'string' }
|
||||
},
|
||||
required: ['subject', 'description']
|
||||
},
|
||||
execute: async (input) => {
|
||||
const ticket = await fetch('/api/tickets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input)
|
||||
}).then(r => r.json());
|
||||
return {
|
||||
output: { ticketId: ticket.id },
|
||||
summary: `Created ticket #${ticket.id}`
|
||||
};
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `visitor` (Authenticated Access)
|
||||
|
||||
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
|
||||
|
||||
[Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access) options passed when creating the frame URL. Used for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) and [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access).
|
||||
|
||||
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
|
||||
|
||||
**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)`
|
||||
**NPM package**: Pass to `getFrameURL({ visitor: {...} })`
|
||||
**React components**: Pass as the `visitor` prop on `<GitBookFrame>`
|
||||
|
||||
- **Type**: `{ token?: string, unsignedClaims?: Record<string, unknown> }`
|
||||
- **Properties**:
|
||||
- `token`: `string` (optional) - Signed JWT token for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) or [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access)
|
||||
- `unsignedClaims`: `Record<string, unknown>` (optional) - Unsigned claims that can be used in dynamic expressions via `visitor.claims.unsigned.<claim-name>`
|
||||
|
||||
```javascript
|
||||
visitor: {
|
||||
token: 'your-jwt-token',
|
||||
unsignedClaims: {
|
||||
userId: '123',
|
||||
plan: 'premium',
|
||||
role: 'admin'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `colorScheme`
|
||||
|
||||
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
|
||||
|
||||
Override the embed's color scheme.
|
||||
|
||||
When omitted, the standalone widget follows the page it is embedded in — its `color-scheme`, falling back to the visitor's OS preference only when that page declares support for both. With the NPM package or the React components you own the iframe, so the embed follows the visitor's OS preference unless you pass this.
|
||||
|
||||
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
|
||||
|
||||
**Standalone script**: `?theme=light` on the script URL
|
||||
**NPM package**: Pass to `getFrameURL({ colorScheme: 'dark' })`
|
||||
**React components**: Pass as the `colorScheme` prop on `<GitBookFrame>`
|
||||
|
||||
- **Type**: `'light' | 'dark'`
|
||||
|
||||
```html
|
||||
<script src="https://docs.company.com/~gitbook/embed/script.js?theme=light"></script>
|
||||
```
|
||||
|
||||
Sites published with a single theme always render in that theme, so `colorScheme` has no effect on
|
||||
them — the widget follows the site instead, to keep its chrome and the docs inside it consistent.
|
||||
|
||||
### `button`
|
||||
|
||||
Available in: Standalone script only
|
||||
|
||||
Configure the widget button for the standalone script. This option is not available when using the NPM package or React components, since they can be customized completely.
|
||||
|
||||
- **Type**: `{ label: string, icon: 'assistant' | 'sparkle' | 'help' | 'book' }`
|
||||
- **Properties**:
|
||||
- `label`: `string` - Button label text
|
||||
- `icon`: `'assistant' | 'sparkle' | 'help' | 'book'` - Icon displayed on the button. Choose from one of 4 presets.
|
||||
|
||||
```javascript
|
||||
button: {
|
||||
label: 'Ask',
|
||||
icon: 'assistant'
|
||||
}
|
||||
```
|
||||
|
||||
+33
-10
@@ -5,27 +5,50 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js",
|
||||
"standalone": "./dist/standalone/index.js",
|
||||
"react": "./dist/react/index.js"
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./react": {
|
||||
"types": "./dist/react/index.d.ts",
|
||||
"default": "./dist/react/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"sideEffects": false,
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:",
|
||||
"bidc": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
"react": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
"react": "^19.0.0"
|
||||
"bun-types": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"react": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"build": "bun run build-lib && bun run build-standalone",
|
||||
"build-lib": "tsdown",
|
||||
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||
"clean": "rm -rf ./dist",
|
||||
"unit": "bun test",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md", "standalone"]
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/GitbookIO/gitbook"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { createGitBook } from './createGitBook';
|
||||
|
||||
describe('createGitBook.getFrameURL', () => {
|
||||
it('builds the embed URL when the site URL has no trailing slash', () => {
|
||||
const client = createGitBook({ siteURL: 'https://example.com/docs' });
|
||||
|
||||
const url = new URL(client.getFrameURL({}));
|
||||
|
||||
expect(url.origin).toBe('https://example.com');
|
||||
expect(url.pathname).toBe('/docs/~gitbook/embed');
|
||||
expect(url.searchParams.toString()).toBe('');
|
||||
});
|
||||
|
||||
it('handles a trailing slash and adds visitor parameters', () => {
|
||||
const client = createGitBook({ siteURL: 'https://example.com/docs/' });
|
||||
|
||||
const url = new URL(
|
||||
client.getFrameURL({
|
||||
visitor: {
|
||||
token: 'signed-token',
|
||||
unsignedClaims: {
|
||||
role: 'editor',
|
||||
count: 3,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(url.pathname).toBe('/docs/~gitbook/embed');
|
||||
expect(url.searchParams.get('jwt_token')).toBe('signed-token');
|
||||
expect(url.searchParams.get('visitor.role')).toBe('editor');
|
||||
expect(url.searchParams.get('visitor.count')).toBe('3');
|
||||
expect(url.searchParams.get('visitor.enabled')).toBe('false');
|
||||
});
|
||||
|
||||
it('adds an explicit color scheme override when requested', () => {
|
||||
const client = createGitBook({ siteURL: 'https://example.com/docs/' });
|
||||
|
||||
const url = new URL(
|
||||
client.getFrameURL({
|
||||
colorScheme: 'dark',
|
||||
})
|
||||
);
|
||||
|
||||
expect(url.pathname).toBe('/docs/~gitbook/embed');
|
||||
expect(url.searchParams.get('theme')).toBe('dark');
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,14 @@ export type CreateGitBookOptions = {
|
||||
};
|
||||
|
||||
export type GetFrameURLOptions = {
|
||||
/**
|
||||
* Override the color scheme used by the embedded docs.
|
||||
* When omitted, the standalone widget follows the page it is embedded in, and only falls back
|
||||
* to the visitor's OS preference when that page supports both schemes. Building the iframe
|
||||
* yourself, the embed follows the visitor's OS preference unless you pass this.
|
||||
*/
|
||||
colorScheme?: 'light' | 'dark';
|
||||
|
||||
/**
|
||||
* Authentication to use for the frame.
|
||||
*/
|
||||
@@ -40,10 +48,14 @@ export function createGitBook(options: CreateGitBookOptions) {
|
||||
const client: GitBookClient = {
|
||||
getFrameURL: (frameOptions) => {
|
||||
const url = new URL(options.siteURL);
|
||||
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed/assistant`;
|
||||
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed`;
|
||||
|
||||
if (frameOptions.colorScheme) {
|
||||
url.searchParams.set('theme', frameOptions.colorScheme);
|
||||
}
|
||||
|
||||
if (frameOptions.visitor?.token) {
|
||||
url.searchParams.set('token', frameOptions.visitor.token);
|
||||
url.searchParams.set('jwt_token', frameOptions.visitor.token);
|
||||
}
|
||||
|
||||
if (frameOptions.visitor?.unsignedClaims) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createChannel } from 'bidc';
|
||||
|
||||
import type {
|
||||
FrameToParentMessage,
|
||||
GitBookEmbeddableConfiguration,
|
||||
@@ -7,7 +8,12 @@ import type {
|
||||
|
||||
export type GitBookFrameClient = {
|
||||
/**
|
||||
* Navigate to a page by its path.
|
||||
* Navigate to a page in the docs tab.
|
||||
*
|
||||
* Accepts the page's path within the site (e.g. `getting-started/quickstart`),
|
||||
* an absolute path (e.g. `/help-center/integrations`), or its full published URL.
|
||||
* The target page may live in any space/section of the site — including one other
|
||||
* than the embed is currently showing — and is resolved to the right space.
|
||||
*/
|
||||
navigateToPage: (path: string) => void;
|
||||
|
||||
@@ -44,6 +50,16 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
|
||||
if (!iframe.contentWindow) {
|
||||
throw new Error('Iframe must have a content window');
|
||||
}
|
||||
|
||||
const allowTokens = iframe.allow
|
||||
.split(';')
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (!allowTokens.includes('clipboard-write')) {
|
||||
iframe.allow = [...allowTokens, 'clipboard-write'].join('; ');
|
||||
}
|
||||
|
||||
const channel = createChannel(iframe.contentWindow);
|
||||
|
||||
channel.receive((message: FrameToParentMessage) => {
|
||||
@@ -61,13 +77,15 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
|
||||
channel.send(message);
|
||||
};
|
||||
|
||||
const events = new Map<string, Array<(...args: any[]) => void>>();
|
||||
const events = new Map<string, ((...args: any[]) => void)[]>();
|
||||
|
||||
const configuration: GitBookEmbeddableConfiguration = {
|
||||
buttons: [],
|
||||
welcomeMessage: '',
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
actions: [],
|
||||
greeting: { title: '', subtitle: '' },
|
||||
suggestions: [],
|
||||
tools: [],
|
||||
trademark: true,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
|
||||
/**
|
||||
* Confirmation action to be displayed to the user before executing a tool.
|
||||
*/
|
||||
export type GitBookToolConfirmation = {
|
||||
icon?: IconName;
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* Supporting context displayed to the user above the confirmation dialog,
|
||||
* to help them understand what they are approving or rejecting.
|
||||
* Limited to 512 characters.
|
||||
*/
|
||||
context?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom tool definition to be passed to the AI assistant.
|
||||
*/
|
||||
export type GitBookToolDefinition = AIToolDefinition & {
|
||||
/**
|
||||
* Confirmation action to be displayed to the user before executing the tool.
|
||||
* Provide a static object, or a function that receives the input provided by
|
||||
* the AI assistant and returns the confirmation — useful to display dynamic
|
||||
* context based on the arguments the tool is about to be executed with.
|
||||
*/
|
||||
confirmation?: {
|
||||
icon?: IconName;
|
||||
label: string;
|
||||
};
|
||||
confirmation?: GitBookToolConfirmation | ((input: object) => GitBookToolConfirmation);
|
||||
|
||||
/**
|
||||
* Callback when the tool is executed.
|
||||
@@ -23,7 +38,7 @@ export type GitBookToolDefinition = AIToolDefinition & {
|
||||
/**
|
||||
* Custom button definition to be passed to the embeddable GitBook.
|
||||
*/
|
||||
export type GitBookEmbeddableButtonDefinition = {
|
||||
export type GitBookEmbeddableActionDefinition = {
|
||||
/**
|
||||
* Icon to be displayed in the button.
|
||||
*/
|
||||
@@ -41,22 +56,48 @@ export type GitBookEmbeddableButtonDefinition = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Overall configuration for the layout of the embeddable GitBook.
|
||||
* Overall configuration for the layout of the GitBook embed.
|
||||
*/
|
||||
export type GitBookEmbeddableConfiguration = {
|
||||
/** Tabs to display in the embed (if enabled on the site). */
|
||||
tabs: ('assistant' | 'docs' | 'search')[];
|
||||
|
||||
/** Additional buttons to be displayed in the header of the GitBook embed. */
|
||||
actions: GitBookEmbeddableActionDefinition[];
|
||||
|
||||
/**
|
||||
* Buttons to be displayed in the header of the embeddable GitBook.
|
||||
* Additional buttons to be displayed in the header of the GitBook embed.
|
||||
* @deprecated Use `actions` instead.
|
||||
*/
|
||||
buttons: GitBookEmbeddableButtonDefinition[];
|
||||
buttons?: GitBookEmbeddableActionDefinition[];
|
||||
|
||||
/** Message to be displayed in the welcome page. */
|
||||
welcomeMessage: string;
|
||||
greeting: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Override the assistant name displayed in the UI.
|
||||
* Limited to 32 characters.
|
||||
*/
|
||||
assistantName?: string;
|
||||
|
||||
/** Suggestions of questions to be displayed in the welcome page. */
|
||||
suggestions: string[];
|
||||
|
||||
/** Tools to be provided to the assistant. */
|
||||
tools: GitBookToolDefinition[];
|
||||
|
||||
/**
|
||||
* Display GitBook branding in the embed.
|
||||
*/
|
||||
trademark?: boolean;
|
||||
|
||||
/**
|
||||
* Display a close button inside the assistant.
|
||||
*/
|
||||
closeButton?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './client';
|
||||
export * from './standalone';
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type {
|
||||
GetFrameURLOptions,
|
||||
GitBookEmbeddableConfiguration,
|
||||
@@ -9,34 +12,63 @@ import { useGitBook } from './GitBookProvider';
|
||||
export type GitBookFrameProps = {
|
||||
className?: string;
|
||||
} & GetFrameURLOptions &
|
||||
GitBookEmbeddableConfiguration;
|
||||
Partial<GitBookEmbeddableConfiguration>;
|
||||
|
||||
/**
|
||||
* Render a frame with the GitBook Assistant in it.
|
||||
*/
|
||||
export function GitBookFrame(props: GitBookFrameProps) {
|
||||
const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props;
|
||||
const {
|
||||
className,
|
||||
colorScheme,
|
||||
visitor,
|
||||
actions = [],
|
||||
greeting,
|
||||
suggestions = [],
|
||||
tools = [],
|
||||
tabs = ['assistant', 'search', 'docs'],
|
||||
trademark = true,
|
||||
closeButton = false,
|
||||
assistantName,
|
||||
} = props;
|
||||
|
||||
const frameRef = React.useRef<HTMLIFrameElement>(null);
|
||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||
const gitbook = useGitBook();
|
||||
const [gitbookFrame, setGitbookFrame] = React.useState<GitBookFrameClient | null>(null);
|
||||
const [gitbookFrame, setGitbookFrame] = useState<GitBookFrameClient | null>(null);
|
||||
|
||||
const frameURL = React.useMemo(() => gitbook.getFrameURL({ visitor }), [gitbook, visitor]);
|
||||
const frameURL = useMemo(
|
||||
() => gitbook.getFrameURL({ visitor, colorScheme }),
|
||||
[gitbook, visitor, colorScheme]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (frameRef.current) {
|
||||
setGitbookFrame(gitbook.createFrame(frameRef.current));
|
||||
}
|
||||
}, [gitbook]);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
gitbookFrame?.configure({
|
||||
buttons,
|
||||
welcomeMessage,
|
||||
tabs,
|
||||
actions,
|
||||
greeting,
|
||||
suggestions,
|
||||
tools,
|
||||
closeButton,
|
||||
trademark,
|
||||
assistantName,
|
||||
});
|
||||
}, [gitbookFrame, buttons, welcomeMessage, suggestions, tools]);
|
||||
}, [
|
||||
gitbookFrame,
|
||||
actions,
|
||||
greeting,
|
||||
suggestions,
|
||||
tools,
|
||||
tabs,
|
||||
closeButton,
|
||||
trademark,
|
||||
assistantName,
|
||||
]);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
@@ -45,6 +77,7 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
src={frameURL}
|
||||
width="100%"
|
||||
height="100%"
|
||||
allow="clipboard-write"
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { type CreateGitBookOptions, createGitBook } from '../client';
|
||||
import { GitBookContext } from './context';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { GitBookClient } from '../client';
|
||||
|
||||
export const GitBookContext = React.createContext<GitBookClient | null>(null);
|
||||
|
||||
@@ -31,24 +31,52 @@ type StandaloneCalls =
|
||||
// Clear the chat
|
||||
| ['clearChat']
|
||||
// Configure the embed
|
||||
| ['configure', Partial<GitBookEmbeddableConfiguration>]
|
||||
| ['configure', Partial<GitBookEmbeddableConfiguration & StandaloneConfiguration>]
|
||||
// Navigate to a page
|
||||
| ['navigateToPage', string]
|
||||
// Navigate to the assistant
|
||||
| ['navigateToAssistant'];
|
||||
|
||||
type StandaloneConfiguration = {
|
||||
/** Configure the button to open the embed */
|
||||
button: {
|
||||
/** Label to be displayed in the button. */
|
||||
label: string;
|
||||
/** Icon to be displayed in the button. */
|
||||
icon: 'assistant' | 'sparkle' | 'help' | 'book';
|
||||
};
|
||||
};
|
||||
|
||||
export type GitBookStandalone = ((...args: StandaloneCalls) => void) & {
|
||||
q?: StandaloneCalls[];
|
||||
};
|
||||
|
||||
let widgetIframe: HTMLIFrameElement | undefined;
|
||||
let _client: GitBookClient | undefined;
|
||||
let _frame: GitBookFrameClient | undefined;
|
||||
let frameOptions: GetFrameURLOptions | undefined;
|
||||
let frameConfigured = false;
|
||||
let frameConfiguration: GitBookEmbeddableConfiguration & StandaloneConfiguration = {
|
||||
button: {
|
||||
label: 'Ask',
|
||||
icon: 'assistant',
|
||||
},
|
||||
actions: [],
|
||||
greeting: { title: '', subtitle: '' },
|
||||
suggestions: [],
|
||||
tools: [],
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
trademark: true,
|
||||
};
|
||||
|
||||
const widgetButton = document.createElement('button');
|
||||
widgetButton.id = 'gitbook-widget-button';
|
||||
widgetButton.addEventListener('click', () => {
|
||||
GitBook('toggle');
|
||||
});
|
||||
widgetButton.innerHTML = `
|
||||
<span id="gitbook-widget-button-icon"></span>
|
||||
<span id="gitbook-widget-button-label">Ask</span>
|
||||
<span id="gitbook-widget-button-icon" data-icon="${frameConfiguration.button.icon}"></span>
|
||||
<span id="gitbook-widget-button-label">${frameConfiguration.button.label}</span>
|
||||
`;
|
||||
|
||||
const widgetWindow = document.createElement('div');
|
||||
@@ -58,16 +86,46 @@ widgetWindow.classList.add('hidden');
|
||||
document.body.appendChild(widgetButton);
|
||||
document.body.appendChild(widgetWindow);
|
||||
|
||||
let widgetIframe: HTMLIFrameElement | undefined;
|
||||
let _client: GitBookClient | undefined;
|
||||
let _frame: GitBookFrameClient | undefined;
|
||||
let frameOptions: GetFrameURLOptions | undefined;
|
||||
let frameConfiguration: GitBookEmbeddableConfiguration = {
|
||||
buttons: [],
|
||||
welcomeMessage: '',
|
||||
suggestions: [],
|
||||
tools: [],
|
||||
};
|
||||
/**
|
||||
* The one scheme everything follows: the widget's chrome, the frame's URL and the docs inside it.
|
||||
* Either it was configured, or we match the page we are embedded in (RND-12558).
|
||||
*/
|
||||
function resolveColorScheme(): 'light' | 'dark' {
|
||||
const configured = frameOptions?.colorScheme;
|
||||
// Callers are plain JS, so anything else — a typo, a `system` — falls back to the page rather
|
||||
// than reaching the CSS and the frame's URL, where the two would disagree.
|
||||
return configured === 'light' || configured === 'dark' ? configured : hostColorScheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* The scheme the embedding page renders in, which is not the visitor's OS preference: a page that
|
||||
* never opted into dark stays light however the OS is set.
|
||||
*
|
||||
* Resolving a `light-dark()` is the only way to read it. A page's *used* color scheme isn't exposed
|
||||
* anywhere — the CSSOM gives computed values, and a `<meta name="color-scheme">` (the common way to
|
||||
* declare it) never even reaches those.
|
||||
*/
|
||||
function hostColorScheme(): 'light' | 'dark' {
|
||||
const probe = document.createElement('div');
|
||||
// The first `color` is the fallback where `light-dark()` is unsupported: without it the probe
|
||||
// would inherit the page's own text colour and a white one would read as dark.
|
||||
probe.style.cssText =
|
||||
'display:none;color:rgb(0,0,0);color:light-dark(rgb(0,0,0), rgb(255,255,255))';
|
||||
document.body.appendChild(probe);
|
||||
const used = getComputedStyle(probe).color;
|
||||
probe.remove();
|
||||
|
||||
return used === 'rgb(255, 255, 255)' ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
/** Mirror the resolved scheme onto the widget's own chrome, and hand it back for the frame's URL. */
|
||||
function applyColorScheme(): 'light' | 'dark' {
|
||||
const colorScheme = resolveColorScheme();
|
||||
for (const element of [widgetButton, widgetWindow]) {
|
||||
element.dataset.colorScheme = colorScheme;
|
||||
}
|
||||
return colorScheme;
|
||||
}
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
@@ -85,31 +143,63 @@ function getIframe() {
|
||||
widgetIframe?.remove();
|
||||
widgetIframe = document.createElement('iframe');
|
||||
widgetIframe.id = 'gitbook-widget-iframe';
|
||||
widgetIframe.src = client.getFrameURL({
|
||||
...frameOptions,
|
||||
});
|
||||
widgetIframe.allow = 'clipboard-write';
|
||||
// One read for both, so the docs can't come back in a different scheme than the panel.
|
||||
widgetIframe.src = client.getFrameURL({ ...frameOptions, colorScheme: applyColorScheme() });
|
||||
widgetWindow.appendChild(widgetIframe);
|
||||
|
||||
_frame = client.createFrame(widgetIframe);
|
||||
_frame.on('close', () => {
|
||||
widgetWindow.classList.add('hidden');
|
||||
widgetButton.classList.remove('open');
|
||||
});
|
||||
// A new frame starts from the site's own defaults, so replay whatever the host configured.
|
||||
if (frameConfigured) {
|
||||
_frame.configure(frameConfiguration);
|
||||
}
|
||||
}
|
||||
return { iframe: widgetIframe, frame: _frame };
|
||||
}
|
||||
|
||||
const GitBook = (...args: StandaloneCalls) => {
|
||||
switch (args[0]) {
|
||||
case 'init':
|
||||
if (_client) {
|
||||
throw new Error(
|
||||
'GitBook client already initialized. Call GitBook("unload") first.'
|
||||
);
|
||||
}
|
||||
case 'init': {
|
||||
// `~gitbook/embed/script.js` already calls `init`, so an integrator following the docs
|
||||
// ends up calling it a second time. Take the new options instead of throwing: throwing
|
||||
// here dropped every call queued behind it (RND-12558).
|
||||
_client = createGitBook(args[1]);
|
||||
frameOptions = args[2];
|
||||
frameOptions = {
|
||||
// Replace rather than merge: a call that leaves out `visitor` — a logout, another
|
||||
// site — must not keep the token from the last one.
|
||||
...args[2],
|
||||
// Except the scheme, where the first one wins: `script.js` passes the site's own
|
||||
// theme when it pins one, and that is not the integrator's to override.
|
||||
colorScheme: frameOptions?.colorScheme ?? args[2]?.colorScheme,
|
||||
};
|
||||
const colorScheme = applyColorScheme();
|
||||
|
||||
// Rebuild the frame only if the new options change its URL — reloading it on the
|
||||
// loader's `init` plus the integrator's would throw away a chat for nothing.
|
||||
const frameURL = _client.getFrameURL({ ...frameOptions, colorScheme });
|
||||
if (widgetIframe && widgetIframe.src !== frameURL) {
|
||||
const wasOpen = !widgetWindow.classList.contains('hidden');
|
||||
widgetIframe.remove();
|
||||
widgetIframe = undefined;
|
||||
_frame = undefined;
|
||||
if (wasOpen) {
|
||||
getIframe();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'unload':
|
||||
_client = undefined;
|
||||
_frame = undefined;
|
||||
widgetIframe?.remove();
|
||||
widgetIframe = undefined;
|
||||
frameOptions = undefined;
|
||||
frameConfigured = false;
|
||||
applyColorScheme();
|
||||
widgetWindow.classList.add('hidden');
|
||||
break;
|
||||
case 'show':
|
||||
@@ -135,27 +225,45 @@ const GitBook = (...args: StandaloneCalls) => {
|
||||
case 'postUserMessage':
|
||||
getIframe().frame.postUserMessage(args[1]);
|
||||
break;
|
||||
case 'configure':
|
||||
case 'configure': {
|
||||
const settings = args[1];
|
||||
|
||||
// If trademark is disabled, change the (branded) icon to the sparkle icon
|
||||
if (
|
||||
settings.trademark === false &&
|
||||
!settings.button?.icon &&
|
||||
frameConfiguration.button.icon === 'assistant'
|
||||
) {
|
||||
settings.button = {
|
||||
label: frameConfiguration.button.label,
|
||||
icon: 'sparkle',
|
||||
};
|
||||
}
|
||||
|
||||
frameConfiguration = {
|
||||
...frameConfiguration,
|
||||
...args[1],
|
||||
...settings,
|
||||
};
|
||||
// Update the button label and icon
|
||||
if (settings.button?.label) {
|
||||
const label = widgetButton.querySelector('#gitbook-widget-button-label');
|
||||
if (label) {
|
||||
label.textContent = settings.button.label;
|
||||
}
|
||||
}
|
||||
if (settings.button?.icon) {
|
||||
const icon = widgetButton.querySelector('#gitbook-widget-button-icon');
|
||||
if (icon) {
|
||||
icon.setAttribute('data-icon', settings.button.icon);
|
||||
}
|
||||
}
|
||||
|
||||
frameConfigured = true;
|
||||
getIframe().frame.configure({
|
||||
...frameConfiguration,
|
||||
buttons: [
|
||||
...frameConfiguration.buttons,
|
||||
|
||||
// Always include a close button
|
||||
{
|
||||
icon: 'close',
|
||||
label: 'Close',
|
||||
onClick: () => {
|
||||
GitBook('close');
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'clearChat':
|
||||
getIframe().frame.clearChat();
|
||||
break;
|
||||
@@ -173,6 +281,11 @@ const precalls = (window.GitBook as GitBookStandalone | undefined)?.q ?? [];
|
||||
|
||||
// @ts-expect-error - GitBook is not defined in the global scope
|
||||
window.GitBook = GitBook;
|
||||
precalls.forEach((call) => GitBook(...call));
|
||||
|
||||
GitBook('configure', {});
|
||||
// Replay each queued call on its own, so one that throws doesn't drop the rest.
|
||||
precalls.forEach((call) => {
|
||||
try {
|
||||
GitBook(...call);
|
||||
} catch (error) {
|
||||
console.error('[gitbook:embed]', error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
--gitbook-widget-top: 1rem;
|
||||
--gitbook-widget-bottom: 1rem;
|
||||
--gitbook-widget-right: 1rem;
|
||||
--gitbook-widget-left: 1rem;
|
||||
|
||||
--gitbook-widget-button-height: 46px;
|
||||
|
||||
@@ -28,6 +29,32 @@
|
||||
--gitbook-widget-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
/* The widget owns the panel's surface, so it renders in the same scheme as the docs inside the
|
||||
* iframe. `data-color-scheme` carries that one resolved scheme — see `resolveColorScheme()` — and
|
||||
* is always set, so the declaration below only shows before the widget initializes (RND-12558).
|
||||
* The colours are declared here rather than on `:root` because `light-dark()` resolves against the
|
||||
* color-scheme of the element that declares them. The bundler lowers it to a variable toggle, so
|
||||
* browsers without `light-dark()` still get both schemes. */
|
||||
#gitbook-widget-button,
|
||||
#gitbook-widget-window {
|
||||
color-scheme: light;
|
||||
|
||||
--gitbook-widget-text-color: light-dark(#656973, #FFFFFF);
|
||||
--gitbook-widget-border-color: light-dark(#e5e5e5, #202020);
|
||||
--gitbook-widget-background-translucent: light-dark(rgba(255, 255, 255, 0.9), rgba(15, 15, 15, 0.9));
|
||||
--gitbook-widget-background-translucent-hover: light-dark(rgba(250, 250, 250, 0.9), rgba(20, 20, 20, 0.9));
|
||||
--gitbook-widget-background-solid: light-dark(#FFFFFF, #0f0f0f);
|
||||
--gitbook-widget-background-solid-hover: light-dark(#FBFBFB, #141414);
|
||||
}
|
||||
#gitbook-widget-button[data-color-scheme="light"],
|
||||
#gitbook-widget-window[data-color-scheme="light"] {
|
||||
color-scheme: light;
|
||||
}
|
||||
#gitbook-widget-button[data-color-scheme="dark"],
|
||||
#gitbook-widget-window[data-color-scheme="dark"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -102,6 +129,18 @@
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
#gitbook-widget-button-icon[data-icon="sparkle"] {
|
||||
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/sparkle.svg?v=2&token=a463935e93");
|
||||
}
|
||||
|
||||
#gitbook-widget-button-icon[data-icon="help"] {
|
||||
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/circle-question.svg?v=2&token=a463935e93");
|
||||
}
|
||||
|
||||
#gitbook-widget-button-icon[data-icon="book"] {
|
||||
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/book-open.svg?v=2&token=a463935e93");
|
||||
}
|
||||
|
||||
#gitbook-widget-button.open #gitbook-widget-button-icon {
|
||||
mask-image: url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/close.svg?v=2&token=a463935e93');
|
||||
}
|
||||
@@ -117,9 +156,10 @@
|
||||
bottom: var(--gitbook-widget-window-bottom);
|
||||
right: var(--gitbook-widget-right);
|
||||
z-index: 9998;
|
||||
width: var(--gitbook-widget-window-width);
|
||||
width: calc(min(var(--gitbook-widget-window-width), calc(100vw - var(--gitbook-widget-right) - var(--gitbook-widget-left))));
|
||||
height: calc(min(var(--gitbook-widget-window-height), calc(100vh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
|
||||
background-color: var(--gitbook-widget-background-solid);
|
||||
background-color: var(--gitbook-widget-background-translucent);
|
||||
backdrop-filter: blur(48px);
|
||||
border: 1px solid var(--gitbook-widget-border-color);
|
||||
border-radius: var(--gitbook-widget-radius);
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"types": []
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: 'src/index.ts',
|
||||
outDir: 'dist',
|
||||
},
|
||||
{
|
||||
entry: 'src/react/index.ts',
|
||||
outDir: 'dist/react',
|
||||
unbundle: true,
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["dist/**", "standalone/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,25 @@
|
||||
# @gitbook/emoji-codepoints
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 57adb3e: Second release to fix publishing with changeset
|
||||
- 57adb3e: Second release to fix publishing with changeset
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5f8a8fe: Initial release
|
||||
- 5f8a8fe: Initial release
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import emojisRaws from 'emoji-assets/emoji.json';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import emojisRaws from 'emoji-assets/emoji.json';
|
||||
|
||||
interface EmojiData {
|
||||
code_points: {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@gitbook/emoji-codepoints",
|
||||
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"exports": "./dist/index.ts",
|
||||
"dependencies": {},
|
||||
"sideEffects": false,
|
||||
"devDependencies": {
|
||||
"emoji-assets": "^8.0.0"
|
||||
"emoji-assets": "^9.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "bun ./build.ts",
|
||||
|
||||
@@ -1,5 +1,70 @@
|
||||
# @gitbook/expr
|
||||
|
||||
## 1.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3fedaaa: Depend on `eval-estree-expression` from the npm registry (`^3.0.1`) instead of a pinned GitHub commit. The published `3.0.1` release is built from the exact commit the package was pinned to, so the code is unchanged — this only removes the fragile git/tarball dependency so consumers install it from npm like any other package.
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 6eabaec: Implement a getVariables function for ExpressionRuntime
|
||||
|
||||
## 1.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8242f18: Extend gitbook/expr std lib with some additional methods
|
||||
|
||||
## 1.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 10995e0: Use NPM Trusted publishing for publishing the package.
|
||||
- 10995e0: Use NPM dependency for eval-estree-expression
|
||||
|
||||
## 1.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
|
||||
## 1.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 1.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a629900: Add dev script for @gitbook/expr
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 6da3655: Fix exports in gitbook/expr package.json
|
||||
|
||||
## 1.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3548fa6: Fix eval-estree-expr named import
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- e1ff17e: Fix bundling of gitbook/expr package
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8ff1e3b: Add support for every/some array methods
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
+29
-12
@@ -1,34 +1,51 @@
|
||||
{
|
||||
"name": "@gitbook/expr",
|
||||
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.3.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"eval-estree-expression": "^2.0.3",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-loose": "8.4.0",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-loose": "^8.5.2",
|
||||
"acorn-walk": "^8.3.4",
|
||||
"assert-never": "^1.2.1"
|
||||
"assert-never": "catalog:",
|
||||
"escodegen": "^2.1.0",
|
||||
"eval-estree-expression": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "^1.1.20",
|
||||
"@tsconfig/strictest": "catalog:",
|
||||
"@tsconfig/node20": "catalog:",
|
||||
"@types/escodegen": "^0.0.10",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@babel/types": "^7.26.0",
|
||||
"@types/json-schema": "^7.0.15"
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"bun-types": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test",
|
||||
"clean": "rm -rf ./dist"
|
||||
"clean": "rm -rf ./dist",
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/GitbookIO/gitbook"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('autocomplete', () => {
|
||||
description: 'An array of string',
|
||||
items: SymbolString(),
|
||||
});
|
||||
const visitorClaimsKeyPropStringSymbol = SymbolString({ name: 'key' });
|
||||
const symbols = {
|
||||
visitor: SymbolObject({
|
||||
name: 'visitor',
|
||||
@@ -33,7 +34,7 @@ describe('autocomplete', () => {
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: SymbolString({ name: 'key' }),
|
||||
key: visitorClaimsKeyPropStringSymbol,
|
||||
flags: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
@@ -57,10 +58,10 @@ describe('autocomplete', () => {
|
||||
}),
|
||||
};
|
||||
const context = new SymbolsTable(symbols);
|
||||
const SCENARIOS: Array<{
|
||||
const SCENARIOS: {
|
||||
expressionWithCursor: string;
|
||||
expectedSuggestions: AutocompleteSuggestions;
|
||||
}> = [
|
||||
}[] = [
|
||||
{
|
||||
expressionWithCursor: 'visit<cur>',
|
||||
expectedSuggestions: [
|
||||
@@ -168,8 +169,12 @@ describe('autocomplete', () => {
|
||||
childrenRefs: [
|
||||
'visitor.claims.key.length',
|
||||
'visitor.claims.key.at',
|
||||
'visitor.claims.key.startsWith',
|
||||
'visitor.claims.key.endsWith',
|
||||
'visitor.claims.key.includes',
|
||||
'visitor.claims.key.toLowerCase',
|
||||
'visitor.claims.key.toUpperCase',
|
||||
'visitor.claims.key.trim',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -210,6 +215,8 @@ describe('autocomplete', () => {
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -225,8 +232,12 @@ describe('autocomplete', () => {
|
||||
childrenRefs: [
|
||||
'visitor.claims.role.length',
|
||||
'visitor.claims.role.at',
|
||||
'visitor.claims.role.startsWith',
|
||||
'visitor.claims.role.endsWith',
|
||||
'visitor.claims.role.includes',
|
||||
'visitor.claims.role.toLowerCase',
|
||||
'visitor.claims.role.toUpperCase',
|
||||
'visitor.claims.role.trim',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -244,13 +255,47 @@ describe('autocomplete', () => {
|
||||
childrenRefs: [
|
||||
'visitor.claims.key.length',
|
||||
'visitor.claims.key.at',
|
||||
'visitor.claims.key.startsWith',
|
||||
'visitor.claims.key.endsWith',
|
||||
'visitor.claims.key.includes',
|
||||
'visitor.claims.key.toLowerCase',
|
||||
'visitor.claims.key.toUpperCase',
|
||||
'visitor.claims.key.trim',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.key.<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolNumber({
|
||||
name: 'length',
|
||||
description:
|
||||
'The length data property of a String value contains the length of the string in UTF-16 code units.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length',
|
||||
}),
|
||||
ref: 'visitor.claims.key.length',
|
||||
parentRef: 'visitor.claims.key',
|
||||
childrenRefs: [],
|
||||
},
|
||||
},
|
||||
...visitorClaimsKeyPropStringSymbol.methods.map<AutocompleteSymbolSuggestion>(
|
||||
(method) => ({
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: method,
|
||||
ref: `visitor.claims.key.${method.name}`,
|
||||
parentRef: 'visitor.claims.key',
|
||||
childrenRefs: [],
|
||||
},
|
||||
})
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.h<cur>',
|
||||
expectedSuggestions: [
|
||||
@@ -268,6 +313,8 @@ describe('autocomplete', () => {
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,11 +42,71 @@ describe('ExpressionRuntime', () => {
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array method',
|
||||
condition: 'reviews.every(review => !!review.status)',
|
||||
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array every',
|
||||
condition: 'reviews.every(review => review.status === "approved")',
|
||||
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array includes',
|
||||
condition: 'reviews.includes("approved")',
|
||||
inputs: { reviews: ['pending', 'approved'] },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array map',
|
||||
condition: '[1, 2, 3].map(n => n * x)',
|
||||
inputs: { x: 2 },
|
||||
expectedResult: [2, 4, 6],
|
||||
},
|
||||
{
|
||||
scenario: 'string startsWith',
|
||||
condition: 'user.role.startsWith("ad")',
|
||||
inputs: { user: { role: 'admin' } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'string endsWith',
|
||||
condition: 'user.role.endsWith("min")',
|
||||
inputs: { user: { role: 'admin' } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'string includes',
|
||||
condition: 'user.role.includes("dm")',
|
||||
inputs: { user: { role: 'admin' } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'string toLowerCase',
|
||||
condition: 'user.role.toLowerCase() === "admin"',
|
||||
inputs: { user: { role: 'ADMIN' } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'string toUpperCase',
|
||||
condition: 'user.role.toUpperCase() === "ADMIN"',
|
||||
inputs: { user: { role: 'admin' } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'string trim',
|
||||
condition: 'user.role.trim() === "admin"',
|
||||
inputs: { user: { role: ' admin ' } },
|
||||
expectedResult: true,
|
||||
},
|
||||
])(
|
||||
'should properly evaluate/safeEvaluate a valid conditional expression: $scenario',
|
||||
({ condition, inputs, expectedResult }) => {
|
||||
expect(runtime.evaluate(condition, inputs)).toBe(expectedResult);
|
||||
expect(runtime.safeEvaluate(condition, inputs).value).toBe(expectedResult);
|
||||
expect(runtime.evaluate(condition, inputs)).toEqual(expectedResult);
|
||||
expect(runtime.safeEvaluate(condition, inputs).value).toEqual(expectedResult);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -135,6 +195,49 @@ describe('ExpressionRuntime', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('getVariables', () => {
|
||||
it.each([
|
||||
{
|
||||
scenario: 'single variable',
|
||||
condition: 'isBetaUser === true',
|
||||
expectedVariables: ['isBetaUser'],
|
||||
},
|
||||
{
|
||||
scenario: 'multiple variables',
|
||||
condition: 'useProductA && !isBetaUser',
|
||||
expectedVariables: ['useProductA', 'isBetaUser'],
|
||||
},
|
||||
{
|
||||
scenario: 'member expression',
|
||||
condition: 'user.role === "admin"',
|
||||
expectedVariables: ['user.role'],
|
||||
},
|
||||
{
|
||||
scenario: 'nested member expression with method call',
|
||||
condition: 'products.includes("productA") && userSegments.alpha',
|
||||
expectedVariables: ['products.includes', 'userSegments.alpha'],
|
||||
},
|
||||
])(
|
||||
'should return variables used in expression: $scenario',
|
||||
({ condition, expectedVariables }) => {
|
||||
expect(runtime.getVariables(condition)).toEqual(expectedVariables);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
scenario: 'invalid syntax',
|
||||
condition: 't}=d',
|
||||
},
|
||||
{
|
||||
scenario: 'non conditional expression',
|
||||
condition: 'const a = 1;',
|
||||
},
|
||||
])('should return an empty array for invalid expressions: $scenario', ({ condition }) => {
|
||||
expect(runtime.getVariables(condition)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('generate', () => {
|
||||
it.each([
|
||||
{
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
} from 'acorn';
|
||||
import { isDummy } from 'acorn-loose';
|
||||
import * as walk from 'acorn-walk';
|
||||
|
||||
import assertNever from 'assert-never';
|
||||
|
||||
import { type ExtractSymbolDef, SymbolType, SymbolsTable } from './symbols';
|
||||
@@ -312,7 +311,7 @@ export class AutoComplete {
|
||||
node: BinaryExpression,
|
||||
cursorOffset: number,
|
||||
context: SymbolsTable
|
||||
): Array<AutocompleteLiteralValueSuggestion> {
|
||||
): AutocompleteLiteralValueSuggestion[] {
|
||||
const { left, right } = node;
|
||||
|
||||
if (left.type !== 'MemberExpression' && left.type !== 'Identifier') {
|
||||
@@ -431,7 +430,7 @@ export class AutoComplete {
|
||||
node: AnyNode,
|
||||
cursorOffset: number,
|
||||
context: SymbolsTable
|
||||
): Array<AutocompleteOperatorSuggestion> {
|
||||
): AutocompleteOperatorSuggestion[] {
|
||||
if (node.type === 'Literal') {
|
||||
const parent = findParentNode(node, ast);
|
||||
|
||||
@@ -496,7 +495,7 @@ export class AutoComplete {
|
||||
*/
|
||||
private getOperatorSuggestionsForSymbol(
|
||||
symbol: ExtractSymbolDef<SymbolType>
|
||||
): Array<AutocompleteOperatorSuggestion> {
|
||||
): AutocompleteOperatorSuggestion[] {
|
||||
switch (symbol.type) {
|
||||
case SymbolType.Number:
|
||||
case SymbolType.Boolean:
|
||||
@@ -526,7 +525,7 @@ export class AutoComplete {
|
||||
case SymbolType.Function:
|
||||
return this.getOperatorSuggestionsForSymbol(symbol.returns);
|
||||
case SymbolType.Union: {
|
||||
return symbol.members.reduce<Array<AutocompleteOperatorSuggestion>>((prev, cur) => {
|
||||
return symbol.members.reduce<AutocompleteOperatorSuggestion[]>((prev, cur) => {
|
||||
prev.push(...this.getOperatorSuggestionsForSymbol(cur));
|
||||
return prev;
|
||||
}, []);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './errors';
|
||||
export * from './input-values';
|
||||
export * from './input-values';
|
||||
export * from './runtime';
|
||||
export * from './symbols';
|
||||
export * from './template';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
|
||||
import { filterOutNullable } from './utils';
|
||||
|
||||
type InputValuesType =
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
tokenizer,
|
||||
} from 'acorn';
|
||||
import { parse as parseLoose } from 'acorn-loose';
|
||||
import { evaluate } from 'eval-estree-expression';
|
||||
import escodegen from 'escodegen';
|
||||
import evalESTreeExpr from 'eval-estree-expression';
|
||||
const { evaluate, variables } = evalESTreeExpr;
|
||||
|
||||
import { AutoComplete } from './autocomplete';
|
||||
import { ExpressionError } from './errors';
|
||||
@@ -49,6 +51,7 @@ export class ExpressionRuntime {
|
||||
return evaluate.sync<Expression>(parsed.result, inputs, {
|
||||
functions: true,
|
||||
withMembers: true,
|
||||
generate: escodegen.generate,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error instanceof Error
|
||||
@@ -159,6 +162,28 @@ export class ExpressionRuntime {
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an expression, returns a list of variables used in the expression.
|
||||
*/
|
||||
public getVariables(expr: string): string[] {
|
||||
try {
|
||||
const parsed = this.parse(expr);
|
||||
|
||||
if (parsed.invalidNodes.length > 0) {
|
||||
throw new ExpressionError('Invalid nodes found when parsing');
|
||||
}
|
||||
|
||||
return variables(parsed.result, {
|
||||
functions: true,
|
||||
withMembers: true,
|
||||
generate: escodegen.generate,
|
||||
});
|
||||
} catch (error) {
|
||||
this.#logger.error(`Error while parsing expression ${expr} to get variables`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a binary expression and returns an @ExpressionParserResult.
|
||||
*/
|
||||
|
||||
@@ -255,8 +255,12 @@ describe('ExpressionRuntime', () => {
|
||||
childrenRefs: [
|
||||
'visitor.claims.key.length',
|
||||
'visitor.claims.key.at',
|
||||
'visitor.claims.key.startsWith',
|
||||
'visitor.claims.key.endsWith',
|
||||
'visitor.claims.key.includes',
|
||||
'visitor.claims.key.toLowerCase',
|
||||
'visitor.claims.key.toUpperCase',
|
||||
'visitor.claims.key.trim',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -301,8 +305,12 @@ describe('ExpressionRuntime', () => {
|
||||
childrenRefs: [
|
||||
'visitor.claims.flags.FLAG1.length',
|
||||
'visitor.claims.flags.FLAG1.at',
|
||||
'visitor.claims.flags.FLAG1.startsWith',
|
||||
'visitor.claims.flags.FLAG1.endsWith',
|
||||
'visitor.claims.flags.FLAG1.includes',
|
||||
'visitor.claims.flags.FLAG1.toLowerCase',
|
||||
'visitor.claims.flags.FLAG1.toUpperCase',
|
||||
'visitor.claims.flags.FLAG1.trim',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -318,8 +326,12 @@ describe('ExpressionRuntime', () => {
|
||||
childrenRefs: [
|
||||
'visitor.claims.flags.FLAG2.length',
|
||||
'visitor.claims.flags.FLAG2.at',
|
||||
'visitor.claims.flags.FLAG2.startsWith',
|
||||
'visitor.claims.flags.FLAG2.endsWith',
|
||||
'visitor.claims.flags.FLAG2.includes',
|
||||
'visitor.claims.flags.FLAG2.toLowerCase',
|
||||
'visitor.claims.flags.FLAG2.toUpperCase',
|
||||
'visitor.claims.flags.FLAG2.trim',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -338,6 +350,8 @@ describe('ExpressionRuntime', () => {
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,6 +156,31 @@ const StandardLibrary: Partial<
|
||||
members: [SymbolString(), SymbolUndefined()],
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'startsWith',
|
||||
description: `Returns true if the given characters are found at the beginning of the string, including when searchString
|
||||
is an empty string. Otherwise returns false.`,
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith',
|
||||
args: [
|
||||
SymbolString({
|
||||
name: 'searchString',
|
||||
description: `The characters to be searched for at the start of this string. Cannot be a regex.
|
||||
All values that are not regexes are coerced to strings, so omitting it or passing undefined causes startsWith() to search for
|
||||
the string "undefined", which is rarely what you want.`,
|
||||
}),
|
||||
OptionalFunctionArg(
|
||||
SymbolNumber({
|
||||
name: 'position',
|
||||
description: `The start position at which searchString is expected to be found
|
||||
(the index of searchString's first character). Defaults to 0.`,
|
||||
})
|
||||
),
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description: `true if the given characters are found at the beginning of the string, including when searchString is an empty string;
|
||||
otherwise, false.`,
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'endsWith',
|
||||
description: `Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding
|
||||
@@ -205,6 +230,39 @@ const StandardLibrary: Partial<
|
||||
otherwise, false.`,
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'toLowerCase',
|
||||
description:
|
||||
'Returns the value of the string converted to lower case. toLowerCase() does not affect the value of the string str itself.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase',
|
||||
args: [],
|
||||
returns: SymbolString({
|
||||
description:
|
||||
'A new string representing the calling string converted to lower case.',
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'toUpperCase',
|
||||
description:
|
||||
'Returns the value of the string converted to uppercase. toUpperCase() does not affect the value of the string str itself.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase',
|
||||
args: [],
|
||||
returns: SymbolString({
|
||||
description:
|
||||
'A new string representing the calling string converted to upper case.',
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'trim',
|
||||
description:
|
||||
'Returns new string representing str stripped of whitespace from both its beginning and end.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim',
|
||||
args: [],
|
||||
returns: SymbolString({
|
||||
description:
|
||||
'A new string representing str stripped of whitespace from both its beginning and end.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
[SymbolType.Array]: (arraySymbolDef: ArraySymbolDef) => ({
|
||||
@@ -259,6 +317,60 @@ const StandardLibrary: Partial<
|
||||
'true if the value searchElement is found within the array (or the part of the array indicated by the index fromIndex, if specified).',
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'some',
|
||||
description:
|
||||
'Tests whether at least one element in the array passes the test implemented by the provided function.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some',
|
||||
args: [
|
||||
SymbolFunction({
|
||||
name: 'callback',
|
||||
description: 'A function that tests each element of the array.',
|
||||
args: [
|
||||
{
|
||||
...arraySymbolDef.items,
|
||||
name: 'element',
|
||||
description: 'The current element being processed in the array.',
|
||||
},
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for at least one element in the array.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for at least one element in the array.',
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'every',
|
||||
description:
|
||||
'Tests whether all elements in the array pass the test implemented by the provided function.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every',
|
||||
args: [
|
||||
SymbolFunction({
|
||||
name: 'callback',
|
||||
description: 'A function that tests each element of the array.',
|
||||
args: [
|
||||
{
|
||||
...arraySymbolDef.items,
|
||||
name: 'element',
|
||||
description: 'The current element being processed in the array.',
|
||||
},
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for all elements in the array.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for all elements in the array.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface ExpressionParserResult {
|
||||
/**
|
||||
* The information of the invalid (non-expression) nodes found from the other portions of the parsed expression.
|
||||
*/
|
||||
invalidNodes: Array<ExpressionStatement>;
|
||||
invalidNodes: ExpressionStatement[];
|
||||
}
|
||||
|
||||
export interface ExpressionAutocompleteResults {
|
||||
@@ -158,11 +158,11 @@ export interface AutocompleteSymbolSuggestion {
|
||||
symbol: SymbolInfo;
|
||||
}
|
||||
|
||||
export type AutocompleteSuggestions = Array<
|
||||
export type AutocompleteSuggestions = (
|
||||
| AutocompleteSymbolSuggestion
|
||||
| AutocompleteLiteralValueSuggestion
|
||||
| AutocompleteOperatorSuggestion
|
||||
>;
|
||||
)[];
|
||||
|
||||
type LoggerFn = (message: string, ...args: any[]) => void;
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": ["./tsconfig.json"],
|
||||
"exclude": ["**/*.test.ts"],
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"noEmit": false,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -14,7 +14,7 @@ declare module 'eval-estree-expression' {
|
||||
/**
|
||||
* Enable support for function statements and expressions by enabling the functions option AND by passing the .generate() function from the escodegen library. Default: undefined
|
||||
*/
|
||||
generate?: boolean;
|
||||
generate?: boolean | ((node: any) => string);
|
||||
/**
|
||||
* Enable the =~ regex operator to support testing values without using functions (example name =~ /^a.*c$/). Default: true
|
||||
*/
|
||||
@@ -41,6 +41,16 @@ declare module 'eval-estree-expression' {
|
||||
options?: EvalESTreeExpressionOptions
|
||||
): Promise<any>;
|
||||
|
||||
/**
|
||||
* Given an ESTree-compliant AST node, returns a list of variables used in the expression.
|
||||
* @param ast An object representing an ESTree-compliant AST node.
|
||||
* @param options Options for evaluation and compilation.
|
||||
*/
|
||||
export function variables<ASTNode>(
|
||||
ast: ASTNode,
|
||||
options?: EvalESTreeExpressionOptions
|
||||
): string[];
|
||||
|
||||
/**
|
||||
* Evaluates an ESTree expression synchronously against a given context.
|
||||
* @param expression - An object representing an ESTree-compliant AST node.
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @gitbook/fonts
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 10995e0: Use NPM Trusted publishing for publishing the package.
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { APIv2 } from 'google-font-metadata';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { APIv2 } from 'google-font-metadata';
|
||||
|
||||
import { CustomizationDefaultFont } from '@gitbook/api';
|
||||
|
||||
import type { FontDefinitions } from '../src/types';
|
||||
|
||||
@@ -4,28 +4,43 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"sideEffects": false,
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"google-font-metadata": "^6.0.3",
|
||||
"typescript": "^5.5.3"
|
||||
"bun-types": "catalog:",
|
||||
"google-font-metadata": "^6.0.7",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "bun ./bin/generate.js",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w",
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"clean": "rm -rf ./dist && rm -rf ./src/data",
|
||||
"unit": "bun test"
|
||||
"unit": "bun test",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
|
||||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/GitbookIO/gitbook"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`getDefaultFont should return correct object for Latin text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should return correct object for Cyrillic text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should return correct object for Greek text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle mixed script text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different font weights: regular 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different font weights: bold 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different fonts: inter 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different fonts: roboto 1`] = `
|
||||
{
|
||||
"font": "Roboto",
|
||||
"url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff",
|
||||
"url": "https://fonts.gstatic.com/s/roboto/v49/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAz0klQm_20.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import rawFonts from './data/fonts.json' with { type: 'json' };
|
||||
import type { FontDefinitions } from './types';
|
||||
|
||||
import rawFonts from './data/fonts.json' with { type: 'json' };
|
||||
|
||||
export const fonts: FontDefinitions = rawFonts;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { CustomizationDefaultFont } from '@gitbook/api';
|
||||
|
||||
import { getDefaultFont } from './getDefaultFont';
|
||||
|
||||
describe('getDefaultFont', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CustomizationDefaultFont } from '@gitbook/api';
|
||||
|
||||
import { fonts } from './fonts';
|
||||
import type { FontWeight } from './types';
|
||||
|
||||
@@ -67,7 +68,7 @@ function getBestUnicodeRange(text: string, ranges: Record<string, string>): stri
|
||||
|
||||
const body = token.slice(2); // drop "U+"
|
||||
const [startHex, endHex] = body.split('-');
|
||||
const start = Number.parseInt(startHex, 16);
|
||||
const start = Number.parseInt(startHex!, 16);
|
||||
const end = endHex ? Number.parseInt(endHex, 16) : start;
|
||||
|
||||
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
|
||||
@@ -86,13 +87,13 @@ function getBestUnicodeRange(text: string, ranges: Record<string, string>): stri
|
||||
// ---------- tally code-point hits ----------
|
||||
const hits: Record<string, number> = Object.fromEntries(Object.keys(parsed).map((k) => [k, 0]));
|
||||
|
||||
for (let i = 0; i < text.length; ) {
|
||||
for (let i = 0; i < text.length;) {
|
||||
const cp = text.codePointAt(i)!;
|
||||
i += cp > 0xffff ? 2 : 1; // advance by 1 UTF-16 code-unit (or 2 for surrogates)
|
||||
|
||||
for (const [label, rangesArr] of Object.entries(parsed)) {
|
||||
if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
|
||||
hits[label]++;
|
||||
hits[label]!++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"module": "ESNext",
|
||||
"target": "es2022",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
@@ -1,5 +1,752 @@
|
||||
# gitbook
|
||||
|
||||
## 0.28.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 3d37684: Add a carousel layout option to cards blocks, rendering them as a horizontally-scrolling, scroll-snapping row instead of a wrapping grid.
|
||||
- 523b7cd: Support external links in published site navigation.
|
||||
- 89c4a0f: Add an `askQuestion` tool to the site MCP server. Alongside `searchDocumentation` and `getPage`, MCP clients can now ask a natural-language question and get a synthesized answer with links to the source pages, powered by the same AI search backend as the site's "ask a question" experience. The tool accepts an optional `goal` param so calling agents can attach the intent they're trying to accomplish, which tailors the answer and is tracked in analytics. The tool is only exposed on sites that have AI enabled.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 048c4e4: Fix "View activity" disclosure in AI Chat not opening after the Base UI migration.
|
||||
- 996d7ec: Scroll to an in-page heading even when the URL hash is unchanged (e.g. clicking the same anchor again).
|
||||
- fa9c9b3: Force software rendering (SwiftShader) in Playwright Chromium to eliminate image downscaling drift between GPU-equipped local runs and headless CI runs in Argos screenshots.
|
||||
- d9fc460: Stop showing previously asked questions as suggestions in the GitBook Assistant
|
||||
- 1ee1853: Add an assistant tool to rate its own previous response when the user reacts to it.
|
||||
- cef1870: Add an assistant tool to submit feedback about the current page on behalf of the user.
|
||||
- 49d35aa: Assistant: you can now send follow-up questions while an answer is still being written. Each one appears as your own message with a "Queued" badge (hover for when it will send, × to cancel), and they're sent automatically one at a time as each answer completes.
|
||||
- 8ad7465: Simplify carousel overflow with symmetric edge masks and visible-item paging. Replaces complex negative-margin bleed logic with transparent edge fades and page-by-visible-item scrolling.
|
||||
- b3db1c5: Keep the breadcrumbs from covering the page actions' hit area.
|
||||
- 9b822ab: Include published page descriptions in the page's Markdown output.
|
||||
- 64e143a: Bump Next.js to 16.3.3.
|
||||
- c08d05a: Fix select filters not working on table and cards blocks.
|
||||
- a566153: Chunk oversized visitor auth cookies to fix an infinite redirect loop when the visitor token exceeds the browser cookie size limit.
|
||||
- 1b7ac0e: Fix inline Ask AI buttons opening a configured custom assistant.
|
||||
- ae9367d: Assistant: the "Explored briefly" activity heading no longer appears when there's nothing to show. It now renders only when the answer is preceded by a real preamble or one or more tool calls, so a simple answer with an empty reasoning step no longer surfaces an empty collapsible.
|
||||
- 688515e: Fix ContentKit buttons to size to their content instead of stretching to the full container width.
|
||||
- e041e49: Stop the built-in cookie banner from flashing on sites using a consent integration such as Osano or OneTrust.
|
||||
- 5f88f4e: Fix breadcrumbs for cross-space page links in grouped sites.
|
||||
- 1ef7160: Show the theme toggle in the footer whenever the outline column that hosts the other toggle isn't pinned open, so it stays reachable on laptop-sized screens in wide layouts and while the AI chat is open.
|
||||
- 3939125: Disable click-to-zoom for inline line sized images.
|
||||
- 177ef85: Fix host action buttons in the Docs Embed not reaching the assistant.
|
||||
- 4e9071d: Fix the docs embed `navigateToPage` API on multi-space sites. Deep-linking to a page in a different space/section (e.g. `navigateToPage('/help-center/integrations')`) previously 404'd because the section base was not placed before `~gitbook/embed/page`. The target is now resolved to its space server-side, so pages in any space resolve correctly. The input accepts the page path, an absolute path, or the full published URL.
|
||||
- 94a496c: Fix the Docs Embed widget rendering with a dark surface when the visitor's OS is in dark mode, even on a light page or a site published with a light theme. The embed script now accepts `?theme=light|dark` to force a scheme.
|
||||
- 9002f65: Fix suggested question clicks in the embed search not opening the assistant.
|
||||
- 13059b8: Fix switching from the Search tab to the Docs or Assistant tab in the embed doing nothing.
|
||||
- b5f3c14: Add configurable default visibility to Prompt block
|
||||
- a69a307: A near-white tint color (e.g. a warm `#F5F3EF`) is now taken as the exact page background, mirroring the existing behavior for near-black tints. The tint's exact lightness, hue and chroma are preserved, and the color is anchored to whichever scale step the active theme renders as the background — so it matches exactly on `muted` (which uses the second step) as well as `clean`. This applies only to near-neutral tints that are light enough to read as a background; saturated or merely light-ish colors keep their normal accent scale. The `bold` theme is unaffected: it already uses the tint for the header and stays intentionally two-tone.
|
||||
- f9ad9b5: Introduce client-side content selection (`select`): a site-wide, recency-ordered list of selected slugs, persisted in localStorage and shareable via `?select=`, applied to `<html>` before first paint so the right variant renders with no flash. All variants stay server-rendered, so pages are byte-identical for every visitor (no cache impact).
|
||||
|
||||
Tabs now use it: switching a tab activates its slug, and every tab group offering that slug follows, across pages. Tabs no longer write to the URL fragment (`#` returns to anchors only); deep-links into a tab still activate and scroll to it.
|
||||
|
||||
- 03bbacf: Add missing link reference to OpenAPI models
|
||||
- 530f98a: Skip internal paths (`~gitbook/*`, `.well-known/oauth-protected-resource`, `llms.txt`, `robots.txt`, `sitemap.xml`, `rss.xml`) when building URL lookup alternatives, to avoid resolving URLs that can never match content.
|
||||
- cc441c9: Keep published search results within the selected site-space scope during local and remote result fusion.
|
||||
- 4452fd7: Fix page link titles showing the site section name instead of the target space title.
|
||||
- d0a63ba: Update the URL hash when a tab is selected, so a copied link scrolls back to that tab
|
||||
- 7bab574: Self-host default Google fonts and inline only the @font-face rules of the fonts a site uses, instead of shipping render-blocking stylesheets covering all 23 families on every page.
|
||||
- 6928a9b: Hide card fields that render no content, along with their title
|
||||
- c41cf9c: Add cover image background mode and masks
|
||||
- 56c2558: Upgrade react-hotkeys-hook to v5 and use its native `useKey` option so keyboard shortcuts match the produced key on non-QWERTY layouts.
|
||||
- 39156ee: Button blocks now respect the `size` option, so you can render small, medium, or large buttons.
|
||||
- bf674a4: Add an optional `context` property (string, up to 512 characters) to the `confirmation` of custom AI tools, shown above the confirmation dialog to help the user understand what they are approving or rejecting. The `confirmation` can now also be a function that receives the AI-provided input and returns the confirmation, so the context can be derived dynamically from the arguments the tool is about to run with. Available both to integrations (`GitBookIntegrationTool`) and to embed consumers (`GitBookToolDefinition`).
|
||||
- c1b5be0: Keep embed demos and frames on the deployment that served them.
|
||||
- b13fd91: Fix a hydration mismatch on every page load caused by the AI chat time-based greeting being computed in the server timezone.
|
||||
- 2ccd43e: Lazy-load the Mermaid code block so pages that have code blocks but no diagram no longer ship its rendering dependencies.
|
||||
- 65f99ea: Lazy load the Scalar API client modal and stop preloading the Scalar runtime. The modal is now code-split into its own chunk, fetched in parallel with the runtime only when a reader clicks "Test it", and a spinner is shown on the button until the client opens.
|
||||
|
||||
Breaking: the package no longer ships the modal in its main entry — consumers must serve the emitted `ScalarApiModal` chunk and use a bundler that supports dynamic `import()`, and the Scalar runtime is no longer preloaded on page load. The internal `preloadScalarRuntime` helper is removed.
|
||||
|
||||
- 810244e: Stop preloading the zoom-modal variant of every zoomable image at render time; it downloaded each image twice during the initial page load. The modal image still loads on hover or click.
|
||||
- 75bdff3: Serve an indexable `X-Robots-Tag` on markdown pages requested by AI agents
|
||||
- 2ff77e1: Bump `@gitbook/api` to 0.199.0, and record a markdown request made from the page actions menu as a page action rather than an agent request.
|
||||
- d7c867a: Fix the page-actions dropdown closing before the "Copied" confirmation could be shown when copying the MCP server URL, an MCP install command, or the page as Markdown.
|
||||
- ccb9d7b: Submit `sendFeedback` MCP tool findings through the dedicated `submitSiteAgentFeedback` API endpoint. The `pageUrl` is now required and an optional `goal` can be provided.
|
||||
- 01c9b05: Mermaid diagrams no longer hijack page scrolling: zooming with the wheel now requires holding Ctrl/Cmd inline, while the fullscreen view keeps free wheel zoom.
|
||||
- b3db1c5: Migrate the headless UI primitives from Radix and react-aria to Base UI.
|
||||
- a664407: Persist content selection (tabs and other `select` blocks) in localStorage only, dropping the `?select=` query parameter from the URL. A tab click still writes the tab's hash, so a copied URL lands on that tab and reactivates it on load.
|
||||
- 14562e9: Serve `X-Robots-Tag: noindex` on internal search/assistant URLs (`?q=` / `?ask=`) and stop disallowing them in robots.txt, so Google can crawl the directive and drop them from the index instead of reporting "Indexed, though blocked by robots.txt".
|
||||
- 90b5468: Redesign the site OAuth consent screen for published sites MCP and translate its strings
|
||||
- 40a879a: Drop a column instead of shrinking them all when a section group dropdown runs out of room
|
||||
- a9c5d1b: Let a section group dropdown scroll when it is taller than the screen
|
||||
- 827e4f9: Fix the variant switcher linking to the wrong URL for non-default variants of the default section
|
||||
- 35be383: Skip the rendering work for off-screen OpenAPI blocks, so pages with many operations stay smooth to scroll.
|
||||
- db176ba: Add an "On this page" table of contents on OpenAPI models pages. Each model in a grouped/multi-model "Models" section is now listed as its own section, matching operations and webhooks.
|
||||
- 332089e: Improve cookie handling in the OpenAPI "Test it" request proxy.
|
||||
- a80d412: Serve the OpenAPI "Test it" request proxy from GitBook's own domain.
|
||||
- 1424c56: Keep the OpenAPI renderer out of the initial bundle of pages that have no OpenAPI block, by building its context on the client behind a dynamic boundary.
|
||||
- e4b214e: Fix OpenAPI webhook payload and schema example panels being clipped instead of scrollable.
|
||||
- 7009bb9: Move paragraph block styles behind a single `paragraph` class and drop the `page-cover-background:` gate from the cover-contrast text. The gate combined with the per-paragraph `:not(:has(...))` made every DOM insertion re-style all paragraphs, which froze very long pages.
|
||||
- 40150f0: Serve Markdown responses to ChatGPT with a `text/plain` Content-Type for compatibility.
|
||||
- e460ec3: Preserve external page destinations returned by published search.
|
||||
- 148a43a: Use one canonical backend-ranked result set for published searches across site sections.
|
||||
- 5da854f: Preserve canonical backend ranking and present page or section context that matches each published search destination.
|
||||
- 78c589f: Align the Previous page navigation button to the left edge and the Next button to the right edge, including when only one of them is present.
|
||||
- b9453a9: Preserve the full site preview path and query parameters when redirecting users to log in.
|
||||
- 8e87856: Fix pages resolving to "not found" when a root URL lookup resolves to a custom homepage, by no longer using the homepage pathname as a prefix for the requested page path.
|
||||
- 1faa57b: Keep current-space search results inside revision previews.
|
||||
- 7543eb8: Prewarm published search caches when readers open the search interface.
|
||||
- 185dd83: Inject site tracking scripts (analytics integrations) after `load` + idle instead of preloading them and executing them during the critical loading window.
|
||||
- 77058e3: Render horizontal and vertical merged table cells on published pages.
|
||||
- 634a24f: Fix text disappearing in Firefox and iOS Safari on pages with a background cover
|
||||
- 64ce9e1: Render a site-space custom home page at its placement root while preserving the full space and normal page URLs.
|
||||
- 81dba64: Resolve stable page, space and file refs in images, definitions and HTML blocks of published markdown pages, instead of leaking internal `/pages/{id}`, `/spaces/{id}` and `/files/{id}` URLs.
|
||||
- da7fb13: Fail closed in `/~gitbook/revalidate` when `GITBOOK_SECRET` is not configured, returning `403 Revalidation is disabled` instead of skipping the signature check, consistent with `force-revalidate`.
|
||||
- a9a52fe: Remove GBO's redundant re-selection of the best-scoring search section. The search API now returns a single highest-scoring section per page (and orders sections highest-score-first), so GBO no longer needs its own `getBestScoredResult` helper to pick the best section for the search and MCP previews. No user-visible change.
|
||||
- 195c9e6: Fix the docs embed not applying `?theme=light`/`?theme=dark`. The theme was dropped when the embed redirected to its default tab, and the embed tabs couldn't read it because they render statically (their request headers are empty). The middleware now threads a forced embed theme through the embed route context (scoped to the embed, not the main site), so the embed tabs honor it while staying statically rendered, and the redirect forwards it to the default tab. The forced theme is also persisted to the embed's own theme storage so it is remembered across tab navigation instead of only applying while the query string is present.
|
||||
- 597fe34: Sync the API reference responses selector with the "Responses" collapsibles, and keep the selected response in sync across every operation on the page (like the code sample language selector). Selecting a status code now expands the matching response section and applies to all operations at once.
|
||||
- e14609c: Fix a light/dark flash on published sites configured to respect the system default (no theme toggle). Such sites forced the `system` theme, but `next-themes`' pre-paint script applies a forced value verbatim without resolving `prefers-color-scheme`, so the page painted light and only switched to dark after hydration. We now leave the theme unforced when the default is `system` (only concrete light/dark themes are forced), letting `next-themes`' existing pre-paint script resolve the system preference before first paint.
|
||||
- 98b2df4: Add a `sendFeedback` MCP tool so AI agents can report documentation findings (outdated / incoherent / gap / other) as `agent_feedback` insights events. The tool only accepts finding categories, so it never records positive feedback.
|
||||
- 8e9a49d: Separate the prompt block actions into a primary "Open in" dropdown and a secondary "Copy prompt" button, instead of a single combined button group, and align the block's design with the expandable block (bordered frame, left disclosure chevron, and subtle elevation when expanded).
|
||||
- cf4efc7: Fix the search field losing focus if it was focused just before the page finished hydrating.
|
||||
- e73b182: Fix the docs embed widget shipping a stale script: declare the embed package's `standalone/` bundle as a Turbo build output. Because it wasn't declared, changes confined to the standalone widget (which compiles to `standalone/` but not `dist/`) didn't invalidate the downstream `generate` cache that copies it into the app, so the deployed widget could lag the source — e.g. the `clipboard-write` permission on the widget iframe never reached production, breaking the copy button in the Assistant embed.
|
||||
- 6d02b8a: Fix code block syntax highlighting so comment delimiters (e.g. `//`, `/*`) use the same color as the rest of the comment. Previously the delimiter fell through to the generic punctuation scope, making it a different color from the comment body (most visible in dark mode).
|
||||
- bcd1c41: Scroll to the top when selecting a search result for the page already being viewed.
|
||||
- 1f250d1: Add end-to-end coverage for root and nested external links in site section navigation.
|
||||
- 8a0e0df: Fix section links in search results opening the page without scrolling to the section.
|
||||
- 484cc11: Fix ScrollContainer scroll buttons not reflecting content overflow immediately or after dynamic content changes (e.g. search results).
|
||||
- c1b5be0: Allow published search to be scoped to any visible site section.
|
||||
- d33e570: Fix the spacebar being ignored in the search bar, which made multi-word queries impossible.
|
||||
- 0a82628: Keep keyboard focus within search while the results are open.
|
||||
- 5889642: Keep the last search query visible after closing search, and restore it when reopening, without breaking navigation when clicking a search result.
|
||||
- 3db14ad: Link page-level search matches to the top of the page while preserving section anchors for section matches.
|
||||
- 0dd2f4f: Show the containing section name when hovering a direct link to a space.
|
||||
- 8c10d92: Fix the section group menu dismissing while the mouse is still inside it, when the collapsed sidebar rail overlaps the menu on no-sidebar pages.
|
||||
- ded2f56: Show loose sections in a section group dropdown as secondary links unless the group starts with one
|
||||
- 0ff21b7: Stop the section tabs in the header from showing a scroll button and faded edge when all tabs already fit.
|
||||
- cd506e3: Keep Shiki out of the initial page bundle by splitting the highlighter from the plain-text token helpers, so client code no longer pulls the engine and language bundles in through a shared import.
|
||||
- ad3399b: Refine the per-paragraph AI ask button: shorten its tooltip to "Ask" (from "Ask <assistant> about this"), and hide it inside cards where it would otherwise be clipped by the card's overflow.
|
||||
- 931cbe7: Use cached Git metadata (via @gitbook/api 0.201.0's `cachedMetadata` param) when rendering Edit on Git actions.
|
||||
- e0bd04f: Render headings with the site's heading font when one is configured, falling back to the main font otherwise.
|
||||
- bf29570: Fix the tabs "more" dropdown showing when no tab is overflowing, and stop a tab click re-rendering every tab group on the page.
|
||||
- 703e654: Split the default-scope site search into two parallel API requests — one restricted to the current site space and one for the other site spaces — rendering each result set as soon as its response arrives. All results are ranked together by score, with the current site space scores boosted.
|
||||
- 57f3077: Keep the "On this page" and "Ask" buttons pinned below the header while scrolling on desktop API reference pages, so the page outline stays reachable throughout long operations.
|
||||
- 8676ad1: Hide unfocusable unlabelled button from screen readers
|
||||
- 6cf4278: Only show the "Back to [space]" shortcut for cross-space links in the table of contents, not for in-content text links or other ways of reaching another space.
|
||||
- 7594a33: Show image thumbnails, including SVG previews, for file attachments on published sites.
|
||||
- 5c74eef: Make the table search "no results" empty state more prominent with vertical spacing so it no longer blends into the content below.
|
||||
- 038008c: Fix heading anchor links being unreachable on touch devices by adding a tap-to-reveal state. The anchor icon now appears after the heading text without wrapping onto an orphan line while retaining its existing desktop placement. Use `pointerup` for the dismiss listener to fix unreliable dismissal on iOS Safari, and enlarge the anchor's touch tap target to a square 24px area (meeting the WCAG 2.5.8 minimum) so the icon stays centered instead of overflowing shorter headings' line height.
|
||||
- a9a1a60: Fix search results from a previous scope staying stuck on top of the new results when switching the search filter.
|
||||
- 46b3a18: Fix tabs nested inside another tab group rendering an empty body once a tab in the outer group was selected.
|
||||
- 8a6baab: Load the admin toolbar and its CSS lazily so published pages no longer ship a render-blocking stylesheet for admin-only UI.
|
||||
- a9c5d1b: Remove the gap between the columns of a section group dropdown holding a large group
|
||||
- a9041a3: Remove the fallback query parameter after successful page navigation without adding a browser history entry.
|
||||
- 931cbe7: Restore Edit on Git page actions for Git-synced pages.
|
||||
- ea19801: Support localized custom AI Assistant greeting subtitles.
|
||||
- 1dccf8f: Fix the first item of a table-of-contents page group appearing cut off (faded under the group header) after client-side navigation.
|
||||
- 48fba7c: Highlight a table-of-contents link entry as active when it points to the page — or the section of a page — you are currently viewing.
|
||||
- 048c4e4: Fix chevrons and open-state styling incorrectly reacting to a tooltip opening on the same trigger, by switching from the shared Base UI `data-popup-open` attribute to `aria-expanded`.
|
||||
- f3408ed: a11y screen reader fixes
|
||||
- 3bf55ce: Support query params in the `@webframe.navigate` action.
|
||||
- cb92754: Let integration block webframes navigate the reader to another page in the site by posting a `@webframe.navigate` action with a `path` (and optional `anchor`). Resolved client-side against the site base path, so navigation stays in-site and drives the standard navigation progress bar.
|
||||
- 6083a88: Expose the current page (`id`, `path`, `title`) to integration block webframes through the client-only webframe `state.page`, alongside adaptive visitor claims.
|
||||
- 2c4d40a: Expose the site's MCP tools to browser agents through WebMCP when the MCP page action is enabled.
|
||||
- 8a70022: Only track embed view events once the frame is actually shown to the reader
|
||||
- cf94386: Fix an issue where certain keywords could cause an exception when rendering emojis
|
||||
- 673f4b6: Add the `select` action to InlineButton. Clicking the button activates its slug, so any block containing that slug switches to it.
|
||||
- Updated dependencies [94a496c]
|
||||
- Updated dependencies [a69a307]
|
||||
- Updated dependencies [03bbacf]
|
||||
- Updated dependencies [bf674a4]
|
||||
- Updated dependencies [65f99ea]
|
||||
- Updated dependencies [1424c56]
|
||||
- Updated dependencies [3ef1802]
|
||||
- Updated dependencies [3bf55ce]
|
||||
- Updated dependencies [cb92754]
|
||||
- Updated dependencies [6083a88]
|
||||
- @gitbook/embed@0.5.2
|
||||
- @gitbook/colors@0.4.4
|
||||
- @gitbook/openapi-parser@3.0.13
|
||||
- @gitbook/react-openapi@2.0.0
|
||||
- @gitbook/browser-types@0.1.6
|
||||
- @gitbook/react-contentkit@0.7.17
|
||||
|
||||
## 0.27.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bb30db1: Add a hover affordance in the document margin to ask the AI Assistant about a paragraph. On devices with a fine pointer, hovering a top-level paragraph reveals a small button that stages the paragraph's text as context and opens the assistant — making the existing text-selection "Ask" flow more discoverable.
|
||||
- 62ca6d3: Show a "Back to [space]" shortcut at the top of the table of contents when a reader follows a link into a different space, so they don't lose track of where they were browsing from.
|
||||
- fdea8f1: Show where a page lives in the site at the start of the page breadcrumbs: its section (including any enclosing section groups) and variant. Hovering any breadcrumb item reveals a dropdown to switch to its siblings at the same level — other section groups, sections, variants, page groups or pages — computed on the server from already-available data (no extra request). Language variants are left to the dedicated language picker rather than shown as a crumb. Also restyles the breadcrumbs to normal casing (matching search results).
|
||||
- 37c2dd8: Navigate customer visual tests with `domcontentloaded` to avoid `load`-event hangs on external sites.
|
||||
- c923c65: Fix center- and end-aligned paragraphs shifting left on wide/no-TOC pages when the AI Assistant is enabled, caused by the per-paragraph ask-button wrapper not inheriting the block alignment.
|
||||
- 37c2dd8: Fix flaky customer e2e screenshots by waiting for the built-in cookie banner before capturing.
|
||||
- 37c2dd8: Add a `data-testid` to the admin toolbar so e2e tests can assert its presence while hiding it from visual screenshots (it animates open, causing flaky diffs).
|
||||
- 36dbdb4: Support an optional `goal` query parameter on the markdown ask interface (`?ask=…&goal=…`), letting agents describe the broader end goal they are working towards so the answer can be steered towards it.
|
||||
- 7e55cd5: Add an "Available in MCP" badge on OpenAPI operations marked with `x-gitbook-mcp: true`. When `x-gitbook-mcp-url` is set (on the operation, path, or root — most specific wins), the badge becomes a button that copies the MCP server URL to the clipboard.
|
||||
- 6146f8e: Reduce the size of `searchDocumentation` MCP responses by returning only the best-matching section per page instead of concatenating every section body.
|
||||
- 37c2dd8: Disable the content max-width transition under reduced motion, matching the surrounding layout transitions.
|
||||
- 37c2dd8: Reset cross-space navigation state between e2e navigations so the "Back to <space>" shortcut can't leak in and cause flaky screenshots.
|
||||
- bcea23e: Render `anyOf`/`oneOf` with a `null` member as a nullable schema instead of a `null` union branch.
|
||||
- 77efd44: Show changed pages in preview toolbars for change requests and revisions.
|
||||
- 578a3a8: Fix site section dropdowns not being bounded by their container during the open/resize animation, causing the contents to visually clip.
|
||||
- 2dc76a2: Fix grouped top-nav section dropdowns rendering empty when the site is embedded in an iframe (visitor-auth embeds, editor preview) or shown in the embeddable view. The dropdown viewport is composited and animated, and a clipped composited layer fails to rasterize its text in Chromium when painted inside a sub compositing root; the rounded-corner clipping is now done on an inner wrapper so the viewport itself is no longer clipped.
|
||||
- 9f65133: Improve the 404 page: remove the table of contents sidebar and center the content, suggest related pages (ranked from the already-cached search index, so no extra request per 404), show a search or assistant input depending on the site's AI configuration, and move "Go to homepage" to the bottom as a last resort.
|
||||
- Updated dependencies [3fedaaa]
|
||||
- Updated dependencies [7e55cd5]
|
||||
- @gitbook/expr@1.3.1
|
||||
- @gitbook/openapi-parser@3.0.12
|
||||
- @gitbook/react-openapi@1.5.15
|
||||
|
||||
## 0.27.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- acf2719: Mark the AI Assistant chat as `aria-busy` while it is generating a response (including the follow-up suggestion phase), so assistive technologies are notified of the in-progress state.
|
||||
- 72a1596: Fix the AI assistant panel overflowing its frame when rendering single/multi-choice question forms with long option text.
|
||||
- 77c717c: Assistant: group all of a turn's tool calls behind a single "Explored with N tools" expandable instead of rendering a separate single-item expandable for each client-side tool call.
|
||||
- c44d822: Trust an optional cookieless integration content host (`GITBOOK_INTEGRATIONS_CONTENT_HOST`) for WebFrame postMessage, in preparation for isolating integration-rendered content onto a separate origin. Defaults to `GITBOOK_INTEGRATIONS_HOST`, so behavior is unchanged until the content host is configured.
|
||||
- 7655041: Match keyboard shortcuts by the logical character typed instead of the physical key position, so that ⌘-C no longer opens the Assistant on the Dvorak layout (and other non-QWERTY layouts).
|
||||
- af3cda1: Fix wide content (such as code blocks and tables) overflowing the edges of hint blocks instead of scrolling within them.
|
||||
- 0ca4952: Remove the external link button from the link tooltip hover card, as its arrow read as clickable even though the preview card isn't interactive.
|
||||
- 3ff88ba: Add a Prompt block
|
||||
- 251630c: Drive page actions ordering and default action from the new `pageActions.items` list, including the reorderable assistant action
|
||||
- ddba803: Add a page reference chip when opening the Assistant from the page action, so the assistant is informed about the page the reader is currently on.
|
||||
- f4469b2: Support OpenAPI one-operation-per-page mode
|
||||
- 6c967c9: Add a client-side search field to table blocks, with per-column filters for select and checkbox columns
|
||||
- 882dfcd: Tidy prompt and add e2e test
|
||||
- Updated dependencies [f4469b2]
|
||||
- Updated dependencies [88b94b5]
|
||||
- @gitbook/react-openapi@1.5.14
|
||||
|
||||
## 0.27.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- aa9d41a: Update llms.txt footer for GitBook sites
|
||||
- 4f754a7: Add GitBook information to footer of markdown pages
|
||||
- 2ecbdd8: Add a `navigateToPage` assistant tool that lets the AI open a documentation page on the reader's behalf. The page opens instantly (no confirmation) and is pushed to the browser history so the reader can navigate back.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7036b67: Fix custom assistants (e.g. Kapa, Inkeep) being opened twice when clicking "Ask with …" in the search bar.
|
||||
- 2885a13: Fix heading anchor alignment with heading text and translate its accessibility label.
|
||||
- b9bb535: Allow Mermaid diagrams to be enlarged into a fullscreen dialog from a control in the bottom-right corner. Clicking outside the dialog, pressing Escape, or using the reduce control returns to the inline view.
|
||||
- b116a0a: Fix OpenAPI CodeBlock scroll
|
||||
- d83ea4c: Improve a11y of list item collapse/extend toggle
|
||||
- 92b6856: Add Ask AI in CodeBlock
|
||||
- 7a1236a: Fix search results linking to the page instead of the section anchor when a section snippet is displayed.
|
||||
- b9bb535: Make `Tooltip` content non-interactive when `disableHoverableContent` is set, so its portaled popper wrapper no longer steals pointer events (e.g. hover-revealed controls) from the trigger.
|
||||
- Updated dependencies [6eabaec]
|
||||
- Updated dependencies [f158064]
|
||||
- @gitbook/expr@1.3.0
|
||||
- @gitbook/react-openapi@1.5.13
|
||||
|
||||
## 0.26.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 67f31aa: Allow user-triggered AI assistants (ChatGPT-User, Claude-User, Perplexity-User) to read pages on non-indexable sites, so end-users can pull content into an LLM without an MCP connection. Search engines and training crawlers remain blocked.
|
||||
- 0c577a0: Allow CORS for the same root domain
|
||||
- 51fef84: Remove the date-gated header logo height logic and always apply the compact logo size to prevent size shifts between sections and translations.
|
||||
- f405fff: Add icon support in tab items
|
||||
- 3697dfe: Add support for hiding page actions from page layout options.
|
||||
- 127ef54: Add tag filtering controls for Updates blocks.
|
||||
|
||||
## 0.26.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b70b8fd: Fix llms-full.txt pagination to include pages from all site sections.
|
||||
- 6ba5e54: Support isComposing for search
|
||||
- 47c8ccb: Render icons as svg markup
|
||||
- a9ba996: Fix left/right aligment of images with caption
|
||||
- 56aeb19: Fix events tracked from the middleware missing some properties.
|
||||
- Updated dependencies [47c8ccb]
|
||||
- Updated dependencies [37a9453]
|
||||
- @gitbook/icons@0.5.0
|
||||
- @gitbook/embed@0.5.1
|
||||
- @gitbook/react-contentkit@0.7.16
|
||||
|
||||
## 0.26.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d3d5150: Add "Content-Signal" to "robots.txt"
|
||||
- 7595706: Allow AI agents to ask questions and get the answer in markdown when fetching with `?ask=<question>`.
|
||||
- a17ab65: Support variant selector in hidden section
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d1a2958: Skip AI questions when it matches the example provided to agents / crawlers.
|
||||
- e3aca9e: Fix embeddable search links
|
||||
- 4b78672: Fix links to other spaces/sections in the llms.txt.
|
||||
- d1a2958: Pass visitor session to Ask AI.
|
||||
|
||||
## 0.25.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- aa1fc24: Add support for sticky first column in tables
|
||||
|
||||
## 0.25.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 88c38fa: Add Search tab to Docs Embed, refactor search
|
||||
- 709964f: Rework page layout
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 789c2fa: Adjust empty TOC logic and layout
|
||||
- 8d2a95b: Docs Embed: Better support light/dark mode overrides
|
||||
- 7fd381c: Hide empty sidebar on OpenAPI pages
|
||||
- 723e579: Fix rendering multi-expandable block in a column
|
||||
- 68c842b: Translations dropdown should show language spaces that best matches the current space title
|
||||
- Updated dependencies [8d2a95b]
|
||||
- Updated dependencies [88c38fa]
|
||||
- @gitbook/embed@0.5.0
|
||||
|
||||
## 0.24.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 50653ab: Redirect sitemap.md and .well-known/sitemap.md to llms.txt.
|
||||
- 50653ab: Improve AI agent detection and readability by outputting 200 for page not found.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 126d4b7: Improve Assistant form controls
|
||||
- Updated dependencies [8242f18]
|
||||
- @gitbook/expr@1.2.5
|
||||
- @gitbook/react-openapi@1.5.12
|
||||
|
||||
## 0.23.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ab17ff4]
|
||||
- @gitbook/icons@0.4.6
|
||||
- @gitbook/react-contentkit@0.7.15
|
||||
|
||||
## 0.23.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3c141f0]
|
||||
- @gitbook/icons@0.4.5
|
||||
- @gitbook/react-contentkit@0.7.14
|
||||
|
||||
## 0.23.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8e5a2aa: Pan and zoom controls for mermaid diagrams
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b77c4fc: Add getPage MCP tool
|
||||
- 31e77d4: Track new events for site analytics.
|
||||
- f884477: Associate questions asked in the embeddable assistant to the proper channel.
|
||||
- deabce9: Sort results by score to mix records and pages.
|
||||
- 4909044: Add Claude Code and Codex connect actions
|
||||
- da648ca: Fix regressions in Assistant UX
|
||||
- 3ce7e52: Fix Variant dropdown not showing in GitBook embed
|
||||
- 7f4e644: Small OpenAPI Improvements
|
||||
- 4787b7e: Use localized titles/descriptions for site entities
|
||||
- 7ab6351: Import site redirect patterns from @gitbook/api
|
||||
- b40465e: Fix normalization of URL causing invalid redirections.
|
||||
- b5d6f2e: Fix translation quality across 11 locales
|
||||
- 3151864: Track events from the proper server URL.
|
||||
- 16bfafe: Add option to sticky header row in tables
|
||||
- 7c2823c: Fix full width header alignment
|
||||
- c4a2604: Fix PDF export crash when page contains an OpenAPI block
|
||||
- c56b569: Support expand-all options for OpenAPI
|
||||
- Updated dependencies [7f4e644]
|
||||
- Updated dependencies [b5d6f2e]
|
||||
- Updated dependencies [9dd2db6]
|
||||
- Updated dependencies [c56b569]
|
||||
- Updated dependencies [ccd21e4]
|
||||
- @gitbook/react-openapi@1.5.11
|
||||
- @gitbook/openapi-parser@3.0.11
|
||||
- @gitbook/icons@0.4.4
|
||||
- @gitbook/react-contentkit@0.7.13
|
||||
|
||||
## 0.22.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f182680: Add backward compatibility for OpenAPI spec content URL
|
||||
- bdc6cd9: Use OpenAPI publicURL as Scalar specUrl
|
||||
- b3875a1: Refactor embeddable context to merge local & site properties in one unified way
|
||||
- eceec6f: Move ~scalar/proxy route to site route group
|
||||
- 887c3f8: Add token verification on OpenAPI proxy
|
||||
- d666212: Add support for localized site section titles
|
||||
- Updated dependencies [b3875a1]
|
||||
- Updated dependencies [bdc6cd9]
|
||||
- Updated dependencies [8ab419a]
|
||||
- Updated dependencies [eceec6f]
|
||||
- Updated dependencies [887c3f8]
|
||||
- @gitbook/embed@0.4.0
|
||||
- @gitbook/react-openapi@1.5.10
|
||||
- @gitbook/openapi-parser@3.0.10
|
||||
|
||||
## 0.22.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e074283: Add server-side proxy for Scalar API client to bypass CORS
|
||||
- afa868e: Show "Ask Assistant" search result immediately
|
||||
- 0803db0: Track opening of search results for records and in the assistant.
|
||||
- cb71511: Wrap tracking call with waitUntil
|
||||
- 4a6f91a: Show social accounts in header
|
||||
- 370b731: Docs Embed: Make trademark optional
|
||||
- afa476c: Add server-side insight event tracking for MCP route
|
||||
- Updated dependencies [e074283]
|
||||
- Updated dependencies [370b731]
|
||||
- @gitbook/openapi-parser@3.0.9
|
||||
- @gitbook/react-openapi@1.5.9
|
||||
- @gitbook/embed@0.3.0
|
||||
|
||||
## 0.22.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9d5e397: Remove code syntax highlighting patch for Safari
|
||||
- 3d8e89c: Show favicon for record search results in the assistant.
|
||||
- 5f66860: Use isCookiesTrackingDisabled for cookie consent integrations
|
||||
- 426aabe: Fix TOC header logo on bold themes
|
||||
- ae30a57: Integrate Ask AI with upcoming insights by passing visitor session IDs.
|
||||
- f7c251e: Update Reddit social account rendering
|
||||
- e31a8f4: Hide cookie banner for AI user agents
|
||||
- 2e495cb: Add Global Privacy Control (GPC) support
|
||||
- Updated dependencies [1a7ef78]
|
||||
- Updated dependencies [93eea0b]
|
||||
- Updated dependencies [b4b471f]
|
||||
- Updated dependencies [5f66860]
|
||||
- Updated dependencies [2ebb18d]
|
||||
- Updated dependencies [11d9b80]
|
||||
- Updated dependencies [2e495cb]
|
||||
- Updated dependencies [f517035]
|
||||
- @gitbook/react-openapi@1.5.8
|
||||
- @gitbook/embed@0.2.2
|
||||
- @gitbook/browser-types@0.1.5
|
||||
|
||||
## 0.22.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- e00d1c5: Support code themes customization
|
||||
- f294818: Serve a markdown version of the page when the "Accept" header contains "text/markdown".
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9d6457f: OpenAPI styling tweaks
|
||||
- d738e11: Prevent form submission during IME composition
|
||||
- 1e53376: Add sidesheet component, use it for TOC and AIChat
|
||||
- f7f6f9d: Set <html lang> based on translated language
|
||||
- 89de966: Support social links
|
||||
- b53cb1d: Align styling of PageLinkItem and PageDocumentItem
|
||||
- 27a34b7: Support `defaultExpanded` in Expandable block
|
||||
- a5ef44a: Better conditional display of page actions
|
||||
- 07c02eb: Header & TOC tweaks
|
||||
- 29e2b22: Fix ordered list item index calculation
|
||||
- f6cf62a: Standardise toggle chevrons across the app
|
||||
- b57561d: Fix rendering of images in reusable content from an external space with the new dereferencing in the API.
|
||||
- e172b25: Grow InlineActionButton to take available width on line
|
||||
- 7a11861: Add support for custom cookie banner registration
|
||||
- 8f32f4f: Better handle huge section groups
|
||||
- 0aa34cc: Fix Embed options in React
|
||||
- Updated dependencies [e00d1c5]
|
||||
- Updated dependencies [96e24a1]
|
||||
- Updated dependencies [4e2d863]
|
||||
- Updated dependencies [d3f6c38]
|
||||
- Updated dependencies [3ba9e46]
|
||||
- Updated dependencies [717ad0b]
|
||||
- Updated dependencies [6f1db32]
|
||||
- Updated dependencies [7a11861]
|
||||
- Updated dependencies [0aa34cc]
|
||||
- Updated dependencies [48ca21e]
|
||||
- @gitbook/react-openapi@1.5.7
|
||||
- @gitbook/embed@0.2.1
|
||||
- @gitbook/openapi-parser@3.0.8
|
||||
- @gitbook/icons@0.4.3
|
||||
- @gitbook/browser-types@0.1.4
|
||||
- @gitbook/react-contentkit@0.7.12
|
||||
|
||||
## 0.21.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf63045]
|
||||
- @gitbook/react-openapi@1.5.6
|
||||
|
||||
## 0.21.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ca68236: Fix 404 when accessing rss.xml for the root page.
|
||||
- 21851d3: Round page cover corners on extrawide screens
|
||||
- 695e99f: Update button sizing
|
||||
- 7517e98: Clean up button and input across the app
|
||||
- 99d788e: Update page cover styling
|
||||
- ebf3b2b: Fix an issue where content refs to spaces outside of site was resolving with share links
|
||||
- 35efc9d: Fix TOCPageIcons backgrounds on gradient and bold themes
|
||||
- a7f58ab: Fix embed tab button size regression
|
||||
- 360b525: Switch icon masking method to fix visual cutoffs in FA7
|
||||
- Updated dependencies [985c1b4]
|
||||
- Updated dependencies [360b525]
|
||||
- @gitbook/openapi-parser@3.0.7
|
||||
- @gitbook/icons@0.4.2
|
||||
- @gitbook/react-openapi@1.5.5
|
||||
- @gitbook/react-contentkit@0.7.11
|
||||
|
||||
## 0.21.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7fb771]
|
||||
- @gitbook/icons@0.4.1
|
||||
- @gitbook/react-contentkit@0.7.10
|
||||
|
||||
## 0.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 3553db0: Add a RSS feed to all pages with update blocks.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f478ddc: Add Input component
|
||||
- 3817289: Fix overflowing section groups
|
||||
- 86e7aa6: Fix search results cursor
|
||||
- 1430cee: Improve Docs Embed with separate Assistant and Docs tabs
|
||||
- ed9e8fe: Add support for button actions
|
||||
- 9c69cc4: Add customizable suggested questions
|
||||
- c51076e: Fix links to other spaces and root page in embeddable view.
|
||||
- 6278a18: Support `greeting` and fix suggested questions if there are no custom ones defined
|
||||
- 04a4175: Remove hidden site spaces from navigation
|
||||
- 74f8931: Fix hidden section not found
|
||||
- 344842f: Improve OpenAPI circular references
|
||||
- Updated dependencies [4766092]
|
||||
- Updated dependencies [8761cee]
|
||||
- Updated dependencies [461e15f]
|
||||
- Updated dependencies [1430cee]
|
||||
- Updated dependencies [62438f4]
|
||||
- Updated dependencies [3e40b4d]
|
||||
- Updated dependencies [87d68ea]
|
||||
- Updated dependencies [344842f]
|
||||
- @gitbook/react-openapi@1.5.4
|
||||
- @gitbook/openapi-parser@3.0.6
|
||||
- @gitbook/embed@0.2.0
|
||||
- @gitbook/icons@0.4.0
|
||||
- @gitbook/react-contentkit@0.7.9
|
||||
|
||||
## 0.20.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 1d92dc1: Update site layout and blocks with container queries and better transitions
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8fdc5c8: Fix webframe height issue
|
||||
- 9022d87: Add breadcrumbs to search results
|
||||
- 0c01832: SearchInput fixes — Translate Clear button, Bold theme color, "Esc" keyboard shortcut
|
||||
- 1d1f0a7: Fix search container shadow
|
||||
- 2aa4f2e: Update dropdown menu styling
|
||||
- a49781e: Printable collapsed code block
|
||||
- 7d6a316: Revert contentref anchor title
|
||||
- e27986b: Reword "Everywhere" search scope to "All docs"
|
||||
- 758b34d: Move the "Ask <query>" to the bottom of search results if query is not a question
|
||||
- 0ae7cfd: Improve AI Search branding
|
||||
- f9f8011: Add alt text support to card covers
|
||||
- ebbb476: Improve ancestor label in references
|
||||
- 1b0613e: Tweak language dropdown styling
|
||||
- 3355960: Fix ToggeableLinkItem display
|
||||
- c31e564: Fix icon resolution for page group
|
||||
- 55c0b03: Support translations and generic variants together
|
||||
- 3876f70: Edgeless Assistant window on `depth: flat` sites
|
||||
- 683bb8f: Add confirmation to "Copy MCP URL" page action
|
||||
- 4534ea8: Implement unique title tags for sections & variants
|
||||
- 6d56df7: Treat pages without visible TOC items as `no-toc`
|
||||
- 8606f22: Add padding to TOC page group items
|
||||
- 3676d64: Update search scope UI
|
||||
- c32d449: Fix OpenAPI path display
|
||||
- 8ce7322: Add OpenAPI servers selection
|
||||
- 2c3066e: Improve OAuth2 scopes handling in OpenAPI
|
||||
- 6b1c9fe: Fix opacity of ScrollContainer buttons on bold themes
|
||||
- Updated dependencies [b4a021a]
|
||||
- Updated dependencies [a512c90]
|
||||
- Updated dependencies [df1966d]
|
||||
- Updated dependencies [b45feaf]
|
||||
- Updated dependencies [10995e0]
|
||||
- Updated dependencies [f9f8011]
|
||||
- Updated dependencies [10995e0]
|
||||
- Updated dependencies [8ce7322]
|
||||
- Updated dependencies [2c3066e]
|
||||
- @gitbook/react-openapi@1.5.3
|
||||
- @gitbook/openapi-parser@3.0.5
|
||||
- @gitbook/react-contentkit@0.7.8
|
||||
- @gitbook/browser-types@0.1.3
|
||||
- @gitbook/cache-tags@0.3.4
|
||||
- @gitbook/react-math@0.6.4
|
||||
- @gitbook/colors@0.4.3
|
||||
- @gitbook/embed@0.1.5
|
||||
- @gitbook/fonts@0.1.3
|
||||
- @gitbook/icons@0.3.4
|
||||
- @gitbook/expr@1.2.4
|
||||
|
||||
## 0.19.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
- 75543b1: Update minimum Node version in .npmrc
|
||||
- 511f1cf: Add scroll margin to Expandable
|
||||
- Updated dependencies [6142d6b]
|
||||
- @gitbook/emoji-codepoints@0.2.2
|
||||
- @gitbook/react-contentkit@0.7.7
|
||||
- @gitbook/openapi-parser@3.0.4
|
||||
- @gitbook/browser-types@0.1.2
|
||||
- @gitbook/react-openapi@1.5.2
|
||||
- @gitbook/cache-tags@0.3.3
|
||||
- @gitbook/react-math@0.6.3
|
||||
- @gitbook/colors@0.4.2
|
||||
- @gitbook/fonts@0.1.2
|
||||
- @gitbook/icons@0.3.3
|
||||
- @gitbook/expr@1.2.3
|
||||
|
||||
## 0.19.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
- Updated dependencies [bdde392]
|
||||
- Updated dependencies [295f03d]
|
||||
- @gitbook/react-contentkit@0.7.6
|
||||
- @gitbook/browser-types@0.1.1
|
||||
- @gitbook/cache-tags@0.3.2
|
||||
- @gitbook/colors@0.4.1
|
||||
- @gitbook/emoji-codepoints@0.2.1
|
||||
- @gitbook/expr@1.2.2
|
||||
- @gitbook/fonts@0.1.1
|
||||
- @gitbook/icons@0.3.2
|
||||
- @gitbook/openapi-parser@3.0.3
|
||||
- @gitbook/react-math@0.6.2
|
||||
- @gitbook/react-openapi@1.5.1
|
||||
|
||||
## 0.19.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ed7d47d: Show a toolbar for authenticated users with access to the current site.
|
||||
- 50c1be3: Move inline expression evaluation from API to GBO
|
||||
- 56e46ce: Add Italian translation
|
||||
- b932e4e: Scope search across sections and variants
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 162cfe4: Hide sections if only one is present
|
||||
- 1456251: Unify section display condition
|
||||
- 9344431: Fix missing geo data in site insights because of incorrect proxying of events.
|
||||
- eea8f1e: Enhance OpenAPI security scopes handling
|
||||
- 2b8a2d2: Add stable scroll gutter to search container
|
||||
- b498521: Fix robots.txt preventing favicon from being indexed.
|
||||
- ceb32b1: Redo search scope UI
|
||||
- 8e99871: Highlight discriminator properties in oneOf, allOf, anyOf objects
|
||||
- 2fc2127: Make search accessible
|
||||
- f3e4041: Adapt OpenAPI code samples to prefill API key using visitor data
|
||||
- 6815dd1: Fix markdownPageURL for PageActions
|
||||
- c06b3dd: Improve default site icon
|
||||
- cba583a: Use sitespace title instead of space title in search
|
||||
- e434442: Support nested site section groups
|
||||
- 469b332: Add support for inline expressions rendering with visitor data on GBO side
|
||||
- 0ef5dc8: Update to column width sizing
|
||||
- 1e4e54e: Fix rounded outline items
|
||||
- 379d486: Expose "Best match" when site has translations
|
||||
- c21693f: Improve screen reader accessibility for hints
|
||||
- badf3a9: Add alt text to inline image
|
||||
- c23d4ef: Fix crash for card cover defined without objectFit
|
||||
- d74f8a7: Improve wide page width layout
|
||||
- b8388e5: Add scrollcontainer component
|
||||
- b85eccd: Refactor section tabs
|
||||
- 1e53f46: Fix button content truncate
|
||||
- 7a504b7: Fix OpenAPI response dropdown & vertical overflow
|
||||
- a4c3399: Fix OpenAPI alternatives not showing
|
||||
- Updated dependencies [a629900]
|
||||
- Updated dependencies [eea8f1e]
|
||||
- Updated dependencies [8e99871]
|
||||
- Updated dependencies [f3e4041]
|
||||
- Updated dependencies [319a1e5]
|
||||
- Updated dependencies [529f940]
|
||||
- Updated dependencies [754cc11]
|
||||
- Updated dependencies [d7948e3]
|
||||
- Updated dependencies [a4c3399]
|
||||
- @gitbook/expr@1.2.1
|
||||
- @gitbook/react-openapi@1.5.0
|
||||
|
||||
## 0.18.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 262afa3: Expose a MCP server for the docs site under /~gitbook/mcp
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [262afa3]
|
||||
- @gitbook/icons@0.3.1
|
||||
- @gitbook/react-openapi@1.4.3
|
||||
- @gitbook/react-contentkit@0.7.5
|
||||
|
||||
## 0.17.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @gitbook/react-openapi@1.4.2
|
||||
|
||||
## 0.17.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 24f601d: Small optim in resolveTryItPrefillForOperation
|
||||
- aea5eb1: Persist language choice across sections if possible
|
||||
- 1165a81: Language selector edge cases
|
||||
- f9a2977: Better handling for external link "mailto:" in Hovered Card in GBO
|
||||
- Updated dependencies [24f601d]
|
||||
- @gitbook/react-openapi@1.4.1
|
||||
|
||||
## 0.17.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 12c9d76: Adapt OpenAPI blocks to eval adaptive exprs & prefill TryIt config
|
||||
- 7911350: Add language selector to site header
|
||||
- 659be55: Track insight event when embedded assistant is displayed.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2e0d706: Fix corner radius of active section on "Line" sidebar style
|
||||
- 78a632b: Fix edit on GitHub button doesn't take you to GitHub
|
||||
- 1edc5d6: Add "hold message" to Assistant
|
||||
- 4520728: Support bold in headings
|
||||
- 61b8507: OpenAPI: Make responses without objects clickable
|
||||
- d1fdc13: Remove ligatures from Lato font
|
||||
- a8fca0e: Fix custom assistant keyboard shortcut
|
||||
- 262a9b1: Fix embed script and assets being cached for too long
|
||||
- c890e01: Fix order in robots.txt preventing indexation of images by Google.
|
||||
- 1839ea2: Fix content min-height with sections
|
||||
- 9201e2c: Adds vertical align to column block
|
||||
- 193d591: Use space language as source of truth for UI locale
|
||||
- f08dd29: Fix Search results are not clickable on sites without header
|
||||
- 17dd382: Add `original` background color step
|
||||
- 4f35882: Fix event ask_question not being tracked
|
||||
- d51b79e: Fix Search bar is broken on site with sections when header is disabled
|
||||
- 6f368b5: Fix embed assistant window width on small screens
|
||||
- Updated dependencies [193d591]
|
||||
- Updated dependencies [12c9d76]
|
||||
- Updated dependencies [4927e96]
|
||||
- Updated dependencies [61b8507]
|
||||
- Updated dependencies [7fefe49]
|
||||
- Updated dependencies [360aa1c]
|
||||
- Updated dependencies [98e42cf]
|
||||
- Updated dependencies [17dd382]
|
||||
- @gitbook/colors@0.4.0
|
||||
- @gitbook/react-openapi@1.4.0
|
||||
- @gitbook/openapi-parser@3.0.2
|
||||
|
||||
## 0.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { type TestsCase, getCustomizationURL, runTestCases } from './util';
|
||||
|
||||
const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'Cookie Banner',
|
||||
contentBaseURL: 'https://gitbook.com/docs/',
|
||||
tests: [
|
||||
{
|
||||
name: 'should show built-in banner when no custom banner is registered',
|
||||
url: getCustomizationURL({
|
||||
privacyPolicy: {
|
||||
url: 'https://policies.gitbook.com/privacy/cookies',
|
||||
},
|
||||
}),
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
// Check that built-in banner is visible
|
||||
const dialog = page.getByTestId('cookies-dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 5000 });
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should not show built-in banner when UserAgent is AI',
|
||||
url: getCustomizationURL({
|
||||
privacyPolicy: {
|
||||
url: 'https://policies.gitbook.com/privacy/cookies',
|
||||
},
|
||||
}),
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
// Override navigator.userAgent to simulate AI crawler (affects isAIUserAgent())
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
get: () =>
|
||||
'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot',
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
await page.reload();
|
||||
|
||||
// Check that built-in banner is not visible for AI UserAgent
|
||||
const dialog = page.getByTestId('cookies-dialog');
|
||||
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should not show built-in banner when custom banner is registered',
|
||||
url: getCustomizationURL({
|
||||
privacyPolicy: {
|
||||
url: 'https://policies.gitbook.com/privacy/cookies',
|
||||
},
|
||||
}),
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
// Register a custom cookie banner handler
|
||||
await page.waitForFunction(() => {
|
||||
return typeof window !== 'undefined' && window.GitBook !== undefined;
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
window.GitBook?.registerCookieBanner(() => {
|
||||
// Custom cookie banner handler - no-op for testing to avoid reload
|
||||
});
|
||||
});
|
||||
|
||||
// Check that built-in banner is not visible
|
||||
const dialog = page.getByTestId('cookies-dialog');
|
||||
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
runTestCases(testCases);
|
||||
@@ -1,5 +1,18 @@
|
||||
import { type TestsCase, runTestCases, waitForCookiesDialog } from './util';
|
||||
|
||||
const ONE_HOUR_IN_MS = 60 * 60 * 1000;
|
||||
|
||||
const CLOSED_ONETRUST_COOKIE_BANNER = [
|
||||
{
|
||||
name: '__gitbook_cookie_granted',
|
||||
value: 'no',
|
||||
},
|
||||
{
|
||||
name: 'OptanonAlertBoxClosed',
|
||||
value: new Date(Date.now() - ONE_HOUR_IN_MS).toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
/** A list of test cases to run on the customers' docs sites. */
|
||||
const testCases: TestsCase[] = [
|
||||
{
|
||||
@@ -10,38 +23,28 @@ const testCases: TestsCase[] = [
|
||||
{ name: 'OpenAPI', url: '/snyk-api/reference/apps', run: waitForCookiesDialog },
|
||||
],
|
||||
},
|
||||
// {
|
||||
// name: 'Nexthink',
|
||||
// contentBaseURL: 'https://docs.nexthink.com',
|
||||
// tests: [
|
||||
// {
|
||||
// name: 'Home',
|
||||
// url: '/',
|
||||
// screenshot: { waitForTOCScrolling: false },
|
||||
// run: waitForCookiesDialog,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
name: 'Nexthink',
|
||||
contentBaseURL: 'https://docs.nexthink.com',
|
||||
tests: [
|
||||
{
|
||||
name: 'Home',
|
||||
url: '/',
|
||||
screenshot: { waitForTOCScrolling: false },
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'asiksupport-stg.dto.kemkes.go.id',
|
||||
contentBaseURL: 'https://asiksupport-stg.dto.kemkes.go.id',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'jasons-tutorials.gitbook.io',
|
||||
contentBaseURL: 'https://jasons-tutorials.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'faq.deltaemulator.com',
|
||||
contentBaseURL: 'https://faq.deltaemulator.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.dify.ai',
|
||||
contentBaseURL: 'https://docs.dify.ai',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'seeddao.gitbook.io',
|
||||
contentBaseURL: 'https://seeddao.gitbook.io',
|
||||
@@ -62,16 +65,12 @@ const testCases: TestsCase[] = [
|
||||
contentBaseURL: 'https://docs.gmgn.ai',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.spicychat.ai',
|
||||
contentBaseURL: 'https://docs.spicychat.ai',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.portainer.io',
|
||||
contentBaseURL: 'https://docs.portainer.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
// Removed, an AI widget, makes it flaky.
|
||||
// {
|
||||
// name: 'docs.portainer.io',
|
||||
// contentBaseURL: 'https://docs.portainer.io',
|
||||
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
// },
|
||||
{
|
||||
name: 'docs.chirptoken.io',
|
||||
contentBaseURL: 'https://docs.chirptoken.io',
|
||||
@@ -102,26 +101,11 @@ const testCases: TestsCase[] = [
|
||||
contentBaseURL: 'https://docs.midas.app',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.keeper.io',
|
||||
contentBaseURL: 'https://docs.keeper.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'adiblar.gitbook.io',
|
||||
contentBaseURL: 'https://adiblar.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.gradient.network',
|
||||
contentBaseURL: 'https://docs.gradient.network',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
// {
|
||||
// name: 'mygate-network.gitbook.io',
|
||||
// contentBaseURL: 'https://mygate-network.gitbook.io',
|
||||
// tests: [{ name: 'Home', url: '/' }],
|
||||
// },
|
||||
{
|
||||
name: 'treasurenft.gitbook.io',
|
||||
contentBaseURL: 'https://treasurenft.gitbook.io',
|
||||
@@ -150,33 +134,18 @@ const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'docs.tickettool.xyz',
|
||||
contentBaseURL: 'https://docs.tickettool.xyz',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'wiki.redmodding.org',
|
||||
contentBaseURL: 'https://wiki.redmodding.org',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
// {
|
||||
// name: 'docs.cherry-ai.com',
|
||||
// contentBaseURL: 'https://docs.cherry-ai.com',
|
||||
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
// },
|
||||
{
|
||||
name: 'docs.snyk.io',
|
||||
contentBaseURL: 'https://docs.snyk.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.realapp.link',
|
||||
contentBaseURL: 'https://docs.realapp.link',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.plaza.finance',
|
||||
contentBaseURL: 'https://docs.plaza.finance',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.publicai.io',
|
||||
contentBaseURL: 'https://docs.publicai.io',
|
||||
@@ -199,16 +168,6 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'sosovalue-white-paper.gitbook.io',
|
||||
contentBaseURL: 'https://sosovalue-white-paper.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
// {
|
||||
// name: 'docs.revrobotics.com',
|
||||
// contentBaseURL: 'https://docs.revrobotics.com',
|
||||
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
// },
|
||||
{
|
||||
name: 'chartschool.stockcharts.com',
|
||||
contentBaseURL: 'https://chartschool.stockcharts.com',
|
||||
@@ -217,23 +176,13 @@ const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'docs.soniclabs.com',
|
||||
contentBaseURL: 'https://docs.soniclabs.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.meshchain.ai',
|
||||
contentBaseURL: 'https://docs.meshchain.ai',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.thousandeyes.com',
|
||||
contentBaseURL: 'https://docs.thousandeyes.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.raydium.io',
|
||||
contentBaseURL: 'https://docs.raydium.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.fluentbit.io',
|
||||
contentBaseURL: 'https://docs.fluentbit.io',
|
||||
@@ -242,12 +191,419 @@ const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'run-ai-docs.nvidia.com',
|
||||
contentBaseURL: 'https://run-ai-docs.nvidia.com',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'customers-v2',
|
||||
tests: [
|
||||
{ name: 'Home', url: '/' },
|
||||
{
|
||||
name: 'Home',
|
||||
url: '/',
|
||||
cookies: CLOSED_ONETRUST_COOKIE_BANNER,
|
||||
},
|
||||
{ name: 'OG Image', url: '/~gitbook/ogimage/h17zQIFwy3MaafVNmItO', mode: 'image' },
|
||||
],
|
||||
},
|
||||
|
||||
// Additional customer docs sites.
|
||||
{
|
||||
name: 'unsloth.ai/docs',
|
||||
contentBaseURL: 'https://unsloth.ai',
|
||||
tests: [{ name: 'Home', url: '/docs', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'mariadb.com/docs',
|
||||
contentBaseURL: 'https://mariadb.com',
|
||||
tests: [{ name: 'Home', url: '/docs', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.n8n.io',
|
||||
contentBaseURL: 'https://docs.n8n.io',
|
||||
// The site registers its own cookie banner, so the built-in one never shows.
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.cherryai.com.cn',
|
||||
contentBaseURL: 'https://docs.cherryai.com.cn',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'library.zoom.com',
|
||||
contentBaseURL: 'https://library.zoom.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'help.verkada.com',
|
||||
contentBaseURL: 'https://help.verkada.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.overleaf.com',
|
||||
contentBaseURL: 'https://docs.overleaf.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'wiki.tiltedphoques.com/tilted-online',
|
||||
contentBaseURL: 'https://wiki.tiltedphoques.com',
|
||||
tests: [{ name: 'Home', url: '/tilted-online' }],
|
||||
},
|
||||
{
|
||||
name: 'handbook.musescore.org',
|
||||
contentBaseURL: 'https://handbook.musescore.org',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'kakaobusiness.gitbook.io/main',
|
||||
contentBaseURL: 'https://kakaobusiness.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/main', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.maestro.dev',
|
||||
contentBaseURL: 'https://docs.maestro.dev',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'developers.oxylabs.io',
|
||||
contentBaseURL: 'https://developers.oxylabs.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.parallels.com/landing',
|
||||
contentBaseURL: 'https://docs.parallels.com',
|
||||
tests: [{ name: 'Home', url: '/landing', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'help.impact.com',
|
||||
contentBaseURL: 'https://help.impact.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.9proxy.com',
|
||||
contentBaseURL: 'https://docs.9proxy.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'vimeo.com/legal',
|
||||
contentBaseURL: 'https://vimeo.com',
|
||||
tests: [{ name: 'Home', url: '/legal' }],
|
||||
},
|
||||
{
|
||||
name: 'help.aikido.dev',
|
||||
contentBaseURL: 'https://help.aikido.dev',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'doc.demarche.numerique.gouv.fr',
|
||||
contentBaseURL: 'https://doc.demarche.numerique.gouv.fr',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.adapta.org',
|
||||
contentBaseURL: 'https://docs.adapta.org',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'www.xabuxa.com',
|
||||
contentBaseURL: 'https://www.xabuxa.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.triumpharcade.com',
|
||||
contentBaseURL: 'https://docs.triumpharcade.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'help.glpi-project.org',
|
||||
contentBaseURL: 'https://help.glpi-project.org',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.waydro.id',
|
||||
contentBaseURL: 'https://docs.waydro.id',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'bellingcat.gitbook.io/toolkit',
|
||||
contentBaseURL: 'https://bellingcat.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/toolkit' }],
|
||||
},
|
||||
{
|
||||
name: 'wiki.project-fika.com',
|
||||
contentBaseURL: 'https://wiki.project-fika.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.pinot.apache.org',
|
||||
contentBaseURL: 'https://docs.pinot.apache.org',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.devolutions.net',
|
||||
contentBaseURL: 'https://docs.devolutions.net',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'guides.gresb.com',
|
||||
contentBaseURL: 'https://guides.gresb.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.prestashop-project.org/welcome',
|
||||
contentBaseURL: 'https://docs.prestashop-project.org',
|
||||
tests: [{ name: 'Home', url: '/welcome', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'help.researchgate.net',
|
||||
contentBaseURL: 'https://help.researchgate.net',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.verifone.com',
|
||||
contentBaseURL: 'https://docs.verifone.com',
|
||||
// Verifone's custom Cookiebot integration races with and suppresses GitBook's built-in
|
||||
// banner, so waitForCookiesDialog is not a stable invariant here.
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
// Deactivate it because of a custom Ask AI that causes flakiness.
|
||||
// {
|
||||
// name: 'docs.roboflow.com',
|
||||
// contentBaseURL: 'https://docs.roboflow.com',
|
||||
// tests: [{ name: 'Home', url: '/' }],
|
||||
// },
|
||||
{
|
||||
name: 'www.netexec.wiki',
|
||||
contentBaseURL: 'https://www.netexec.wiki',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'guide.strikepack.com',
|
||||
contentBaseURL: 'https://guide.strikepack.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'gitbook.com/docs',
|
||||
contentBaseURL: 'https://gitbook.com',
|
||||
tests: [
|
||||
{ name: 'Home', url: '/docs', run: waitForCookiesDialog },
|
||||
{
|
||||
name: 'OpenAPI',
|
||||
url: '/docs/developers/gitbook-api/api-reference/docs-sites/site-ai-ask/ask-a-question-in-a-site',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'documentation.gravitee.io',
|
||||
contentBaseURL: 'https://documentation.gravitee.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'faq.wanttopay.net/wanttopay-app',
|
||||
contentBaseURL: 'https://faq.wanttopay.net',
|
||||
tests: [{ name: 'Home', url: '/wanttopay-app' }],
|
||||
},
|
||||
{
|
||||
name: 'guide.prismlive.com',
|
||||
contentBaseURL: 'https://guide.prismlive.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.ionos.com/cloud',
|
||||
contentBaseURL: 'https://docs.ionos.com',
|
||||
tests: [{ name: 'Home', url: '/cloud', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'support.evite.com',
|
||||
contentBaseURL: 'https://support.evite.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'knowledge.illumina.com',
|
||||
contentBaseURL: 'https://knowledge.illumina.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'wiki.retrobat.org',
|
||||
contentBaseURL: 'https://wiki.retrobat.org',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'wiki.polymaker.com',
|
||||
contentBaseURL: 'https://wiki.polymaker.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.ducks-services.com',
|
||||
contentBaseURL: 'https://docs.ducks-services.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.hex-rays.com',
|
||||
contentBaseURL: 'https://docs.hex-rays.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'whitepaper.interlinklabs.ai',
|
||||
contentBaseURL: 'https://whitepaper.interlinklabs.ai',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'help.openloyalty.io',
|
||||
contentBaseURL: 'https://help.openloyalty.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'retrozia.gitbook.io/retrozia',
|
||||
contentBaseURL: 'https://retrozia.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/retrozia' }],
|
||||
},
|
||||
{
|
||||
name: 'helpcenter.channable.com',
|
||||
contentBaseURL: 'https://helpcenter.channable.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'developerdocs.instructure.com',
|
||||
contentBaseURL: 'https://developerdocs.instructure.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'legal.jagex.com',
|
||||
contentBaseURL: 'https://legal.jagex.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'manual.edgetx.org',
|
||||
contentBaseURL: 'https://manual.edgetx.org',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.cortex.io',
|
||||
contentBaseURL: 'https://docs.cortex.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
// Flaky because of the GIF
|
||||
// {
|
||||
// name: 'docs.mufy.ai',
|
||||
// contentBaseURL: 'https://docs.mufy.ai',
|
||||
// tests: [{ name: 'Home', url: '/' }],
|
||||
// },
|
||||
{
|
||||
name: 'docs.ndi.video/all',
|
||||
contentBaseURL: 'https://docs.ndi.video',
|
||||
tests: [{ name: 'Home', url: '/all', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.sevenpens.com/drawtab',
|
||||
contentBaseURL: 'https://docs.sevenpens.com',
|
||||
tests: [{ name: 'Home', url: '/drawtab' }],
|
||||
},
|
||||
{
|
||||
name: 'manuals.i-reporter.jp',
|
||||
contentBaseURL: 'https://manuals.i-reporter.jp',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.holybro.com',
|
||||
contentBaseURL: 'https://docs.holybro.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'help.tokenpocket.pro/en',
|
||||
contentBaseURL: 'https://help.tokenpocket.pro',
|
||||
tests: [{ name: 'Home', url: '/en' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.bullmq.io',
|
||||
contentBaseURL: 'https://docs.bullmq.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'tools.osintnewsletter.com',
|
||||
contentBaseURL: 'https://tools.osintnewsletter.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'wiki.mmorealms.gg',
|
||||
contentBaseURL: 'https://wiki.mmorealms.gg',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.vectra.ai',
|
||||
contentBaseURL: 'https://docs.vectra.ai',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.cipp.app',
|
||||
contentBaseURL: 'https://docs.cipp.app',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'sinfa-com-co.gitbook.io/manual-de-usuario',
|
||||
contentBaseURL: 'https://sinfa-com-co.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/manual-de-usuario' }],
|
||||
},
|
||||
{
|
||||
name: 'support.skylum.com',
|
||||
contentBaseURL: 'https://support.skylum.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.jgscripts.com',
|
||||
contentBaseURL: 'https://docs.jgscripts.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.patchmypc.com',
|
||||
contentBaseURL: 'https://docs.patchmypc.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'guide.cryosparc.com',
|
||||
contentBaseURL: 'https://guide.cryosparc.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'guides.stellaraio.com/stellar',
|
||||
contentBaseURL: 'https://guides.stellaraio.com',
|
||||
tests: [{ name: 'Home', url: '/stellar', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.iyzico.com',
|
||||
contentBaseURL: 'https://docs.iyzico.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'help.wotnot.io',
|
||||
contentBaseURL: 'https://help.wotnot.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.sportmonks.com/v3',
|
||||
contentBaseURL: 'https://docs.sportmonks.com',
|
||||
tests: [{ name: 'Home', url: '/v3', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.payments.thalescloud.io',
|
||||
contentBaseURL: 'https://docs.payments.thalescloud.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'doc.anytype.io/anytype-docs',
|
||||
contentBaseURL: 'https://doc.anytype.io',
|
||||
tests: [{ name: 'Home', url: '/anytype-docs', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'help.blotato.com',
|
||||
contentBaseURL: 'https://help.blotato.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.cartographer3d.com',
|
||||
contentBaseURL: 'https://docs.cartographer3d.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.acestudio.ai',
|
||||
contentBaseURL: 'https://docs.acestudio.ai',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
];
|
||||
|
||||
runTestCases(testCases);
|
||||
|
||||
+1353
-173
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { getContentTestURL } from '../tests/utils';
|
||||
import { waitForIcons } from './util';
|
||||
|
||||
@@ -29,7 +30,7 @@ test.describe('PDF export', () => {
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
await argosScreenshot(page, 'pdf - e2e - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
@@ -67,7 +68,7 @@ test.describe('PDF export', () => {
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
await argosScreenshot(page, 'pdf - gb-docs - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
@@ -109,7 +110,7 @@ test.describe('PDF export', () => {
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
await argosScreenshot(page, 'pdf - e2e - single-page', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
@@ -151,7 +152,7 @@ test.describe('PDF export', () => {
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
await argosScreenshot(page, 'pdf - gb-docs - single-page', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
import { type Page, expect, test } from '@playwright/test';
|
||||
|
||||
// Import the specific modules (not the package barrel) so this stays free of the `@/` path alias
|
||||
// that the store pulls in — Playwright's loader doesn't resolve it.
|
||||
import { SELECT_LIST_CAP, selectRankAttribute } from '../src/lib/select/constants';
|
||||
import { generateSelectCSS, selectSetClassName } from '../src/lib/select/generateSelectCSS';
|
||||
|
||||
/**
|
||||
* Behaviour tests for the `select` CSS: given a recency-ordered selection applied to `<html>`, a
|
||||
* group must show exactly the most-recently-activated of its options (its "first ranking"
|
||||
* selection), falling back to its default when none are active. These run in a real browser against
|
||||
* the actual generated CSS, so they assert observable visibility — not how the selectors are built.
|
||||
*/
|
||||
|
||||
/** Render a single group of option panes with the generated stylesheet. First slug = default. */
|
||||
async function renderGroup(page: Page, slugs: string[]) {
|
||||
const css = generateSelectCSS(slugs);
|
||||
const scope = selectSetClassName(slugs);
|
||||
const panes = slugs
|
||||
.map(
|
||||
(slug, index) =>
|
||||
`<div data-testid="pane-${slug}" data-select-option="${slug}"${index === 0 ? ' data-select-default' : ''}>${slug}</div>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
await page.setContent(
|
||||
`<!doctype html><html><head><style>${css}</style></head><body><div class="${scope}" data-select-group>${panes}</div></body></html>`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the recency list to `<html>` as `data-sel-*` attributes (most-recent first), via the shared
|
||||
* attribute-name helper — mirroring what the pre-paint script / store do at runtime.
|
||||
*/
|
||||
async function applySelection(page: Page, active: string[]) {
|
||||
for (const [rank, value] of active.entries()) {
|
||||
await page.evaluate(
|
||||
({ attr, value }) => document.documentElement.setAttribute(attr, value),
|
||||
{ attr: selectRankAttribute(rank), value }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Assert exactly one pane is visible, and it is the expected slug. */
|
||||
async function expectOnlyVisible(page: Page, slugs: string[], expectedSlug: string) {
|
||||
for (const slug of slugs) {
|
||||
const pane = page.getByTestId(`pane-${slug}`);
|
||||
if (slug === expectedSlug) {
|
||||
await expect(pane).toBeVisible();
|
||||
} else {
|
||||
await expect(pane).toBeHidden();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setup(page: Page, slugs: string[], active: string[]) {
|
||||
await renderGroup(page, slugs);
|
||||
await applySelection(page, active);
|
||||
}
|
||||
|
||||
test.describe('select CSS visibility', () => {
|
||||
const slugs = ['python', 'go', 'java'];
|
||||
|
||||
test('shows the default when nothing is selected', async ({ page }) => {
|
||||
await setup(page, slugs, []);
|
||||
await expectOnlyVisible(page, slugs, 'python'); // first pane is the default
|
||||
});
|
||||
|
||||
test('shows the selected option and hides the rest', async ({ page }) => {
|
||||
await setup(page, slugs, ['go']);
|
||||
await expectOnlyVisible(page, slugs, 'go');
|
||||
});
|
||||
|
||||
test('shows the most-recently-activated option of the group', async ({ page }) => {
|
||||
// Recency list is most-recent-first: `go` is more recent than `python`.
|
||||
await setup(page, slugs, ['go', 'python']);
|
||||
await expectOnlyVisible(page, slugs, 'go');
|
||||
|
||||
await setup(page, slugs, ['python', 'go']);
|
||||
await expectOnlyVisible(page, slugs, 'python');
|
||||
});
|
||||
|
||||
test('ignores more-recent selections that are not in the group', async ({ page }) => {
|
||||
// `dark` is more recent but not one of this group's options, so `go` still wins.
|
||||
await setup(page, slugs, ['dark', 'go', 'python']);
|
||||
await expectOnlyVisible(page, slugs, 'go');
|
||||
});
|
||||
|
||||
test('falls back to the default when no active slug is in the group', async ({ page }) => {
|
||||
await setup(page, slugs, ['dark', 'light']);
|
||||
await expectOnlyVisible(page, slugs, 'python');
|
||||
});
|
||||
|
||||
test('keeps symbol-bearing slugs (c / c++ / c#) distinct through the CSS selectors', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Slugs can contain `+` and `#` (see slugifySelectValue); they must survive quoted attribute
|
||||
// selectors without collapsing together.
|
||||
const symbols = ['c', 'c++', 'c#'];
|
||||
await setup(page, symbols, ['c++']);
|
||||
await expectOnlyVisible(page, symbols, 'c++');
|
||||
});
|
||||
|
||||
test('shows only the first pane when a group repeats a slug (duplicate tab names)', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Two panes share the slug `js`; activating it must reveal only the first, never both.
|
||||
const scope = selectSetClassName(['js', 'ts']);
|
||||
await page.setContent(
|
||||
`<!doctype html><html><head><style>${generateSelectCSS(['js', 'ts'])}</style></head><body><div class="${scope}" data-select-group><div data-testid="js-first" data-select-option="js" data-select-default>js 1</div><div data-testid="js-second" data-select-option="js">js 2</div><div data-testid="ts" data-select-option="ts">ts</div></div></body></html>`
|
||||
);
|
||||
await applySelection(page, ['js']);
|
||||
await expect(page.getByTestId('js-first')).toBeVisible();
|
||||
await expect(page.getByTestId('js-second')).toBeHidden();
|
||||
await expect(page.getByTestId('ts')).toBeHidden();
|
||||
});
|
||||
|
||||
test('a pinned pane overrides first-match (the duplicate the visitor clicked)', async ({
|
||||
page,
|
||||
}) => {
|
||||
// The client marks the clicked pane data-select-pinned and its same-slug sibling unpinned;
|
||||
// the pinned one must win over the first-match default.
|
||||
const scope = selectSetClassName(['js', 'ts']);
|
||||
await page.setContent(
|
||||
`<!doctype html><html><head><style>${generateSelectCSS(['js', 'ts'])}</style></head><body><div class="${scope}" data-select-group><div data-testid="js-first" data-select-option="js" data-select-default data-select-unpinned>js 1</div><div data-testid="js-second" data-select-option="js" data-select-pinned>js 2</div></div></body></html>`
|
||||
);
|
||||
await applySelection(page, ['js']);
|
||||
await expect(page.getByTestId('js-second')).toBeVisible();
|
||||
await expect(page.getByTestId('js-first')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
interface GroupSpec {
|
||||
id: string;
|
||||
slugs: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render several tab groups, each with clickable tab buttons wired to `__select` — an in-page
|
||||
* stand-in for the store's `activate()`/`mirrorToHtml()` (whose recency/dedupe/cap logic is unit
|
||||
* tested in store.test.ts). It prepends the clicked slug onto the `data-sel-*` recency list on
|
||||
* `<html>`, most-recent first. This keeps the test focused on the observable behaviour a visitor
|
||||
* sees — a real click switching every group that offers that option — driven by real browser CSS.
|
||||
*/
|
||||
async function renderGroups(page: Page, groups: GroupSpec[]) {
|
||||
const styles = [
|
||||
...new Map(
|
||||
groups.map((group) => [selectSetClassName(group.slugs), generateSelectCSS(group.slugs)])
|
||||
).values(),
|
||||
]
|
||||
.map((css) => `<style>${css}</style>`)
|
||||
.join('');
|
||||
|
||||
const markup = groups
|
||||
.map((group) => {
|
||||
const scope = selectSetClassName(group.slugs);
|
||||
const buttons = group.slugs
|
||||
.map(
|
||||
(slug) =>
|
||||
`<button data-testid="${group.id}-btn-${slug}" onclick="__select('${slug}')">${slug}</button>`
|
||||
)
|
||||
.join('');
|
||||
const panes = group.slugs
|
||||
.map(
|
||||
(slug, index) =>
|
||||
`<div data-testid="${group.id}-pane-${slug}" data-select-option="${slug}"${index === 0 ? ' data-select-default' : ''}>${slug}</div>`
|
||||
)
|
||||
.join('');
|
||||
return `<div class="${scope}" data-select-group><div role="tablist">${buttons}</div>${panes}</div>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
const selectScript = `window.__select=function(slug){var el=document.documentElement,cur=[],i,v;for(i=0;i<${SELECT_LIST_CAP};i++){v=el.getAttribute('data-sel-'+i);if(v)cur.push(v);}var next=[slug];for(i=0;i<cur.length;i++){if(cur[i]!==slug)next.push(cur[i]);}next=next.slice(0,${SELECT_LIST_CAP});for(i=0;i<${SELECT_LIST_CAP};i++){if(next[i])el.setAttribute('data-sel-'+i,next[i]);else el.removeAttribute('data-sel-'+i);}};`;
|
||||
|
||||
await page.setContent(
|
||||
`<!doctype html><html><head>${styles}<script>${selectScript}</script></head><body>${markup}</body></html>`
|
||||
);
|
||||
}
|
||||
|
||||
/** Assert a specific group shows exactly `expectedSlug` and hides its other options. */
|
||||
async function expectGroupShows(
|
||||
page: Page,
|
||||
groupId: string,
|
||||
slugs: string[],
|
||||
expectedSlug: string
|
||||
) {
|
||||
for (const slug of slugs) {
|
||||
const pane = page.getByTestId(`${groupId}-pane-${slug}`);
|
||||
if (slug === expectedSlug) {
|
||||
await expect(pane).toBeVisible();
|
||||
} else {
|
||||
await expect(pane).toBeHidden();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('select syncing across groups (click-driven)', () => {
|
||||
test('clicking a tab syncs every group offering that option', async ({ page }) => {
|
||||
const slugs = ['python', 'go'];
|
||||
await renderGroups(page, [
|
||||
{ id: 'a', slugs },
|
||||
{ id: 'b', slugs },
|
||||
]);
|
||||
|
||||
// Both groups start on their default (first) pane.
|
||||
await expectGroupShows(page, 'a', slugs, 'python');
|
||||
await expectGroupShows(page, 'b', slugs, 'python');
|
||||
|
||||
// Clicking a tab in group A switches group B too.
|
||||
await page.getByTestId('a-btn-go').click();
|
||||
await expectGroupShows(page, 'a', slugs, 'go');
|
||||
await expectGroupShows(page, 'b', slugs, 'go');
|
||||
|
||||
// And the sync works from either group.
|
||||
await page.getByTestId('b-btn-python').click();
|
||||
await expectGroupShows(page, 'a', slugs, 'python');
|
||||
await expectGroupShows(page, 'b', slugs, 'python');
|
||||
});
|
||||
|
||||
test('only groups that share the clicked option follow along', async ({ page }) => {
|
||||
const shared = ['python', 'go'];
|
||||
const other = ['go', 'rust'];
|
||||
await renderGroups(page, [
|
||||
{ id: 'a', slugs: shared },
|
||||
{ id: 'b', slugs: other },
|
||||
]);
|
||||
|
||||
// `rust` exists only in group B, so clicking it leaves group A on its default.
|
||||
await page.getByTestId('b-btn-rust').click();
|
||||
await expectGroupShows(page, 'b', other, 'rust');
|
||||
await expectGroupShows(page, 'a', shared, 'python');
|
||||
|
||||
// `go` is shared, so clicking it in A moves both groups.
|
||||
await page.getByTestId('a-btn-go').click();
|
||||
await expectGroupShows(page, 'a', shared, 'go');
|
||||
await expectGroupShows(page, 'b', other, 'go');
|
||||
});
|
||||
});
|
||||
|
||||
interface NestedSpec {
|
||||
outer: string[];
|
||||
inner: string[];
|
||||
/** Which of the outer options hosts the nested group. */
|
||||
host: string;
|
||||
/**
|
||||
* Emit the nested group's stylesheet before the outer one, as happens when a group with the
|
||||
* same option set appears earlier on the page and its deduped sheet lands in `<head>` first.
|
||||
*/
|
||||
innerStyleFirst?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a group nested inside one of another group's panes, mirroring the DOM `DynamicTabs`
|
||||
* produces: panes are direct children of the element carrying the set class, and a pane's body is
|
||||
* wrapped in a padding div before the nested group.
|
||||
*/
|
||||
async function renderNestedGroups(page: Page, spec: NestedSpec) {
|
||||
const { outer, inner, host, innerStyleFirst = false } = spec;
|
||||
const outerScope = selectSetClassName(outer);
|
||||
const innerScope = selectSetClassName(inner);
|
||||
|
||||
const innerPanes = inner
|
||||
.map(
|
||||
(slug, index) =>
|
||||
`<div data-testid="inner-pane-${slug}" data-select-option="${slug}"${index === 0 ? ' data-select-default' : ''}>${slug}</div>`
|
||||
)
|
||||
.join('');
|
||||
const innerGroup = `<div class="${innerScope}" data-select-group>${innerPanes}</div>`;
|
||||
|
||||
const outerPanes = outer
|
||||
.map(
|
||||
(slug, index) =>
|
||||
`<div data-testid="outer-pane-${slug}" data-select-option="${slug}"${index === 0 ? ' data-select-default' : ''}><div>${slug}${slug === host ? innerGroup : ''}</div></div>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
const styles = [generateSelectCSS(outer), generateSelectCSS(inner)];
|
||||
if (innerStyleFirst) {
|
||||
styles.reverse();
|
||||
}
|
||||
|
||||
await page.setContent(
|
||||
`<!doctype html><html><head>${styles.map((css) => `<style>${css}</style>`).join('')}</head><body><div class="${outerScope}" data-select-group>${outerPanes}</div></body></html>`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A group's stylesheet must resolve only its own panes. Because every pane of a nested group is also
|
||||
* a descendant of the outer group, a sheet that reached descendants instead of children would hide
|
||||
* the nested panes whenever an outer option was active, leaving the nested tab bar with an empty body.
|
||||
*/
|
||||
test.describe('select CSS visibility in nested groups', () => {
|
||||
const outer = ['macos', 'windows'];
|
||||
const inner = ['npm', 'yarn'];
|
||||
|
||||
test('shows both defaults when nothing is selected', async ({ page }) => {
|
||||
await renderNestedGroups(page, { outer, inner, host: 'macos' });
|
||||
await expect(page.getByTestId('outer-pane-macos')).toBeVisible();
|
||||
await expect(page.getByTestId('inner-pane-npm')).toBeVisible();
|
||||
await expect(page.getByTestId('inner-pane-yarn')).toBeHidden();
|
||||
});
|
||||
|
||||
test('keeps the nested group resolved when an outer option is activated', async ({ page }) => {
|
||||
await renderNestedGroups(page, { outer, inner, host: 'macos' });
|
||||
await applySelection(page, ['macos']);
|
||||
await expect(page.getByTestId('outer-pane-macos')).toBeVisible();
|
||||
await expect(page.getByTestId('inner-pane-npm')).toBeVisible();
|
||||
await expect(page.getByTestId('inner-pane-yarn')).toBeHidden();
|
||||
});
|
||||
|
||||
test('resolves a nested group hosted by a non-default outer option', async ({ page }) => {
|
||||
await renderNestedGroups(page, { outer, inner, host: 'windows' });
|
||||
await applySelection(page, ['windows']);
|
||||
await expect(page.getByTestId('outer-pane-windows')).toBeVisible();
|
||||
await expect(page.getByTestId('inner-pane-npm')).toBeVisible();
|
||||
await expect(page.getByTestId('inner-pane-yarn')).toBeHidden();
|
||||
});
|
||||
|
||||
test('resolves each group against its own options', async ({ page }) => {
|
||||
await renderNestedGroups(page, { outer, inner, host: 'macos' });
|
||||
await applySelection(page, ['yarn', 'macos']);
|
||||
await expect(page.getByTestId('outer-pane-macos')).toBeVisible();
|
||||
await expect(page.getByTestId('inner-pane-yarn')).toBeVisible();
|
||||
await expect(page.getByTestId('inner-pane-npm')).toBeHidden();
|
||||
});
|
||||
|
||||
test('resolves the same way whichever stylesheet comes first', async ({ page }) => {
|
||||
await renderNestedGroups(page, { outer, inner, host: 'macos', innerStyleFirst: true });
|
||||
await applySelection(page, ['yarn', 'macos']);
|
||||
await expect(page.getByTestId('outer-pane-macos')).toBeVisible();
|
||||
await expect(page.getByTestId('inner-pane-yarn')).toBeVisible();
|
||||
await expect(page.getByTestId('inner-pane-npm')).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
import { type CDPSession, type Locator, type Page, expect, test } from '@playwright/test';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
import { getContentTestURL } from '../tests/utils';
|
||||
import { waitForCookiesDialog } from './util';
|
||||
|
||||
/** Already covered by `customers.spec.ts`, and among the largest pages we render. */
|
||||
const LARGE_PAGE_URL = 'https://docs.snyk.io/snyk-api/reference/apps';
|
||||
|
||||
/** Table rows the preview workflow pastes into a PR comment. Relative to the package, gitignored. */
|
||||
const REPORT_FILE = 'test-results/style-perf.md';
|
||||
|
||||
// Share of the page one popup may restyle: a ratio so it survives the page growing, a count rather
|
||||
// than a duration so it does not move with CI machine speed. `search` is above 1 because it still
|
||||
// restyles the whole document — a ratchet against today's state, to lower as families get fixed.
|
||||
const RESTYLE_BUDGET_RATIO = {
|
||||
'openapi-select': 0.25,
|
||||
search: 1.25,
|
||||
};
|
||||
|
||||
type TraceEvent = { name: string; args?: { elementCount?: number } };
|
||||
|
||||
/** Elements Blink restyled while `action` ran — DevTools reports this as "Elements affected". */
|
||||
async function countRestyledElements(
|
||||
page: Page,
|
||||
client: CDPSession,
|
||||
action: () => Promise<void>
|
||||
): Promise<number> {
|
||||
// Style is computed lazily, so settle queued recalcs or earlier work lands in the trace.
|
||||
await page.evaluate(() => void document.body.offsetHeight);
|
||||
|
||||
await client.send('Tracing.start', {
|
||||
categories: 'disabled-by-default-devtools.timeline',
|
||||
transferMode: 'ReturnAsStream',
|
||||
});
|
||||
|
||||
await action();
|
||||
await page.evaluate(() => void document.body.offsetHeight);
|
||||
|
||||
const handle = await new Promise<string>((resolve) => {
|
||||
client.once('Tracing.tracingComplete', (event) => resolve(event.stream as string));
|
||||
void client.send('Tracing.end');
|
||||
});
|
||||
|
||||
let raw = '';
|
||||
for (let eof = false; !eof;) {
|
||||
const chunk = await client.send('IO.read', { handle });
|
||||
raw += chunk.data;
|
||||
eof = chunk.eof;
|
||||
}
|
||||
await client.send('IO.close', { handle });
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
const events: TraceEvent[] = Array.isArray(parsed) ? parsed : parsed.traceEvents;
|
||||
|
||||
return events
|
||||
.filter((event) => event.name === 'UpdateLayoutTree')
|
||||
.reduce((total, event) => total + (event.args?.elementCount ?? 0), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure a *re*open: the first open also pays for mounting the popup, and waiting on anything
|
||||
* looser than the popup being hidden again undercounts the reopen by ~10x.
|
||||
*/
|
||||
async function measureReopen(
|
||||
page: Page,
|
||||
client: CDPSession,
|
||||
open: () => Promise<void>,
|
||||
popup: Locator
|
||||
): Promise<number> {
|
||||
await open();
|
||||
await expect(popup).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(popup).toBeHidden();
|
||||
|
||||
const restyled = await countRestyledElements(page, client, open);
|
||||
await expect(popup, 'the measurement is meaningless if the popup did not open').toBeVisible();
|
||||
|
||||
// Leave the page as we found it, so the next measurement is not measuring this popup's teardown.
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(popup).toBeHidden();
|
||||
|
||||
return restyled;
|
||||
}
|
||||
|
||||
type Measurement = { name: keyof typeof RESTYLE_BUDGET_RATIO; restyled: number };
|
||||
|
||||
function expectWithinBudgets(measurements: Measurement[], total: number) {
|
||||
const count = (value: number) => value.toLocaleString('en-US');
|
||||
|
||||
// Reported before asserting, so a blown budget still reaches the PR comment.
|
||||
mkdirSync(dirname(REPORT_FILE), { recursive: true });
|
||||
writeFileSync(
|
||||
REPORT_FILE,
|
||||
measurements
|
||||
.map(({ name, restyled }) => {
|
||||
const ratio = restyled / total;
|
||||
const budget = RESTYLE_BUDGET_RATIO[name];
|
||||
const status = ratio < budget ? '✅' : '❌';
|
||||
return `| \`${name}\` | ${count(restyled)} | ${count(total)} | ${(ratio * 100).toFixed(1)}% | ${(budget * 100).toFixed(0)}% | ${status} |\n`;
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
|
||||
// Soft, so one breach still reports the other interaction rather than masking it.
|
||||
for (const { name, restyled } of measurements) {
|
||||
expect
|
||||
.soft(restyled / total, `${name} restyled ${restyled} of ${total} elements`)
|
||||
.toBeLessThan(RESTYLE_BUDGET_RATIO[name]);
|
||||
}
|
||||
}
|
||||
|
||||
const countElements = (page: Page) =>
|
||||
page.evaluate(() => document.getElementsByTagName('*').length);
|
||||
|
||||
// Not `networkidle`: third-party subresources on this customer site can hang, so it never settles.
|
||||
async function waitForStableElementCount(page: Page): Promise<number> {
|
||||
let previous = await countElements(page);
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const current = await countElements(page);
|
||||
const stable = current === previous;
|
||||
previous = current;
|
||||
return stable;
|
||||
},
|
||||
{ message: 'the tree never stopped changing', timeout: 15_000 }
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
async function openLargePage(page: Page) {
|
||||
await page.goto(getContentTestURL(LARGE_PAGE_URL));
|
||||
await waitForCookiesDialog(page);
|
||||
|
||||
// Measure a settled page: before hydration the tree is smaller and no popup can open at all.
|
||||
await page.locator('html.hydrated').waitFor();
|
||||
await expect(page.getByLabel('OpenAPI Select').first()).toBeAttached();
|
||||
|
||||
const totalElements = await waitForStableElementCount(page);
|
||||
const client = await page.context().newCDPSession(page);
|
||||
|
||||
return { client, totalElements };
|
||||
}
|
||||
|
||||
// Guards the harness: if an idle window is busy, background work is leaking into the measurements
|
||||
// and they mean nothing. Closing a popup leaves a few elements of teardown, so this is not zero —
|
||||
// it only has to sit far below a real measurement (hundreds) and a whole-document restyle (~11k).
|
||||
const IDLE_RESTYLE_TOLERANCE = 100;
|
||||
|
||||
async function expectIdle(page: Page, client: CDPSession) {
|
||||
const restyled = await countRestyledElements(page, client, async () => {});
|
||||
expect(restyled, 'an idle page should barely restyle').toBeLessThan(IDLE_RESTYLE_TOLERANCE);
|
||||
}
|
||||
|
||||
// Both checks below probe a block's children, never the block itself: `content-visibility: auto`
|
||||
// skips an element's *contents*, so the element carrying it keeps reporting visible and every block
|
||||
// would look rendered. Same reason `toBeVisible()` is no use here — a skipped child still has a box.
|
||||
function countRenderedBlocks(page: Page): Promise<number> {
|
||||
return page.evaluate(
|
||||
() =>
|
||||
[...document.querySelectorAll('.openapi-block')].filter((block) =>
|
||||
[...block.children].some((child) =>
|
||||
child.checkVisibility({ contentVisibilityAuto: true })
|
||||
)
|
||||
).length
|
||||
);
|
||||
}
|
||||
|
||||
test('off-screen OpenAPI blocks skip their rendering work', async ({ page }) => {
|
||||
await openLargePage(page);
|
||||
|
||||
const blocks = page.locator('.openapi-block');
|
||||
const total = await blocks.count();
|
||||
expect(total, 'the fixture needs enough blocks for some to sit off-screen').toBeGreaterThan(4);
|
||||
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
const rendered = await countRenderedBlocks(page);
|
||||
expect(
|
||||
rendered,
|
||||
`${rendered} of ${total} blocks rendered from the top of the page`
|
||||
).toBeLessThan(total / 2);
|
||||
|
||||
// Un-skipping on approach is what keeps #anchors and find-in-page working.
|
||||
const last = blocks.last();
|
||||
await last.scrollIntoViewIfNeeded();
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
last.evaluate((block) =>
|
||||
[...block.children].some((child) =>
|
||||
child.checkVisibility({ contentVisibilityAuto: true })
|
||||
)
|
||||
),
|
||||
{ message: 'the last block never rendered after being scrolled to' }
|
||||
)
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test('opening a popup restyles a bounded part of a large API reference', async ({ page }) => {
|
||||
const { client, totalElements } = await openLargePage(page);
|
||||
await expectIdle(page, client);
|
||||
|
||||
const trigger = page.getByLabel('OpenAPI Select').first();
|
||||
await trigger.scrollIntoViewIfNeeded();
|
||||
const select = await measureReopen(
|
||||
page,
|
||||
client,
|
||||
() => trigger.click(),
|
||||
page.locator('.openapi-select-popover')
|
||||
);
|
||||
|
||||
await expectIdle(page, client);
|
||||
|
||||
const searchInput = page.getByTestId('search-input');
|
||||
const search = await measureReopen(
|
||||
page,
|
||||
client,
|
||||
() => searchInput.focus(),
|
||||
page.getByTestId('search-results')
|
||||
);
|
||||
|
||||
expectWithinBudgets(
|
||||
[
|
||||
{ name: 'openapi-select', restyled: select },
|
||||
{ name: 'search', restyled: search },
|
||||
],
|
||||
totalElements
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import { type Page, expect, test } from '@playwright/test';
|
||||
|
||||
// Import the specific module (not the package barrel) so this stays free of the `@/` path alias,
|
||||
// which Playwright's loader doesn't resolve — same reason as `select.spec.ts`.
|
||||
import { resolveOverflowingItems } from '../src/components/hooks/listOverflow';
|
||||
|
||||
/**
|
||||
* Behaviour tests for the tab bar's overflow rule (`useListOverflow`), which decides which tabs move
|
||||
* into the "more" dropdown. Rects are measured in a real browser so the geometry is genuine — the
|
||||
* layout below mirrors the tab bar in `DynamicTabs`: a non-wrapping flex row of `shrink-0` items,
|
||||
* clipped by `overflow: hidden`, measured with the dropdown rendered ahead of the tabs.
|
||||
*
|
||||
* The regression these guard is a dropdown appearing when nothing actually overflowed: measuring
|
||||
* with the dropdown present consumes `MENU` pixels, so a bar whose tabs total just under the
|
||||
* container would hand its last tab to a menu it never needed.
|
||||
*/
|
||||
|
||||
/** Width of the ellipsis button, matching the `px-3.5` + `size-4` icon of the real one. */
|
||||
const MENU = 44;
|
||||
|
||||
interface Row {
|
||||
/** Container width in px. */
|
||||
container: number;
|
||||
/** Tab widths in px, in order. */
|
||||
tabs: number[];
|
||||
/** Whether the dropdown is reserving space ahead of the tabs, as during a measure pass. */
|
||||
withMenu?: boolean;
|
||||
/** Hide an ancestor, so the row measures with no box at all. */
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lay the row out in the browser and run the real rule over the rects it produces.
|
||||
* Returns the ids reported as overflowing, or `null` when the measurement carried no information.
|
||||
*/
|
||||
async function measure(page: Page, row: Row): Promise<string[] | null> {
|
||||
const { container, tabs, withMenu = true, hidden = false } = row;
|
||||
|
||||
const items = tabs
|
||||
.map((w, i) => `<div class="item" id="tab-${i}" style="width:${w}px">${i}</div>`)
|
||||
.join('');
|
||||
const menu = withMenu ? `<div class="item" style="width:${MENU}px">…</div>` : '';
|
||||
|
||||
await page.setContent(
|
||||
`<!doctype html><html><head><style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; }
|
||||
.pane { ${hidden ? 'display: none;' : ''} }
|
||||
.bar {
|
||||
width: ${container}px;
|
||||
display: inline-flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bar::after { content: ""; flex: 1; }
|
||||
.item { flex-shrink: 0; max-width: 100%; }
|
||||
</style></head><body>
|
||||
<div class="pane"><div class="bar" id="bar">${menu}${items}</div></div>
|
||||
</body></html>`
|
||||
);
|
||||
|
||||
const measured = await page.evaluate(() => {
|
||||
const bar = document.getElementById('bar');
|
||||
if (!bar) {
|
||||
throw new Error('missing bar');
|
||||
}
|
||||
const rect = bar.getBoundingClientRect();
|
||||
return {
|
||||
container: { left: rect.left, right: rect.right, width: rect.width },
|
||||
items: [...bar.querySelectorAll<HTMLElement>('.item[id]')].map((el) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return { id: el.id, rect: { left: r.left, right: r.right } };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const result = resolveOverflowingItems(measured.container, measured.items);
|
||||
return result ? [...result].sort() : null;
|
||||
}
|
||||
|
||||
test.describe('tab overflow rule', () => {
|
||||
test('reports nothing when every tab fits with room to spare', async ({ page }) => {
|
||||
// 3 x 100 = 300 of 600, so even with the menu reserved there is slack.
|
||||
expect(await measure(page, { container: 600, tabs: [100, 100, 100] })).toEqual([]);
|
||||
});
|
||||
|
||||
test('reports nothing when the tabs fit exactly', async ({ page }) => {
|
||||
expect(await measure(page, { container: 300, tabs: [100, 100, 100] })).toEqual([]);
|
||||
});
|
||||
|
||||
test('reports nothing when only the reserved menu made the row overflow', async ({ page }) => {
|
||||
// The regression: tabs total 300 and the container is 320, so they fit — but measuring
|
||||
// reserves 44 for the menu, which used to push the last tab out and show a needless
|
||||
// dropdown. Every width in `container - MENU < 300 <= container` must stay empty.
|
||||
for (const container of [300, 305, 320, 330, 343]) {
|
||||
expect(
|
||||
await measure(page, { container, tabs: [100, 100, 100] }),
|
||||
`container ${container}px`
|
||||
).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test('reports the tabs that genuinely do not fit alongside the menu', async ({ page }) => {
|
||||
// 300 of tabs into 290: the row really does overflow, so the menu is warranted and the
|
||||
// remaining tabs must fit beside it (100 + 100 + 44 = 244 <= 290).
|
||||
expect(await measure(page, { container: 290, tabs: [100, 100, 100] })).toEqual(['tab-2']);
|
||||
});
|
||||
|
||||
test('gives up as many tabs as the width demands', async ({ page }) => {
|
||||
expect(await measure(page, { container: 190, tabs: [100, 100, 100] })).toEqual([
|
||||
'tab-1',
|
||||
'tab-2',
|
||||
]);
|
||||
expect(await measure(page, { container: 150, tabs: [100, 100, 100] })).toEqual([
|
||||
'tab-1',
|
||||
'tab-2',
|
||||
]);
|
||||
});
|
||||
|
||||
test('moves every tab into the menu once not even the first fits beside it', async ({
|
||||
page,
|
||||
}) => {
|
||||
// 100 + 44 > 120, so no tab can share the row with the menu. Everything goes in, leaving a
|
||||
// bar that is only the menu — deliberately, since the menu is then the sole route to any
|
||||
// tab. Forcing the first tab to stay would push the menu past the clipped edge and strand
|
||||
// the rest.
|
||||
expect(await measure(page, { container: 120, tabs: [100, 100, 100] })).toEqual([
|
||||
'tab-0',
|
||||
'tab-1',
|
||||
'tab-2',
|
||||
]);
|
||||
});
|
||||
|
||||
test('keeps a single tab that fills the bar rather than hiding it behind a menu', async ({
|
||||
page,
|
||||
}) => {
|
||||
// `max-width: 100%` truncates it to the container, so it fits — a lone tab should never be
|
||||
// the only thing in the dropdown.
|
||||
expect(await measure(page, { container: 200, tabs: [400] })).toEqual([]);
|
||||
});
|
||||
|
||||
test('cuts a nested bar earlier, since its pane padding narrows it', async ({ page }) => {
|
||||
// A nested tab bar sits inside a `p-4` pane, so it has 32px less to work with. At 330 the
|
||||
// outer bar keeps all three tabs; the nested one at 330 - 32 cannot.
|
||||
expect(await measure(page, { container: 330, tabs: [100, 100, 100] })).toEqual([]);
|
||||
expect(await measure(page, { container: 330 - 32, tabs: [100, 100, 100] })).toEqual([
|
||||
'tab-2',
|
||||
]);
|
||||
});
|
||||
|
||||
test('progressively fills the menu as a long list is squeezed', async ({ page }) => {
|
||||
const tabs = Array.from({ length: 12 }, () => 100);
|
||||
let previous = -1;
|
||||
for (const container of [1300, 1200, 1000, 800, 600, 400, 200]) {
|
||||
const overflowing = await measure(page, { container, tabs });
|
||||
expect(overflowing, `container ${container}px`).not.toBeNull();
|
||||
const hidden = overflowing?.length ?? 0;
|
||||
// Never loses a tab, and never un-hides one as the space shrinks.
|
||||
expect(hidden, `container ${container}px`).toBeGreaterThanOrEqual(previous);
|
||||
expect(hidden, `container ${container}px`).toBeLessThanOrEqual(tabs.length);
|
||||
previous = hidden;
|
||||
}
|
||||
// Widest fits everything; at 200 only the first tab still fits beside the menu.
|
||||
expect(await measure(page, { container: 1300, tabs })).toEqual([]);
|
||||
expect((await measure(page, { container: 200, tabs }))?.length).toBe(11);
|
||||
});
|
||||
|
||||
test('reports nothing measurable while an ancestor is hidden', async ({ page }) => {
|
||||
// A bar behind an inactive tab has no box, so every rect is zero. That says nothing about
|
||||
// what fits, and must not be mistaken for "everything overflows".
|
||||
expect(
|
||||
await measure(page, { container: 200, tabs: [100, 100, 100], hidden: true })
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('reports nothing measurable for an empty list', async ({ page }) => {
|
||||
expect(await measure(page, { container: 600, tabs: [] })).toBeNull();
|
||||
});
|
||||
});
|
||||
+169
-34
@@ -1,29 +1,39 @@
|
||||
import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import {
|
||||
type BrowserContext,
|
||||
type FrameLocator,
|
||||
type Page,
|
||||
type Response,
|
||||
expect,
|
||||
test,
|
||||
} from '@playwright/test';
|
||||
import deepMerge from 'deepmerge';
|
||||
import rison from 'rison';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
|
||||
import {
|
||||
CustomizationAIMode,
|
||||
CustomizationBackground,
|
||||
CustomizationCodeTheme,
|
||||
CustomizationCorners,
|
||||
CustomizationDefaultFont,
|
||||
CustomizationDefaultMonospaceFont,
|
||||
CustomizationDefaultThemeMode,
|
||||
CustomizationDepth,
|
||||
type CustomizationHeaderItem,
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationIconsStyle,
|
||||
CustomizationLinksStyle,
|
||||
CustomizationLocale,
|
||||
CustomizationPageActionType,
|
||||
CustomizationSearchStyle,
|
||||
CustomizationSidebarBackgroundStyle,
|
||||
CustomizationSidebarListStyle,
|
||||
CustomizationTheme,
|
||||
CustomizationThemeMode,
|
||||
type CustomizationThemedColor,
|
||||
type SiteCustomizationSettings,
|
||||
SiteExternalLinksTarget,
|
||||
} from '@gitbook/api';
|
||||
import { type BrowserContext, type Page, type Response, expect, test } from '@playwright/test';
|
||||
import deepMerge from 'deepmerge';
|
||||
import rison from 'rison';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
|
||||
import { getContentTestURL, getTestURL } from '../tests/utils';
|
||||
|
||||
@@ -38,6 +48,16 @@ export interface Test {
|
||||
* Test to run
|
||||
*/
|
||||
run?: (page: Page, response: Response | null) => Promise<unknown>;
|
||||
/**
|
||||
* Re-applied right before every viewport screenshot (after Argos
|
||||
* stabilization), so it survives re-renders triggered by viewport resizing.
|
||||
*
|
||||
* Use this — rather than mutating the DOM once in `run` — to normalize
|
||||
* non-deterministic content (e.g. AI responses). A one-time mutation in `run`
|
||||
* is clobbered when React re-renders on resize (e.g. crossing the mobile
|
||||
* breakpoint), so only the first viewport ends up normalized.
|
||||
*/
|
||||
normalizeBeforeScreenshot?: (page: Page) => Promise<void> | void;
|
||||
/**
|
||||
* Mode for the test.
|
||||
*/
|
||||
@@ -72,8 +92,12 @@ export interface Test {
|
||||
export type TestsCase = {
|
||||
name: string;
|
||||
skip?: boolean;
|
||||
tests: Array<Test>;
|
||||
tests: Test[];
|
||||
contentBaseURL?: string;
|
||||
/**
|
||||
* Whether screenshots in this test case should capture the full scrollable page by default.
|
||||
*/
|
||||
fullPage?: boolean;
|
||||
};
|
||||
|
||||
export const allLocales: CustomizationLocale[] = [
|
||||
@@ -83,15 +107,15 @@ export const allLocales: CustomizationLocale[] = [
|
||||
CustomizationLocale.Zh,
|
||||
];
|
||||
|
||||
export const allThemeModes: CustomizationThemeMode[] = [
|
||||
CustomizationThemeMode.Light,
|
||||
CustomizationThemeMode.Dark,
|
||||
export const allThemeModes: CustomizationDefaultThemeMode[] = [
|
||||
CustomizationDefaultThemeMode.Light,
|
||||
CustomizationDefaultThemeMode.Dark,
|
||||
];
|
||||
|
||||
export const allTintColors: Array<{
|
||||
export const allTintColors: {
|
||||
label: string;
|
||||
value: CustomizationThemedColor | undefined;
|
||||
}> = [
|
||||
}[] = [
|
||||
{
|
||||
label: 'Off',
|
||||
value: undefined,
|
||||
@@ -143,7 +167,27 @@ export const headerLinks: CustomizationHeaderItem[] = [
|
||||
|
||||
export async function waitForCookiesDialog(page: Page) {
|
||||
const dialog = page.getByTestId('cookies-dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog).toBeVisible({
|
||||
// Cookies dialog may take some times to appear
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForHydration(page: Page) {
|
||||
await page.locator('html.hydrated').waitFor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the GitBook admin toolbar to be present.
|
||||
*
|
||||
* The toolbar only renders when signed in to GitBook. It is hidden from
|
||||
* screenshots (see `argosCSS`) because it auto-expands with an animation, so
|
||||
* use this to assert it is rendered without capturing its flaky visual state.
|
||||
*/
|
||||
export async function waitForAdminToolbar(page: Page) {
|
||||
await expect(page.getByTestId('admin-toolbar')).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForNotFound(_page: Page, response: Response | null) {
|
||||
@@ -151,6 +195,45 @@ export async function waitForNotFound(_page: Page, response: Response | null) {
|
||||
expect(response?.status()).toBe(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an AI chat response to be fully settled before asserting or
|
||||
* screenshotting it.
|
||||
*
|
||||
* The chat exposes `aria-busy` on its container (`[data-testid="ai-chat"]`),
|
||||
* which stays true from the moment a message is sent until the stream — including
|
||||
* the follow-up suggestion phase — completes. Gating on it avoids the two main
|
||||
* sources of flakiness: capturing a "thinking" placeholder or a half-streamed
|
||||
* answer, and running the content normalization while React is still re-rendering
|
||||
* (which would clobber the replacements).
|
||||
*
|
||||
* Argos also waits for `aria-busy` to clear during its own stabilization
|
||||
* (`waitForAriaBusy`), so this is both an explicit gate and a backstop.
|
||||
*
|
||||
* Accepts a `Page` or a `FrameLocator` (for the embedded assistant in an iframe).
|
||||
*/
|
||||
export async function waitForAIChatResponse(scope: Page | FrameLocator) {
|
||||
await expect(scope.getByTestId('ai-chat')).toHaveAttribute('aria-busy', 'false', {
|
||||
timeout: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export async function setTimeToMorning(page: Page) {
|
||||
const now = new Date();
|
||||
now.setHours(8, 0, 0, 0); // 8:00:00.000 AM (local time)
|
||||
|
||||
await page.clock.install({ time: now });
|
||||
}
|
||||
|
||||
export async function waitForCoverImages(page: Page, options?: { darkMode?: boolean }) {
|
||||
const selector = options?.darkMode
|
||||
? 'img[alt="Page cover"].dark\\:inline'
|
||||
: 'img[alt="Page cover"]:not(.dark\\:inline)';
|
||||
// Wait for cover images to exist (not the shimmer placeholder)
|
||||
await expect(page.locator(selector)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform test cases into Playwright tests and run it.
|
||||
*/
|
||||
@@ -183,6 +266,19 @@ export function runTestCases(testCases: TestsCase[]) {
|
||||
);
|
||||
}
|
||||
|
||||
// Reset the cross-space navigation state on every document load so the
|
||||
// "Back to <space>" shortcut never leaks between navigations/tests. It is
|
||||
// detected client-side from this sessionStorage, and a stale value (e.g.
|
||||
// after a retry or a cross-space redirect) makes it appear or not
|
||||
// non-deterministically, causing flaky screenshots.
|
||||
await page.addInitScript(() => {
|
||||
try {
|
||||
sessionStorage.removeItem('gitbook-space-navigation:last');
|
||||
sessionStorage.removeItem('gitbook-space-navigation:back');
|
||||
sessionStorage.removeItem('gitbook-space-navigation:from-picker');
|
||||
} catch {}
|
||||
});
|
||||
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
@@ -198,7 +294,11 @@ export function runTestCases(testCases: TestsCase[]) {
|
||||
}
|
||||
});
|
||||
|
||||
const response = await page.goto(url);
|
||||
// Wait only for `domcontentloaded` rather than the default `load`: these
|
||||
// are real customer sites whose third-party subresources can hang and
|
||||
// never fire `load`, aborting the navigation. Argos stabilization (run in
|
||||
// `beforeScreenshot`) still waits for images/fonts before capturing.
|
||||
const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
if (testEntry.run) {
|
||||
await testEntry.run(page, response);
|
||||
}
|
||||
@@ -219,15 +319,24 @@ export function runTestCases(testCases: TestsCase[]) {
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
/* Hide the GitBook admin toolbar: it auto-expands with an
|
||||
animation, so its state at capture time is non-deterministic.
|
||||
Its presence is asserted separately via waitForAdminToolbar. */
|
||||
[data-testid="admin-toolbar"] {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: screenshotOptions?.threshold ?? undefined,
|
||||
fullPage: testEntry.fullPage ?? false,
|
||||
fullPage: testEntry.fullPage ?? testCase.fullPage ?? false,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
if (screenshotOptions?.waitForTOCScrolling !== false) {
|
||||
await waitForTOCScrolling(page);
|
||||
}
|
||||
await waitForIcons(page);
|
||||
// Re-apply per viewport, last — after any resize-driven
|
||||
// re-render — so normalized content survives to capture.
|
||||
await testEntry.normalizeBeforeScreenshot?.(page);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -288,6 +397,16 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
background: CustomizationBackground.Plain,
|
||||
icons: CustomizationIconsStyle.Regular,
|
||||
links: CustomizationLinksStyle.Default,
|
||||
codeTheme: {
|
||||
default: {
|
||||
light: CustomizationCodeTheme.DefaultLight,
|
||||
dark: CustomizationCodeTheme.DefaultDark,
|
||||
},
|
||||
openapi: {
|
||||
light: CustomizationCodeTheme.DefaultLight,
|
||||
dark: CustomizationCodeTheme.DefaultDark,
|
||||
},
|
||||
},
|
||||
sidebar: {
|
||||
background: CustomizationSidebarBackgroundStyle.Default,
|
||||
list: CustomizationSidebarListStyle.Default,
|
||||
@@ -309,12 +428,9 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
groups: [],
|
||||
},
|
||||
themes: {
|
||||
default: CustomizationThemeMode.Light,
|
||||
default: CustomizationDefaultThemeMode.System,
|
||||
toggeable: true,
|
||||
},
|
||||
pdf: {
|
||||
enabled: true,
|
||||
},
|
||||
feedback: {
|
||||
enabled: false,
|
||||
},
|
||||
@@ -327,15 +443,17 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
advancedCustomization: {
|
||||
enabled: true,
|
||||
},
|
||||
git: {
|
||||
showEditLink: false,
|
||||
},
|
||||
pagination: {
|
||||
enabled: true,
|
||||
},
|
||||
pageActions: {
|
||||
externalAI: true,
|
||||
markdown: true,
|
||||
items: [
|
||||
CustomizationPageActionType.Assistant,
|
||||
CustomizationPageActionType.Markdown,
|
||||
CustomizationPageActionType.ExternalAi,
|
||||
CustomizationPageActionType.Mcp,
|
||||
CustomizationPageActionType.Pdf,
|
||||
],
|
||||
},
|
||||
trademark: {
|
||||
enabled: true,
|
||||
@@ -344,9 +462,14 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
url: 'https://www.gitbook.com/privacy',
|
||||
},
|
||||
socialPreview: {},
|
||||
socialAccounts: [],
|
||||
};
|
||||
|
||||
const encoded = rison.encode_object(deepMerge(DEFAULT_CUSTOMIZATION, partial));
|
||||
const encoded = rison.encode_object(
|
||||
deepMerge(DEFAULT_CUSTOMIZATION, partial, {
|
||||
arrayMerge: (_target, source) => source,
|
||||
})
|
||||
);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('customization', encoded);
|
||||
@@ -359,11 +482,13 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
*/
|
||||
export async function waitForIcons(page: Page) {
|
||||
await page.waitForFunction(() => {
|
||||
const urlStates: Record<
|
||||
type IconURLStates = Record<
|
||||
string,
|
||||
{ state: 'pending'; uri: null } | { state: 'loaded'; uri: string }
|
||||
> = (window as any).__ICONS_STATES__ || {};
|
||||
(window as any).__ICONS_STATES__ = urlStates;
|
||||
>;
|
||||
const iconStatesWindow = window as Window & { __ICONS_STATES__?: IconURLStates };
|
||||
const urlStates: IconURLStates = iconStatesWindow.__ICONS_STATES__ || {};
|
||||
iconStatesWindow.__ICONS_STATES__ = urlStates;
|
||||
|
||||
const fetchSvgAsDataUri = async (url: string): Promise<string> => {
|
||||
const response = await fetch(url);
|
||||
@@ -406,14 +531,23 @@ export async function waitForIcons(page: Page) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/light/moon.svg?v=2&token=a463935e93")
|
||||
const maskImage = window.getComputedStyle(icon).getPropertyValue('mask-image');
|
||||
const urlMatch = maskImage.match(/url\("([^"]+)"\)/);
|
||||
const url = urlMatch?.[1];
|
||||
const maskImage = icon.querySelector('[data-testid="mask-image"]');
|
||||
if (!maskImage) {
|
||||
const inlineContent = icon.querySelector(
|
||||
'path, circle, ellipse, line, polygon, polyline, rect, g, use'
|
||||
);
|
||||
if (inlineContent) {
|
||||
icon.setAttribute('data-argos-state', 'loaded');
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error('Icon has no inline SVG content or mask-image element');
|
||||
}
|
||||
|
||||
const url = maskImage.getAttribute('href');
|
||||
// If URL is invalid we throw an error.
|
||||
if (!url) {
|
||||
throw new Error('No mask-image');
|
||||
throw new Error('No mask-image url');
|
||||
}
|
||||
|
||||
// If the URL is already queued for loading, we return the state.
|
||||
@@ -443,7 +577,8 @@ export async function waitForIcons(page: Page) {
|
||||
*/
|
||||
async function waitForTOCScrolling(page: Page) {
|
||||
const viewport = await page.viewportSize();
|
||||
if (viewport && viewport.width >= 1024) {
|
||||
if (viewport && viewport.width >= 1024 && !page.url().includes('~gitbook/embed/demo')) {
|
||||
// The embed demo is an iframe, which means the viewport is only a fraction of the main document. So there is no open TOC to scroll to.
|
||||
const toc = page.getByTestId('table-of-contents');
|
||||
await expect(toc).toBeVisible();
|
||||
await page.evaluate(() => {
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
// @ts-check
|
||||
|
||||
import { networkInterfaces } from 'node:os';
|
||||
|
||||
// Next blocks its dev client and HMR when a physical device opens the server over a LAN address.
|
||||
// Needed to hydrate the dev client on physical phones/tablets over the internal network
|
||||
const allowedDevOrigins =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? [
|
||||
...new Set(
|
||||
Object.values(networkInterfaces())
|
||||
.flat()
|
||||
.filter(
|
||||
(networkInterface) =>
|
||||
networkInterface &&
|
||||
!networkInterface.internal &&
|
||||
networkInterface.family === 'IPv4'
|
||||
)
|
||||
.map((networkInterface) => networkInterface?.address)
|
||||
),
|
||||
]
|
||||
: undefined;
|
||||
|
||||
// We don't use the deployment ID yet on 2c, we need to remove it because of https://github.com/opennextjs/opennextjs-aws/issues/1136
|
||||
let deploymentId =
|
||||
process.env.GITBOOK_RUNTIME === 'cloudflare'
|
||||
? undefined
|
||||
: process.env.GITBOOK_HEAD_SHA || process.env.GITHUB_SHA || Date.now().toString(); // Needed because we use a custom deployment method i.e. https://vercel.com/docs/skew-protection#custom-deployment-id
|
||||
|
||||
const { VERCEL_TARGET_ENV } = process.env;
|
||||
|
||||
// Because preview, staging and prod shares the same SHA, the deployment will fail if we don't prefix it with the environment name.
|
||||
if (VERCEL_TARGET_ENV === 'preview') {
|
||||
deploymentId = `t-${deploymentId}`;
|
||||
} else if (VERCEL_TARGET_ENV === 'staging') {
|
||||
deploymentId = `s-${deploymentId}`;
|
||||
} else if (VERCEL_TARGET_ENV === 'production') {
|
||||
deploymentId = `p-${deploymentId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
*/
|
||||
const nextConfig = {
|
||||
allowedDevOrigins,
|
||||
deploymentId: deploymentId?.slice(0, 32), // Vercel's deployment ID has a max length of 32 characters
|
||||
experimental: {
|
||||
// This is needed to throw "forbidden" when the api token expired during revalidation
|
||||
authInterrupts: true,
|
||||
@@ -17,16 +57,31 @@ const nextConfig = {
|
||||
|
||||
// Since content is fully static, we don't want to fetch on hover again
|
||||
optimisticClientCache: false,
|
||||
// Disable splitting the RSC in like 5 chunks
|
||||
prefetchInlining: true,
|
||||
|
||||
// Rewrites barrel imports into deep ones: without it, importing a single helper from
|
||||
// react-openapi drags its whole client renderer into every page's entry.
|
||||
optimizePackageImports: ['@gitbook/react-openapi'],
|
||||
},
|
||||
|
||||
env: {
|
||||
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
|
||||
BUILD_VERSION: (
|
||||
process.env.GITBOOK_HEAD_SHA ||
|
||||
process.env.GITHUB_SHA ||
|
||||
Date.now().toString()
|
||||
).slice(0, 7),
|
||||
|
||||
// GitBook envs
|
||||
GITBOOK_API_URL: process.env.GITBOOK_API_URL,
|
||||
GITBOOK_APP_URL: process.env.GITBOOK_APP_URL,
|
||||
GITBOOK_OAUTH_SERVER_URL: process.env.GITBOOK_OAUTH_SERVER_URL,
|
||||
GITBOOK_SITE_OAUTH_SIGNING_SECRET: process.env.GITBOOK_SITE_OAUTH_SIGNING_SECRET,
|
||||
GITBOOK_PREVIEW_BASE_URL: process.env.GITBOOK_PREVIEW_BASE_URL,
|
||||
GITBOOK_INTEGRATIONS_HOST: process.env.GITBOOK_INTEGRATIONS_HOST,
|
||||
GITBOOK_INTEGRATIONS_CONTENT_HOST: process.env.GITBOOK_INTEGRATIONS_CONTENT_HOST,
|
||||
GITBOOK_IMAGE_RESIZE_URL: process.env.GITBOOK_IMAGE_RESIZE_URL,
|
||||
GITBOOK_IMAGE_RESIZE_SALT: process.env.GITBOOK_IMAGE_RESIZE_SALT,
|
||||
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
|
||||
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
|
||||
GITBOOK_URL: process.env.GITBOOK_URL,
|
||||
@@ -37,6 +92,8 @@ const nextConfig = {
|
||||
GITBOOK_IMAGE_RESIZE_MODE: process.env.GITBOOK_IMAGE_RESIZE_MODE,
|
||||
GITBOOK_FONTS_URL: process.env.GITBOOK_FONTS_URL,
|
||||
GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME,
|
||||
GITBOOK_BLOCK_SEARCH_INDEXATION: process.env.GITBOOK_BLOCK_SEARCH_INDEXATION,
|
||||
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: process.env.GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE,
|
||||
|
||||
// Next.js envs
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
|
||||
@@ -48,6 +105,9 @@ const nextConfig = {
|
||||
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
poweredByHeader: false,
|
||||
|
||||
// We maintain our own AGENTS.md/CLAUDE.md at the repo root.
|
||||
agentRules: false,
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,8 @@ export default {
|
||||
converter: 'edge',
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
|
||||
incrementalCache: () =>
|
||||
import('./openNext/incrementalCache/server').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
},
|
||||
},
|
||||
@@ -18,7 +19,8 @@ export default {
|
||||
converter: 'edge',
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
|
||||
incrementalCache: () =>
|
||||
import('./openNext/incrementalCache/middleware').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
|
||||
//Only needed to run locally, in prod we'll use the one from do.js
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
@@ -28,9 +28,8 @@ export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
// We can't move the handler import to the top level, otherwise the runtime will not be properly initialized
|
||||
const { handler } = await import(
|
||||
'../../.open-next/server-functions/default/handler.mjs'
|
||||
);
|
||||
const { handler } =
|
||||
await import('../../.open-next/server-functions/default/handler.mjs');
|
||||
|
||||
// - `Request`s are handled by the Next server
|
||||
return handler(request, env, ctx);
|
||||
|
||||
@@ -1,177 +1,180 @@
|
||||
{
|
||||
"main": "default.js",
|
||||
"name": "gitbook-open-v2-server",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"keep_names": false,
|
||||
"compatibility_date": "2026-04-02",
|
||||
"minify": true,
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
"global_fetch_strictly_public"
|
||||
"global_fetch_strictly_public",
|
||||
],
|
||||
"observability": {
|
||||
"enabled": true
|
||||
"enabled": false,
|
||||
},
|
||||
"vars": {
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
},
|
||||
"env": {
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "dev",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
"GITBOOK_URL": "http://localhost:8771",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-preview",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-dev"
|
||||
}
|
||||
"service": "gitbook-open-v2-server-dev",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
"class_name": "R2WriteBuffer",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
}
|
||||
]
|
||||
"class_name": "DOShardedTagCache",
|
||||
},
|
||||
],
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["R2WriteBuffer", "DOShardedTagCache"]
|
||||
}
|
||||
]
|
||||
"new_sqlite_classes": ["R2WriteBuffer", "DOShardedTagCache"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
// Just as a test for the preview environment to check that everything works
|
||||
"NEXT_PRIVATE_DEBUG_CACHE": "true"
|
||||
"NEXT_PRIVATE_DEBUG_CACHE": "true",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-preview",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-preview"
|
||||
}
|
||||
"service": "gitbook-open-v2-server-preview",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
"script_name": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
"script_name": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
}
|
||||
]
|
||||
}
|
||||
"script_name": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-staging",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-staging"
|
||||
}
|
||||
"service": "gitbook-open-v2-server-staging",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
"script_name": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
"script_name": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
}
|
||||
]
|
||||
"script_name": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
],
|
||||
},
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-staging-tail"
|
||||
}
|
||||
]
|
||||
"service": "gitbook-x-staging-tail",
|
||||
},
|
||||
],
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
// This is a bit misleading, but it means that we can have 500 concurrent revalidations
|
||||
// This means that we'll have up to 100 durable objects instance running at the same time
|
||||
"MAX_REVALIDATE_CONCURRENCY": "100",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-production"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-production",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-production"
|
||||
}
|
||||
"service": "gitbook-open-v2-server-production",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
"script_name": "gitbook-open-v2-do-production",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
"script_name": "gitbook-open-v2-do-production",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
}
|
||||
]
|
||||
"script_name": "gitbook-open-v2-do-production",
|
||||
},
|
||||
],
|
||||
},
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-prod-tail"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"service": "gitbook-x-prod-tail",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,99 +2,100 @@
|
||||
"main": "do.js",
|
||||
"name": "gitbook-open-v2-do",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"keep_names": false,
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
"global_fetch_strictly_public"
|
||||
"global_fetch_strictly_public",
|
||||
],
|
||||
"observability": {
|
||||
"enabled": true
|
||||
"enabled": false,
|
||||
},
|
||||
"env": {
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-preview",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-preview"
|
||||
}
|
||||
"service": "gitbook-open-v2-preview",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler"
|
||||
"class_name": "DOQueueHandler",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
"class_name": "DOShardedTagCache",
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
"class_name": "R2WriteBuffer",
|
||||
},
|
||||
],
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||
}
|
||||
]
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
"STAGE": "staging",
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-staging",
|
||||
},
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-staging-tail"
|
||||
}
|
||||
"service": "gitbook-x-staging-tail",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-staging"
|
||||
}
|
||||
"service": "gitbook-open-v2-staging",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler"
|
||||
"class_name": "DOQueueHandler",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
"class_name": "DOShardedTagCache",
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
"class_name": "R2WriteBuffer",
|
||||
},
|
||||
],
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||
}
|
||||
]
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
@@ -103,47 +104,47 @@
|
||||
// We don't want to pollute the memory with broken cache entries
|
||||
// Most of the time, those are fake requests.
|
||||
"NEXT_CACHE_DO_QUEUE_MAX_RETRIES": "1",
|
||||
"STAGE": "production"
|
||||
"STAGE": "production",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-production"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-production",
|
||||
},
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-prod-tail"
|
||||
}
|
||||
"service": "gitbook-x-prod-tail",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-production"
|
||||
}
|
||||
"service": "gitbook-open-v2-production",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler"
|
||||
"class_name": "DOQueueHandler",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
"class_name": "DOShardedTagCache",
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
"class_name": "R2WriteBuffer",
|
||||
},
|
||||
],
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WorkerEntrypoint } from 'cloudflare:workers';
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
import { handler as middlewareHandler } from '../../.open-next/middleware/handler.mjs';
|
||||
|
||||
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
@@ -26,8 +26,10 @@ export default class extends WorkerEntrypoint {
|
||||
async fetch(request) {
|
||||
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
|
||||
const startTime = Date.now();
|
||||
const middlewareRequest = new Request(request.url, request);
|
||||
middlewareRequest.headers.set('x-open-next-continent', request.cf?.continent || '');
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
|
||||
const reqOrResp = await middlewareHandler(middlewareRequest, this.env, this.ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
const duration = Date.now() - startTime;
|
||||
const logMessage = formatLog(
|
||||
@@ -36,7 +38,7 @@ export default class extends WorkerEntrypoint {
|
||||
reqOrResp.headers.get('x-opennext-cache') ?? 'MISS',
|
||||
getResolvedRoute(reqOrResp, 'middleware')
|
||||
);
|
||||
// biome-ignore lint/suspicious/noConsole: <explanation>
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log(logMessage);
|
||||
return reqOrResp;
|
||||
}
|
||||
@@ -59,7 +61,7 @@ export default class extends WorkerEntrypoint {
|
||||
`SERVER-${response.headers.get('x-nextjs-cache') ?? 'MISS'}`,
|
||||
getResolvedRoute(reqOrResp, 'unresolved')
|
||||
);
|
||||
// biome-ignore lint/suspicious/noConsole: <explanation>
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log(formatedLog);
|
||||
|
||||
return response;
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
{
|
||||
"main": "middleware.js",
|
||||
"name": "gitbook-open-v2",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_date": "2026-04-02",
|
||||
"keep_names": false,
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
"global_fetch_strictly_public"
|
||||
"global_fetch_strictly_public",
|
||||
],
|
||||
"observability": {
|
||||
"enabled": false,
|
||||
},
|
||||
"assets": {
|
||||
"directory": "../../.open-next/assets",
|
||||
"binding": "ASSETS"
|
||||
},
|
||||
"observability": {
|
||||
"enabled": true
|
||||
"binding": "ASSETS",
|
||||
},
|
||||
"vars": {
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
},
|
||||
"env": {
|
||||
"dev": {
|
||||
@@ -26,129 +27,129 @@
|
||||
// we should just bypass the cache to go to the server directly
|
||||
"SHOULD_BYPASS_CACHE": "true",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
"GITBOOK_URL": "http://localhost:8771"
|
||||
"GITBOOK_URL": "http://localhost:8771",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-preview",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-dev"
|
||||
"service": "gitbook-open-v2-dev",
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-dev"
|
||||
}
|
||||
]
|
||||
"service": "gitbook-open-v2-server-dev",
|
||||
},
|
||||
],
|
||||
},
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
"PREVIEW_HOSTNAME": "TO_REPLACE",
|
||||
"WORKER_VERSION_ID": "TO_REPLACE"
|
||||
"WORKER_VERSION_ID": "TO_REPLACE",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-preview",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-preview"
|
||||
"service": "gitbook-open-v2-preview",
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-preview"
|
||||
}
|
||||
"service": "gitbook-open-v2-server-preview",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
"script_name": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
"script_name": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
}
|
||||
]
|
||||
}
|
||||
"script_name": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
"STAGE": "staging",
|
||||
"WORKER_VERSION_ID": "TO_REPLACE",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "open-2c.gitbook-staging.com/*",
|
||||
"zone_name": "gitbook-staging.com"
|
||||
"zone_name": "gitbook-staging.com",
|
||||
},
|
||||
{
|
||||
"pattern": "static-2c.gitbook-staging.com/*",
|
||||
"zone_name": "gitbook-staging.com"
|
||||
}
|
||||
"zone_name": "gitbook-staging.com",
|
||||
},
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-staging",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-staging"
|
||||
"service": "gitbook-open-v2-staging",
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-staging"
|
||||
}
|
||||
"service": "gitbook-open-v2-server-staging",
|
||||
},
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-staging-tail"
|
||||
}
|
||||
"service": "gitbook-x-staging-tail",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
"script_name": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
"script_name": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
}
|
||||
]
|
||||
"script_name": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
],
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
|
||||
}
|
||||
]
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
@@ -160,64 +161,64 @@
|
||||
"DEBUG_CLOUDFLARE": "true",
|
||||
"WORKER_VERSION_ID": "TO_REPLACE",
|
||||
"STAGE": "production",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "open-2c.gitbook.com/*",
|
||||
"zone_name": "gitbook.com"
|
||||
"zone_name": "gitbook.com",
|
||||
},
|
||||
{
|
||||
"pattern": "static-2c.gitbook.com/*",
|
||||
"zone_name": "gitbook.com"
|
||||
}
|
||||
"zone_name": "gitbook.com",
|
||||
},
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-production"
|
||||
}
|
||||
"bucket_name": "gitbook-open-v2-cache-production",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-production"
|
||||
"service": "gitbook-open-v2-production",
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-production"
|
||||
}
|
||||
"service": "gitbook-open-v2-server-production",
|
||||
},
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-prod-tail"
|
||||
}
|
||||
"service": "gitbook-x-prod-tail",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
"script_name": "gitbook-open-v2-do-production",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
"script_name": "gitbook-open-v2-do-production",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
}
|
||||
]
|
||||
"script_name": "gitbook-open-v2-do-production",
|
||||
},
|
||||
],
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const args = process.argv.slice(2);
|
||||
const versionId = args[0];
|
||||
|
||||
// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev
|
||||
const previewHostname = `${versionId.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
|
||||
const previewHostname = `${versionId?.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
|
||||
|
||||
let updatedFile = file.replace(
|
||||
/"PREVIEW_HOSTNAME": "TO_REPLACE"/,
|
||||
|
||||
+12
-92
@@ -1,4 +1,4 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
|
||||
import type {
|
||||
CacheEntryType,
|
||||
CacheValue,
|
||||
@@ -6,8 +6,7 @@ import type {
|
||||
WithLastModified,
|
||||
} from '@opennextjs/aws/types/overrides.js';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
|
||||
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
|
||||
export const DEFAULT_PREFIX = 'incremental-cache';
|
||||
@@ -18,15 +17,12 @@ export type KeyOptions = {
|
||||
|
||||
/**
|
||||
*
|
||||
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace
|
||||
* the cache operations. It also integrates both R2 and Cache API in a single class.
|
||||
* Having our own, will allow us to customize it in the future if needed.
|
||||
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it has an additional
|
||||
* R2WriteBuffer Durable Object to handle writes to R2. Given how we set up cache, we often end up writing to the same key too fast.
|
||||
*/
|
||||
class GitbookIncrementalCache implements IncrementalCache {
|
||||
export class GitbookIncrementalCache implements IncrementalCache {
|
||||
name = 'GitbookIncrementalCache';
|
||||
|
||||
protected localCache: Cache | undefined;
|
||||
|
||||
async get<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
cacheType?: CacheType
|
||||
@@ -34,7 +30,6 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
const cacheKey = this.getR2Key(key, cacheType);
|
||||
|
||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||
const localCache = await this.getCacheInstance();
|
||||
if (!r2) throw new Error('No R2 bucket');
|
||||
if (process.env.SHOULD_BYPASS_CACHE === 'true') {
|
||||
// We are in a local middleware environment, we should bypass the cache
|
||||
@@ -42,27 +37,17 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// Check local cache first if available
|
||||
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
|
||||
if (localCacheEntry) {
|
||||
const result = (await localCacheEntry.json()) as WithLastModified<
|
||||
CacheValue<CacheType>
|
||||
>;
|
||||
return this.returnNullOn404({
|
||||
...result,
|
||||
// Because we use tag cache and also invalidate them every time,
|
||||
// if we get a cache hit, we don't need to check the tag cache as we already know it's not been revalidated
|
||||
// this should improve performance even further, and reduce costs
|
||||
shouldBypassTagCache: true,
|
||||
});
|
||||
}
|
||||
|
||||
const r2Object = await r2.get(cacheKey);
|
||||
if (!r2Object) return null;
|
||||
|
||||
const json = (await r2Object.json()) as CacheValue<CacheType>;
|
||||
const lastModified = r2Object.uploaded.getTime();
|
||||
|
||||
if (!json) return null;
|
||||
|
||||
return this.returnNullOn404({
|
||||
value: await r2Object.json(),
|
||||
lastModified: r2Object.uploaded.getTime(),
|
||||
value: json,
|
||||
lastModified,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to get from cache', e);
|
||||
@@ -89,39 +74,8 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
): Promise<void> {
|
||||
const cacheKey = this.getR2Key(key, cacheType);
|
||||
|
||||
const localCache = await this.getCacheInstance();
|
||||
|
||||
try {
|
||||
await this.writeToR2(cacheKey, JSON.stringify(value));
|
||||
|
||||
//TODO: Check if there is any places where we don't have tags
|
||||
// Ideally we should always have tags, but in case we don't, we need to decide how to handle it
|
||||
// For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment
|
||||
const tags = this.getTagsFromCacheEntry(value) ?? [
|
||||
`build_id/${process.env.NEXT_BUILD_ID}`,
|
||||
];
|
||||
|
||||
// We consider R2 as the source of truth, so we update the local cache
|
||||
// only after a successful R2 write
|
||||
await localCache.put(
|
||||
this.getCacheUrlKey(cacheKey),
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
value,
|
||||
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
|
||||
// See https://developers.cloudflare.com/workers/reference/security-model/
|
||||
lastModified: Date.now(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
// Cache-Control default to 30 minutes, will be overridden by `revalidate`
|
||||
// In theory we should always get the `revalidate` value
|
||||
'cache-control': `max-age=${value.revalidate ?? 60 * 30}`,
|
||||
'cache-tag': tags.join(','),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to set to cache', e);
|
||||
}
|
||||
@@ -131,14 +85,10 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
const cacheKey = this.getR2Key(key);
|
||||
|
||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||
const localCache = await this.getCacheInstance();
|
||||
if (!r2) throw new Error('No R2 bucket');
|
||||
|
||||
try {
|
||||
await r2.delete(cacheKey);
|
||||
|
||||
// Here again R2 is the source of truth, so we delete from local cache first
|
||||
await localCache.delete(this.getCacheUrlKey(cacheKey));
|
||||
} catch (e) {
|
||||
console.error('Failed to delete from cache', e);
|
||||
}
|
||||
@@ -168,12 +118,6 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
}
|
||||
}
|
||||
|
||||
async getCacheInstance(): Promise<Cache> {
|
||||
if (this.localCache) return this.localCache;
|
||||
this.localCache = await caches.open('incremental-cache');
|
||||
return this.localCache;
|
||||
}
|
||||
|
||||
// Utility function to generate keys for R2/Cache API
|
||||
getR2Key(initialKey: string, cacheType: CacheEntryType = 'cache'): string {
|
||||
let key = initialKey;
|
||||
@@ -189,28 +133,4 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
'/'
|
||||
);
|
||||
}
|
||||
|
||||
getCacheUrlKey(cacheKey: string): string {
|
||||
return `http://cache.local/${cacheKey}`;
|
||||
}
|
||||
|
||||
getTagsFromCacheEntry<CacheType extends CacheEntryType>(
|
||||
entry: CacheValue<CacheType>
|
||||
): string[] | undefined {
|
||||
if ('tags' in entry && entry.tags) {
|
||||
return entry.tags;
|
||||
}
|
||||
|
||||
if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) {
|
||||
const rawTags = entry.meta.headers['x-next-cache-tags'];
|
||||
if (typeof rawTags === 'string') {
|
||||
return rawTags.split(',');
|
||||
}
|
||||
}
|
||||
if ('value' in entry) {
|
||||
return entry.tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new GitbookIncrementalCache();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user