mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-23 11:03:39 +00:00
Compare commits
1899 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 021f340695 | |||
| 581abe0bb7 | |||
| 4503be2f19 | |||
| bdfc98b97e | |||
| 0f0cfde6ba | |||
| 357af3f6d2 | |||
| b95add7b27 | |||
| 99984ebbb3 | |||
| a52daf8b15 | |||
| f80429e4e8 | |||
| 6b4c453497 | |||
| c7d042a733 | |||
| e94a5085eb | |||
| 3ae1524a88 | |||
| b967b248e7 | |||
| a9e6814744 | |||
| 5725da19e2 | |||
| 05cff0a03b | |||
| 1e8c34a426 | |||
| cd881f9ac1 | |||
| 39316fc205 | |||
| 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 | |||
| 9251b33959 | |||
| 81a6bd756a | |||
| ada195d329 | |||
| 1211ebea9b | |||
| 2e6e28eb54 | |||
| 854c448bad | |||
| 64a0b0f169 | |||
| 13ff22b6a8 | |||
| 8927e8fbbb | |||
| 25e2b40d47 | |||
| a60af8ff9c | |||
| 27b328cfd7 | |||
| fa6ef0c01c | |||
| 6217a2ed3b | |||
| ffa866cb99 | |||
| 011aa931cf | |||
| 68308154b5 | |||
| 36af03f4d5 | |||
| cbc71a56b6 | |||
| 1420180220 | |||
| e1b2cf6d81 | |||
| c0e9c49679 | |||
| afc4bd4415 | |||
| bcfa8d8b19 | |||
| 43766d6519 | |||
| 42c17f5c74 | |||
| d655d3eece | |||
| 3500de95e8 | |||
| 7f03b94949 | |||
| f0cf060191 | |||
| 5a137e7b14 | |||
| 28f7fbaa8a | |||
| ff96bb5787 | |||
| 87841124e0 | |||
| a650b58276 | |||
| 388b20d44f | |||
| ea7e94fc51 | |||
| d30bcbabdb | |||
| fb858a1941 | |||
| cc2e615b9a | |||
| a862cde6b4 | |||
| e2b7bec162 | |||
| c1b577ecc9 | |||
| ba7ec14f76 | |||
| 9f0117723c | |||
| f72b6a20db | |||
| 2f798209bb | |||
| eec8f16424 | |||
| 885264f132 | |||
| f1a6dec951 | |||
| 44f41510fa | |||
| f220229e5e | |||
| 61b4cb8d7c | |||
| 185cdb4883 | |||
| 2cdba53451 | |||
| 950f6c36cf | |||
| ea37977b1e | |||
| 81da82aed7 | |||
| 39a3518651 | |||
| d903273759 | |||
| d130532f69 | |||
| 94fcd21f34 | |||
| 1d2db4ae2b | |||
| d270b4af9b | |||
| ccfbd3c1a8 | |||
| 5c4923557f | |||
| b1f608b865 | |||
| c7993756dd | |||
| d02465823f | |||
| 6b9f6b5b79 | |||
| df7de8fb0c | |||
| 72f5ce3985 | |||
| 04c80262e2 | |||
| 56d1b8fe2d | |||
| 054a1634ab | |||
| 6ff63f2db6 | |||
| b5ad0ce1e5 | |||
| 2ba7e54c81 | |||
| 0a858e74ad | |||
| b0c534fc2c | |||
| 250c194a02 | |||
| 5b10cde936 | |||
| 4aeb81bfa5 | |||
| 955cebf944 | |||
| 7c951ef437 | |||
| 68f0dbcc35 | |||
| 39c4f76aea | |||
| 2fe58e8cbb | |||
| 765141a4d8 | |||
| eb1bd3aa01 | |||
| b7284b0223 | |||
| 5bbd254742 | |||
| 611e28626a | |||
| 2a3bb0eb22 | |||
| 6816f0fa5a | |||
| 17386776ec | |||
| 9ee9082614 | |||
| 334cfdd522 | |||
| 8c961a5dbd | |||
| 4708956e77 | |||
| 813dd03dbb | |||
| 0522dbc61e | |||
| acb9f53156 | |||
| 6016846156 | |||
| cf1fae5837 | |||
| 7212345466 | |||
| e38caf78ff | |||
| 0a8dd67541 | |||
| ed684c19a9 | |||
| 52ab3681e3 | |||
| 9cc5a787e9 | |||
| 0003030bf5 | |||
| 9ad8f45d38 | |||
| a3855257c8 | |||
| 9169c2f053 | |||
| abe028bd38 | |||
| 12c2451f4f | |||
| 8daede5427 | |||
| 4c89aa60eb | |||
| d0a3f64ae4 | |||
| 433d66482d | |||
| bd553bcf9e | |||
| f5894bcf74 | |||
| 1b59e7ce36 | |||
| 32d6f5845d | |||
| 4f5cbfe732 | |||
| afba8fcb34 | |||
| 377a4899e4 | |||
| 938bdeb34b | |||
| d216eaa3f6 | |||
| 5ca5da0f21 | |||
| ebe6eb3b39 | |||
| 3bfe347096 | |||
| 7b38f89078 | |||
| 09b689b0ea | |||
| 2350baa75f | |||
| bc1eca815e | |||
| 78c10340e7 | |||
| c16890a3a6 | |||
| 3b4fe2827a | |||
| 7e807aabb5 | |||
| 973c74ee69 | |||
| efed0b0617 | |||
| ca3b9aca5d | |||
| e2afc07ab2 | |||
| e8fb84d362 | |||
| 57f951a7d9 | |||
| 28008667ed | |||
| f3affc3034 | |||
| 59da30f3c6 | |||
| 2db721112a | |||
| b60039b7d5 | |||
| 8fe9c9afea | |||
| 216ba7a556 | |||
| a2ff57081b | |||
| 40dbd9a14b | |||
| e5bac69c5c | |||
| 8d6598393f | |||
| 59b86eb9dc | |||
| 52c9f6da2f | |||
| dd65987c94 | |||
| ace61901a4 | |||
| e6c3c7635d | |||
| 5134d9e7f8 | |||
| 6821fb2937 | |||
| cfe4812045 | |||
| 14843ac343 | |||
| 8fb6d7465f | |||
| 0ef647586f | |||
| b4039627d9 | |||
| 4a295e6db4 | |||
| 5726999a5a | |||
| 58d7f3c140 | |||
| 07eab986d5 | |||
| 711cf38f9b | |||
| 4f7c0eea9f | |||
| f3448de2da | |||
| ec92075ef8 | |||
| 1e013cd87c | |||
| f033734308 | |||
| 50c05164f7 | |||
| 7fb9004b43 | |||
| 4721403986 | |||
| df848ef64e | |||
| 0b40ccd286 | |||
| caaa692f0b | |||
| 42d43e09c4 | |||
| 7ccbf55ba7 | |||
| 81f5bfc2f9 | |||
| a7a713bca2 | |||
| d6613c787f | |||
| 427f748e1c | |||
| 4f5fec7213 | |||
| 8c0a53ab40 | |||
| 2dfd5aab20 | |||
| 8d3c6562a2 | |||
| dae019c115 | |||
| 73e0cbb2d6 | |||
| 392f59450c | |||
| b4918f60ce | |||
| 8f7c304d58 | |||
| a3a944d7dd | |||
| 88a35ed057 | |||
| 67998b6f15 | |||
| ff6d1150a5 | |||
| 500c8cb649 | |||
| 6859f7d239 | |||
| dfa8a37be1 | |||
| 11a6511b7a | |||
| af98402655 | |||
| b7a0db3339 | |||
| 382a19885b | |||
| d99da6a3ae | |||
| 72cd0e59e6 | |||
| 42d88da73c | |||
| 2863fe0dc1 | |||
| 87218062be | |||
| 8a3910e208 | |||
| 6294bbb53c | |||
| a28a997851 | |||
| 28a9ee701e | |||
| 50e8b2ec52 | |||
| 315717fa2d | |||
| da67711c90 | |||
| fecdbe8dde | |||
| 9316ccd1f4 | |||
| c3b620e975 | |||
| 7a00880e5b | |||
| b6b597564d | |||
| d410381957 | |||
| 902c3c6c1a | |||
| 4fb2a4a6d7 | |||
| 2d64c78787 | |||
| fbfcca5dae | |||
| 664ae8bff6 | |||
| fa3d2aae08 | |||
| e7a591d9f6 | |||
| 2860beacdc | |||
| 231167d3e1 | |||
| c0ee60ec13 | |||
| f58b904ba6 | |||
| d5992f1a5c | |||
| fa12f9eeb6 | |||
| 33726c88a0 | |||
| 6aa3ff9c5e | |||
| 7d3fe23195 | |||
| 521052d84f | |||
| c730845e17 | |||
| 7b5a3616c4 | |||
| c9373efdb5 | |||
| 015615de8a | |||
| 0f867e533a | |||
| 7d5a6d2255 | |||
| a1da4f023d | |||
| 666e8426e1 | |||
| df2fa42bda | |||
| b3a7ad6d2c | |||
| 2932077bf9 | |||
| 957afd967f | |||
| 4c9a9d0416 | |||
| 40df91a363 | |||
| af66ff71ed | |||
| 6f8ecd6111 | |||
| fa3eb07617 | |||
| dc4268db64 | |||
| a0c06a72cc | |||
| 19c20c0be3 | |||
| fae34e56ac | |||
| ba0094a862 | |||
| b259009c9c | |||
| d88dd5ed8a | |||
| 57bb146075 | |||
| 4b67fe5317 | |||
| 938125e772 | |||
| 1c8d9febb3 | |||
| 223edae17d | |||
| 12a455dab5 | |||
| d00dc8ca72 | |||
| 2c85a302fc | |||
| 3460b78b45 | |||
| 95a1f652f9 | |||
| cc37e2ae15 | |||
| 08382ce62f | |||
| 80cb52a237 | |||
| c6637b09e2 | |||
| 778624af00 | |||
| e15757d01f | |||
| aed79fd6c3 | |||
| 0c973a3292 | |||
| 56c923c0ed | |||
| 7d7806df30 | |||
| cb5598dc19 | |||
| 47f01edcb8 | |||
| e6ddc0f07f | |||
| 373f18f451 | |||
| 3f292065cd | |||
| 5e975ab95b | |||
| a3ec264764 | |||
| 5d504ffa4c | |||
| 04999662db | |||
| f7a34706c7 | |||
| f328a41982 | |||
| 20ebecb114 | |||
| 5a69692f54 | |||
| 42ca7e1580 | |||
| c4ebb3fecd | |||
| 2a805cc082 | |||
| 89a5816ee4 | |||
| c3f6b8c003 | |||
| 0e201d5899 | |||
| ae5f1abc58 | |||
| 580101d04e | |||
| 81b4a4db53 | |||
| 8339e91e2b | |||
| 3119066728 | |||
| 326e28e9b0 | |||
| 90f0127ada | |||
| 0353a8186c | |||
| dd043df00f | |||
| 97b7c79bc6 | |||
| 634e0b4302 | |||
| 2a23f1f003 | |||
| 88ffdcc303 | |||
| b6b09d42af | |||
| ebc39e9db9 | |||
| 0df3849ea2 | |||
| 8ed1bda2f2 | |||
| d67699a1f1 | |||
| 3363a18856 | |||
| ad1dc0b914 | |||
| e4c7307c99 | |||
| f9c942c8ff | |||
| eeb977fc03 | |||
| 4b8a62122a | |||
| e44e947fc1 | |||
| 7588cfe1c5 | |||
| a9a643f222 | |||
| 77d6cb75be | |||
| 1942d221df | |||
| 041601ed65 | |||
| 5567281a85 | |||
| 4c3753046b | |||
| 89f8e16184 | |||
| a3e1125ced | |||
| 4dae8acfed | |||
| bc081c4344 | |||
| e4275b235d | |||
| 77397cac82 | |||
| b6a08bc679 | |||
| 116575c811 | |||
| 4ba7bd5ce1 | |||
| 223ab5377f | |||
| ae43ca146c | |||
| 42342893d6 | |||
| eedefdd5c9 | |||
| f5e152dd74 | |||
| 95ea22d9c5 | |||
| f23023ddfa | |||
| da74077954 | |||
| 29aaba5949 | |||
| daf41fc728 | |||
| b08eb1af59 | |||
| 8322cd3e35 | |||
| 444039c142 | |||
| 3b0eff7f71 | |||
| c765463e38 | |||
| e8ca25b67e | |||
| 528eee3fc6 | |||
| 168a4fa998 | |||
| 3ea75adba4 | |||
| cbd768a095 | |||
| 2d01653a78 | |||
| aa3357a57d | |||
| de53946896 | |||
| d51204148e | |||
| 319761a39e | |||
| 7fbf01a348 | |||
| c22490bd80 | |||
| 860fd6d02e | |||
| c43c524804 | |||
| 66bb938d85 | |||
| 0e6d928496 | |||
| f07982d9f8 | |||
| b92ecfad58 | |||
| 416bde70bc | |||
| 580f7ade15 | |||
| cdffd7c47b | |||
| 5a959cf041 | |||
| 31466546a2 | |||
| 90ead98eaa | |||
| 65e9cb9d5c | |||
| e59076a074 | |||
| 653920dec9 | |||
| 23cedd2d8b | |||
| 70c4182fd8 | |||
| b62b101065 | |||
| b9c929beeb | |||
| 2b6c593289 | |||
| c0b339e406 | |||
| da485f5144 | |||
| 139a8050a5 | |||
| da7b369ffd | |||
| 432da64919 | |||
| 7d0b422200 | |||
| fb90eb0acc | |||
| b647538e41 | |||
| e50797a5c1 | |||
| 5b1e01c786 | |||
| 813b2af06b | |||
| 6f71da88d8 | |||
| fa91eb7eaf | |||
| c756761591 | |||
| 434af90366 | |||
| 0f41e194cb | |||
| bd353489e9 | |||
| 77fd393f15 | |||
| 8701b5e078 | |||
| 61db166f92 | |||
| 0d9b9cea0f | |||
| 7bb37c7f0a | |||
| 5b2bf828e3 | |||
| c85d3f9752 | |||
| 4d7088bbf0 | |||
| dd6ab6044c | |||
| d236bf029c | |||
| d70d566abe | |||
| a25fdedea9 | |||
| 373183a2cd | |||
| e9fa50dfb5 | |||
| 57ca4e0e88 | |||
| bc90adb73a | |||
| 1505ddbd5a | |||
| cd99ed57fb | |||
| 27de1cd01d | |||
| ae78fc5394 | |||
| 40e8e69c6d | |||
| e84a46a558 | |||
| 18a72d4466 | |||
| d7aa5fa003 | |||
| 27532a5f41 | |||
| 48c18c03b9 | |||
| bba2e52e24 | |||
| 54ee0149e1 | |||
| 8fda9d2fab | |||
| 77a72d79ce | |||
| 73e2b472ef | |||
| 2f28cc1323 | |||
| f5516d4e21 | |||
| d2facb2db1 | |||
| 6eae764b7c | |||
| 970ef8f886 | |||
| ed0720699e | |||
| ad94d00d5f | |||
| c7843f7f52 | |||
| 1fe3286bdb | |||
| ce030fdf76 | |||
| 9ba8783952 | |||
| 2f3966cc53 | |||
| 8f65a22e13 | |||
| 2787d884cd | |||
| fc00b51683 | |||
| f80c3a7b37 | |||
| 4e5d6c7487 | |||
| 96f84b5917 | |||
| 6b8a5fdd1b | |||
| 89083ac790 | |||
| 70be2c6b2b | |||
| bcf877841b | |||
| a84b06bb19 | |||
| 24b7808d9a | |||
| 8364ee988e | |||
| 07f6e2d233 | |||
| 7212973f34 | |||
| af44e0436b | |||
| 3f9c35d505 | |||
| 2c13f8b95c | |||
| 4dab1c56e5 | |||
| 4723f03ef1 | |||
| 278689e0a0 | |||
| 886e204e68 | |||
| c59947ab39 | |||
| 4f0a772a21 | |||
| 903f962673 | |||
| 31d800e48b | |||
| c1c58eb919 | |||
| eec3eed100 | |||
| 99da8df3d1 | |||
| c9ea23919e | |||
| 16292def79 | |||
| 9108c56ca1 | |||
| b011ea0af8 | |||
| d33171b779 | |||
| 6aaeae269e | |||
| e5f0cc8663 | |||
| 9b5f971aa1 | |||
| ff3b708ad3 | |||
| e996904013 | |||
| f32bf1f15d | |||
| 6cda2dd57b | |||
| c60e9ba27f | |||
| 9bc3d504c8 | |||
| b5e4fd75e9 | |||
| a92cb49445 | |||
| 88f64b0d63 | |||
| 844059f9a5 | |||
| eaf2d68308 | |||
| 12bdbf988d | |||
| 29bab79005 | |||
| 8d5de97ddf | |||
| d9b5a898ab | |||
| f127d28675 | |||
| 65c1bdc455 | |||
| 0711a171d4 | |||
| c58c551f9e | |||
| f574858b9b | |||
| 3e1592e16b | |||
| 90604c4c3b | |||
| 9ffc3b606d | |||
| e00c87d3e6 | |||
| bb3ca9c165 | |||
| 8ee9757132 | |||
| 946a463979 | |||
| 7aad9f7ff8 | |||
| 3fe8f7c219 | |||
| 0278a14e8e | |||
| e5366d3a49 | |||
| 38e84ffe1a | |||
| 052e07a936 | |||
| 7e67ebc11d | |||
| c9dea94717 | |||
| 1344754ca9 | |||
| 0f4759fb89 | |||
| 6d77b647a4 | |||
| 5907bd9af4 | |||
| b66d04c9c5 | |||
| 3173d8ec2d | |||
| 31e381035e | |||
| 259074b81f | |||
| 9a2f5b9295 | |||
| 58cd70b0c1 | |||
| 821899a767 | |||
| bfcc119ff3 | |||
| 76c797498e | |||
| 13514bf6fa | |||
| 05ffd0e321 | |||
| 701eaad92a | |||
| c66ef5dd10 | |||
| f774f9b3a0 | |||
| 29dc7ccb1a | |||
| 80237c34f9 | |||
| 979233c5c0 | |||
| 2f03e6acee | |||
| c492f09a19 | |||
| 2c3af5e73c | |||
| 6597f8508d | |||
| 8beb5d6bcf | |||
| 53f5dbea26 | |||
| 725952a106 | |||
| 3319375e9a | |||
| c5a4619020 | |||
| 989fc69abe | |||
| 0924259217 | |||
| 05affac34f | |||
| 453a459566 | |||
| 722f02ea09 | |||
| 5bcea2fe3b | |||
| 73afd1f5d0 | |||
| 9b8a0d3e54 | |||
| 75bd98d66c | |||
| 9b914d10bd | |||
| 3e11678d8d | |||
| 6e03638130 | |||
| 23f884efb7 | |||
| 2ae76f999b | |||
| 685325cdc7 | |||
| ab132eadb7 | |||
| 14504e43b5 | |||
| 4ed79574e8 | |||
| 31bcd1a386 | |||
| 270e5ef7ad | |||
| 1f4733355e | |||
| 027a859ef4 | |||
| 59f50eaee7 | |||
| 80d2733c25 | |||
| feb6735a4d | |||
| f1d1d2fd81 | |||
| a749c67803 | |||
| c808bb1483 | |||
| e24206ebf0 | |||
| 3410785a3a | |||
| b4a12d606e | |||
| a0545545d7 | |||
| 1f11650ca1 | |||
| da55facf2c | |||
| dc2dbc5710 | |||
| bdd6303bcc | |||
| 66d0fc0683 | |||
| 05e1d8cd96 | |||
| 9f0de74caa | |||
| a820739bd2 | |||
| 304042017c | |||
| 82cd9f2979 | |||
| a3f1fea27b | |||
| bb5c6a42e7 | |||
| 445baaaa61 | |||
| 7419ee7dba | |||
| aa6be381a4 | |||
| 61575838f3 | |||
| 359bb979f8 | |||
| b29a885bae | |||
| 919da7f2c9 | |||
| d222c116e0 | |||
| df840dac18 | |||
| f8d4c7697c | |||
| dddb4ecbb8 | |||
| 470d10cb47 | |||
| dae11e9607 | |||
| 3d1eb53708 | |||
| a76c9a56a7 | |||
| eaad0d4e09 | |||
| 46edde9da6 | |||
| dda0cc635e | |||
| 727bde2d16 | |||
| 9f30af1b33 | |||
| ff05e20537 | |||
| 53de5b13c1 | |||
| a025118f69 | |||
| 71688a817b | |||
| 00c476ea7f | |||
| 5576906914 | |||
| d5aaccd2c4 | |||
| 7059c2ba25 | |||
| aaab157d49 | |||
| 9943276c0c | |||
| c31cf5243a | |||
| 0c03676efb | |||
| 883d10acf8 | |||
| 3e5e458118 | |||
| 0510b6f29e | |||
| 92b7668bf3 | |||
| 3973b1cbf3 | |||
| 160fca1ea1 | |||
| e90c96f7e9 | |||
| d935fb1b2e | |||
| a6529584a5 | |||
| 18953b2d6b | |||
| 21cbd9e3b0 | |||
| 38061bdf65 | |||
| d9029c7e1e | |||
| fe8acc986e | |||
| e721f17f7e | |||
| 2f73db785e | |||
| 9e18ae6587 | |||
| 9eca010f94 | |||
| 6e54a06d9c | |||
| 1f8e416e41 | |||
| cfccc44406 | |||
| 45b30c9c27 | |||
| 67f78368fb | |||
| d9c8d57e8e | |||
| 0d615e3888 | |||
| 68287d3df4 | |||
| dff08ae3e3 | |||
| 8cfa67c1dd | |||
| b41d425993 | |||
| 12c7862250 | |||
| 6f54826296 | |||
| ccf2cffc4b | |||
| f7b801b4c8 | |||
| 95f2aa45eb | |||
| 9524ff5f5a | |||
| 1823101b03 | |||
| 1a8cfd2a8b | |||
| 5b4e710770 | |||
| 1762f85ef9 | |||
| d370a3f0e3 | |||
| d3e573c2e9 | |||
| 94876e3ad1 | |||
| ad190602c7 | |||
| 162b4b78b6 | |||
| e5dc05e994 | |||
| b46cff70c0 | |||
| b6c3870eda | |||
| 1c1f504601 | |||
| 1b8a45678c | |||
| 38de9eb9d7 | |||
| eac1314a7d | |||
| 29d59793dc | |||
| 128ad20280 | |||
| 994ebff8c1 | |||
| 142938401c | |||
| 65cc4afb23 | |||
| e000f23c44 | |||
| 1de9d1aa33 | |||
| 0e601e277b | |||
| e582a47f37 | |||
| 1b251a78b0 | |||
| 24f5249ae9 | |||
| 17c1631013 | |||
| cb100d5e00 | |||
| 1fcc807513 | |||
| 5664e5a30f | |||
| 26e64019d8 | |||
| 56331d2e74 | |||
| 55b095e503 | |||
| 6a073e1dd5 | |||
| 960e25a3f6 | |||
| 1de338c312 | |||
| dbba50cea4 | |||
| 8f046a9cd6 | |||
| 2906e6038a | |||
| 87b8ea8c05 | |||
| 47971dce31 | |||
| 528a05398c | |||
| f584148ab9 | |||
| 0de22f7a96 | |||
| 09c7c30779 | |||
| fde32e26bf | |||
| a6f65916f1 | |||
| 6059efe094 | |||
| 16194c50ba | |||
| af3c6a971c | |||
| ecfdb976a3 | |||
| 08acea651a | |||
| 6b503608cd | |||
| d8763997a5 | |||
| 32aa1f99c1 | |||
| 718a8a54b1 | |||
| 741dd490d4 | |||
| 02d876e6d8 | |||
| 56c52e0bb0 | |||
| 6036becf93 | |||
| 3a7210d514 | |||
| 6088fa582d | |||
| 6691492ef7 | |||
| 7ee9158cd3 | |||
| 5ae1b883ab | |||
| 88397bf419 | |||
| 1138d5935b | |||
| 8276ba080e | |||
| 37d13d80f5 | |||
| 1c97536ab1 | |||
| ca8f028cc0 | |||
| 300f7bfe4b | |||
| 82dc9c40f7 | |||
| 44a20fe5ee | |||
| b950a64406 | |||
| c5f374f66f | |||
| ae99f87db0 | |||
| 5112e3e79b | |||
| c1e27ccacc | |||
| b0bd871997 | |||
| c30bc24fd6 | |||
| 648f0e9e84 | |||
| d2bc5672e0 | |||
| 5c87ec7c2f | |||
| d66c184ed2 | |||
| 12f25d8bfc | |||
| cbe61397a3 | |||
| deb8c54da3 | |||
| 5dab70fab4 | |||
| 665b6bed66 | |||
| f4a90defc3 | |||
| 46f63cbd55 | |||
| c77142a16f | |||
| eb7c22f52a | |||
| e86e51f06f | |||
| db74ea3001 | |||
| 99579ac29c | |||
| 2f6540e008 | |||
| 72c3b80529 | |||
| 8126a83c50 | |||
| 210c432b37 | |||
| f92e90603f | |||
| 14172791e9 | |||
| e4e2f524d4 | |||
| fc7b16f6a7 | |||
| 6ba3ae74d5 | |||
| c71d1598d8 | |||
| 5950657b0e | |||
| 0e1a48cd06 | |||
| 98245e5f20 | |||
| 0b6ddca981 | |||
| ea1468c892 | |||
| e8e64bf510 | |||
| aaf8daf0d7 | |||
| 8af1abc4e1 | |||
| 48ab59feb3 | |||
| f64d5073d4 | |||
| 53b9f10ac8 | |||
| 75606e4df7 | |||
| 0643b7f351 | |||
| 5b3b4e0614 | |||
| 8d039d1348 | |||
| 87eea73081 | |||
| 67a6fb4c87 | |||
| 23584c9862 | |||
| 2f767125c4 | |||
| c73e07d42d | |||
| 6b5d7e6057 | |||
| 076dc48e89 | |||
| bb208ab36a | |||
| 60e35001b6 | |||
| 29a0d7ea02 | |||
| 1f2b7f7acf | |||
| 39e5cb63ef | |||
| 5b5928fb8d | |||
| 08068743a5 | |||
| 35eae1adcf | |||
| e85d357abd | |||
| 31396d8109 | |||
| 664debc0bc | |||
| 56c31c76b3 | |||
| 2d1a71f3d1 | |||
| c67cd73058 | |||
| 2572cb4352 | |||
| c9f6c7ccac | |||
| b6520b0dbc | |||
| 9fe8142117 | |||
| 57cdd25b5f | |||
| 1005ee5d52 | |||
| d9bb9f9a07 | |||
| 75a6fabe61 | |||
| 75e29b69cc | |||
| ffd3937538 | |||
| 575587e4c5 | |||
| 2ce59d7c92 | |||
| 3b3d6e2a73 | |||
| f2a467fdbf | |||
| 867481c66d | |||
| fc8065b7a8 | |||
| 28f1ed9205 | |||
| 568266bc9a | |||
| 7739f341b8 | |||
| 2b501f191c | |||
| aa2ed0fda1 | |||
| 51cba074d7 | |||
| 2c945c663c | |||
| d9a18ecabb | |||
| eab7931afb | |||
| 99b6c92377 | |||
| 4d56f1163b | |||
| 07cf835551 | |||
| 061c0c1a90 | |||
| 1ed18c05ef | |||
| ca134c8a60 | |||
| 7ba67fd852 | |||
| 7675c2c411 | |||
| 7c71363af8 | |||
| d48926eb2a | |||
| d843e5e06f | |||
| a2e564738e | |||
| 54a797a0b1 | |||
| a78c1ecb4d | |||
| 4771c78001 | |||
| b7a5106f36 | |||
| 4bcbdc50ff | |||
| 5d72b35c7f | |||
| ff50ac215c | |||
| 3fe3809416 | |||
| 363d337a72 | |||
| f89b31c940 | |||
| 630c3e84dd | |||
| d43202f137 | |||
| 0964043489 | |||
| e9b31a545d | |||
| 7f28a88374 | |||
| 07ea45bf84 | |||
| a86d35f721 | |||
| d6c28a0ff6 | |||
| a7af3ca7bc | |||
| cda08a9843 | |||
| 7588774237 | |||
| 683373b263 | |||
| 673ec8fb5d | |||
| 63bacaa671 | |||
| e3a3d6a505 | |||
| 175171dda1 | |||
| a4b63b88c8 | |||
| c3675fdfe7 | |||
| 2fa08519b6 | |||
| 065627060b | |||
| 2982b6a808 | |||
| c3f56ecd40 | |||
| fe9e6c1afd | |||
| 73394ae929 | |||
| c754fc9ff4 | |||
| 1ca6208057 | |||
| cdfae4d4df | |||
| c3da5469ca | |||
| 79f2d8ba76 | |||
| 7b1bdb92a6 | |||
| f9a075eed1 | |||
| 32e4193557 | |||
| 133c3e7c0a | |||
| 6ce3cea682 | |||
| 250e77d18b | |||
| e3bfe006e9 | |||
| 76aeecdc68 | |||
| 15d2ee3e76 | |||
| b066db479f | |||
| a69ff2789a | |||
| 4df235d64a | |||
| 11524457e3 | |||
| 8f7364293e | |||
| 56f5fa1fbe | |||
| b075f0f7e9 | |||
| 0ca32afe8d | |||
| 32c2f257a9 | |||
| a7066ccbe1 | |||
| 09248e0efe | |||
| e2ef22fc5f | |||
| 75f825e4f8 | |||
| 9b8d5198e4 | |||
| e09f747727 | |||
| 16e6171750 | |||
| f885e887ea | |||
| 5096f7fbc5 | |||
| f16560c1ee | |||
| d0f4860759 | |||
| 34d36c6de1 | |||
| 042b850b5d | |||
| 73892f4fc2 | |||
| 636b868bf1 | |||
| b144368e3f | |||
| 683b4193d1 | |||
| f4c953665a | |||
| 0f990c772a | |||
| 0bf985af61 | |||
| 42473611d3 | |||
| 2c437f7d94 | |||
| aa8c49eb0c | |||
| b32e40c72a | |||
| 2ff7ed1b52 | |||
| bfbed1a69b | |||
| ef9d012be9 | |||
| f12a2155c0 | |||
| 43f696c4a8 | |||
| 4c19014dcd | |||
| 0e7f9d6778 | |||
| 00a0bf3c1b | |||
| e9149036f6 | |||
| b6a9967959 | |||
| 8bbc3caf3e | |||
| 237b703a6c | |||
| cf3045a718 | |||
| a679e7283f | |||
| 13c75341e2 | |||
| 4cbcc5bb20 | |||
| 51955dae3b | |||
| 51fa3ab2c8 | |||
| af9e66e87e | |||
| 8878dc7f80 | |||
| 4336bc20e0 | |||
| 4a7613a521 | |||
| 689f55340f | |||
| 160ae3d49d | |||
| c079c3c6d9 | |||
| cb782a7557 | |||
| 502d8fc087 | |||
| 6295881138 | |||
| 17f71ba680 | |||
| 5fe7adb87b | |||
| 41684d091b | |||
| 9f6c3347cb | |||
| aa321989c4 | |||
| 0f1565cb4b | |||
| 3996110ebd | |||
| 3422ad4c84 | |||
| 3c07e65b9a | |||
| 287df91661 | |||
| 776bc3135a | |||
| 4638958c9f | |||
| 1f24fe46c4 | |||
| ccbcab8822 | |||
| 094e9cdac2 | |||
| 5c35f36b3a | |||
| d7ec2b57a4 | |||
| 373a1e0312 | |||
| e9fe3975bb | |||
| 32a73b55d4 | |||
| 0426312d6c | |||
| ede2335fb3 | |||
| 709f1a1ccb | |||
| 24b785cbfb | |||
| acc3f2f418 | |||
| d4a653d6a9 | |||
| 7e74d25447 | |||
| 3445db41ce | |||
| d1bd02a6f5 | |||
| 24cd72e088 | |||
| 47f7558c38 | |||
| de747b7b33 | |||
| 8e8b73fbd9 | |||
| e966e33e8f | |||
| bd0ca5babd | |||
| f6bacea1ae | |||
| ba2a523722 | |||
| 57adb3e54a | |||
| 7377928c8a | |||
| 5f8a8fe9b1 | |||
| 29a0bf25c4 | |||
| 92bb016576 | |||
| c8810ea002 | |||
| aa8bbaddc4 | |||
| b8c136e9c0 | |||
| 46d54172b0 | |||
| 68dc2de190 | |||
| 81b372f372 | |||
| 966e520640 | |||
| 12702930ec | |||
| 6ce57bcda1 | |||
| d99b453bc4 | |||
| aaf5793cb7 | |||
| 9b1762d2ab | |||
| 8d38d000fd | |||
| 4cf9cdd56d | |||
| 39406bbf54 | |||
| 9c5c1e5b68 | |||
| 6929b0b98b | |||
| 7c26d491b1 | |||
| 97035a054a | |||
| db45c6d773 | |||
| e3f5f81e62 |
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
name: write-pr-description
|
||||||
|
description: Write a concise, product-focused PR description from the changes and available discussion. Use when asked for a PR summary or description.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Write a PR description
|
||||||
|
|
||||||
|
Read the full branch diff and commit history against the PR's base branch, not just the latest commit. Use the PR discussion and linked issues for context when available. The PR's changes are the source of truth for what ships if an issue describes something different.
|
||||||
|
|
||||||
|
Write for contributors and reviewers who may not know the area. Lead with what changes, prefer user or product impact when supported, and include implementation details only when they help review. Keep the proposed changes short, using bullets only when useful.
|
||||||
|
|
||||||
|
Include relevant validation and screenshots when available. Do not claim checks passed unless they ran. Use only information appropriate for a public repository; do not copy private issue details or customer information into the description.
|
||||||
|
|
||||||
|
## Template and changelog
|
||||||
|
|
||||||
|
Read `.github/pull_request_template.md` immediately before writing. Preserve its section names and order. Always include `Proposed changes` and `Changelog`; omit `Demo` when there is nothing to show and `Context` when it adds no useful explanation.
|
||||||
|
|
||||||
|
Write a short changelog entry describing what shipped. Use `[Feature]` for user-facing additions or improvements, `[Fix]` for user-facing bug fixes, and `[Chore]` for internal maintenance. Remove unused examples and placeholders. Usually one entry is enough; use more only for distinct changes. The PR changelog does not replace any changeset required by `AGENTS.md`.
|
||||||
|
|
||||||
|
## Optional context
|
||||||
|
|
||||||
|
Add this section only when it helps someone outside the area understand why the change matters. Context explains why the change matters; the changelog says what shipped; proposed changes can explain implementation. Omit context if it repeats those sections, adds generic filler, or requires guessing the reason or impact.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**Problem:** What wasn't working or what was missing?
|
||||||
|
|
||||||
|
**Outcome:** What becomes possible or works differently after this PR?
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep each field to 1–2 short, non-technical sentences. Describe the problem and outcome, not the implementation. For infrastructure changes, explain the failure or limitation they caused. Do not invent an actor, customer segment, ownership, or impact, or force the change into a user story.
|
||||||
|
|
||||||
|
## Writing pass
|
||||||
|
|
||||||
|
- Use plain words and direct verbs: "use" instead of "leverage", "help" instead of "facilitate". Prefer active voice when the actor is known.
|
||||||
|
- Name the concrete behavior instead of saying "improves the experience" or "makes it more robust". Support performance claims with measurements.
|
||||||
|
- Cut filler, promotional language, and generic conclusions. If a sentence could fit almost any PR, make it specific or delete it.
|
||||||
|
- Keep one idea per sentence and use consistent names. Remove redundant hedging without hiding real uncertainty.
|
||||||
|
- Avoid "not just X, but Y", forced groups of three, em dashes, decorative emojis, and unnecessary bold text.
|
||||||
|
|
||||||
|
Return the description in a Markdown code block unless asked to update the PR. When asked to update it, apply the description with `gh pr edit` using a body file.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Changesets
|
||||||
|
|
||||||
|
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||||
|
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||||
|
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||||
|
|
||||||
|
We have a quick list of common questions to get you started engaging with this project in
|
||||||
|
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
|
||||||
|
"changelog": "@changesets/cli/changelog",
|
||||||
|
"commit": false,
|
||||||
|
"fixed": [],
|
||||||
|
"linked": [],
|
||||||
|
"access": "public",
|
||||||
|
"baseBranch": "main",
|
||||||
|
"updateInternalDependencies": "patch",
|
||||||
|
"ignore": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "🚀 Dev server",
|
||||||
|
"runtimeExecutable": "sh",
|
||||||
|
"runtimeArgs": ["-c", "bun install && bun dev"],
|
||||||
|
"port": 3000,
|
||||||
|
"autoPort": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/write-pr-description
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/write-pr-description
|
||||||
@@ -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,27 +0,0 @@
|
|||||||
# Configurations
|
|
||||||
|
|
||||||
### API ###
|
|
||||||
|
|
||||||
## API token to use when looking up the published content
|
|
||||||
# GITBOOK_API_URL=https://api.gitbook.com
|
|
||||||
# GITBOOK_TOKEN=xxx
|
|
||||||
|
|
||||||
## User agent to use when making requests to the API
|
|
||||||
# GITBOOK_USER_AGENT=GitBook/1.0.0
|
|
||||||
# GITBOOK_USER_AGENT_COMMENT=something
|
|
||||||
|
|
||||||
### URL of the application ###
|
|
||||||
# NEXT_PUBLIC_GITBOOK_APP_URL=https://app.gitbook.com
|
|
||||||
|
|
||||||
### Image resizing ###
|
|
||||||
# GITBOOK_IMAGE_RESIZE_SIGNING_KEY=1234567890
|
|
||||||
# GITBOOK_IMAGE_RESIZE_URL=https://mycompany.com/cdn-cgi/image/
|
|
||||||
|
|
||||||
### SEO ###
|
|
||||||
# GITBOOK_BLOCK_SEARCH_INDEXATION=true
|
|
||||||
|
|
||||||
## Caching
|
|
||||||
# GITBOOK_OUTPUT_CACHE=true
|
|
||||||
|
|
||||||
### Sentry ###
|
|
||||||
# SENTRY_DSN=xxx
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "next/core-web-vitals",
|
|
||||||
"plugins": ["import"],
|
|
||||||
"rules": {
|
|
||||||
"import/order": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"groups": ["builtin", "external", ["internal", "parent", "sibling", "index"]],
|
|
||||||
"newlines-between": "always",
|
|
||||||
"distinctGroup": true,
|
|
||||||
"pathGroups": [
|
|
||||||
{
|
|
||||||
"pattern": "@/**",
|
|
||||||
"group": "external",
|
|
||||||
"position": "after"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pattern": "@gitbook/**",
|
|
||||||
"group": "external",
|
|
||||||
"position": "after"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"alphabetize": {
|
|
||||||
"order": "asc",
|
|
||||||
"caseInsensitive": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@next/next/no-img-element": ["off"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+46
-7
@@ -1,6 +1,8 @@
|
|||||||
# Welcome to GitBook's contributing guide!
|
# Welcome to GitBook's contributing guide!
|
||||||
|
|
||||||
Thank you for investing your time in contributing to GitBook. Any contribution you make will be reviewed by our team.In this guide, you'll learn the different ways you can contribute.
|
> _For help, support, feature requests, and product questions - head to our [GitHub Community](https://github.com/orgs/GitbookIO/discussions) 🤖_
|
||||||
|
|
||||||
|
Thank you for investing your time in contributing to GitBook. Any contribution you make will be reviewed by our team. In this guide, you'll learn the different ways you can contribute.
|
||||||
|
|
||||||
## Types of Contributions
|
## Types of Contributions
|
||||||
|
|
||||||
@@ -45,13 +47,50 @@ Any contribution you make can be made to the code located in this repository. In
|
|||||||
|
|
||||||
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
|
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
|
||||||
|
|
||||||
##### GitHub Codespaces:
|
|
||||||
|
|
||||||
- [Fork, edit, and preview](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace) using [GitHub Codespaces](https://github.com/features/codespaces) without having to install and run the project locally.
|
|
||||||
|
|
||||||
#### 2. Create a working branch and start with your changes
|
#### 2. Create a working branch and start with your changes
|
||||||
|
|
||||||
After forking this repository, you'll want to [create a branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) to work off of. After creating the branch, you can start making changes!
|
After forking this repository, you'll want to [create a branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) to work off of.
|
||||||
|
|
||||||
|
#### 3. Install dependencies and run the project locally
|
||||||
|
|
||||||
|
##### Prerequisites:
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
|
||||||
|
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
|
||||||
|
|
||||||
|
##### Setup steps:
|
||||||
|
|
||||||
|
1. Ensure you are using the project's version of Node:
|
||||||
|
```bash
|
||||||
|
nvm use
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies using Bun:
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start the development server:
|
||||||
|
```bash
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Additional development commands:
|
||||||
|
- `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
|
||||||
|
|
||||||
|
#### 4. Preview your changes
|
||||||
|
|
||||||
|
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/url`.
|
||||||
|
|
||||||
|
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/url/gitbook.com/docs` after running the development server.
|
||||||
|
|
||||||
|
You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
|
||||||
|
|
||||||
### Commit your update
|
### Commit your update
|
||||||
|
|
||||||
@@ -71,4 +110,4 @@ When you're finished with the changes, [create a pull request](https://docs.gith
|
|||||||
|
|
||||||
### Your PR is merged
|
### Your PR is merged
|
||||||
|
|
||||||
Congratulations 🎉Thank you for your contribution! Once your PR is merged, your contributions will be publicly visible on the relevant repository.
|
Congratulations 🎉 Thank you for your contribution! Once your PR is merged, your contributions will be publicly visible on the relevant repository.
|
||||||
|
|||||||
@@ -0,0 +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
|
||||||
|
outputs:
|
||||||
|
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.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
|
||||||
|
|
||||||
|
- 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.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.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 }}"
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
name: 'Setup Playwright'
|
|
||||||
description: 'Install Playwright and dependencies'
|
|
||||||
runs:
|
|
||||||
using: 'composite'
|
|
||||||
steps:
|
|
||||||
# Run npm ci and get Playwright version
|
|
||||||
- name: 🏗 Prepare Playwright env
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["@playwright/test"].version')
|
|
||||||
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Cache browser binaries, cache key is based on Playwright version and OS
|
|
||||||
- name: 🧰 Cache Playwright browser binaries
|
|
||||||
uses: actions/cache@v3
|
|
||||||
id: playwright-cache
|
|
||||||
with:
|
|
||||||
path: '~/.cache/ms-playwright'
|
|
||||||
key: '${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}'
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-playwright-
|
|
||||||
|
|
||||||
# Install browser binaries & OS dependencies if cache missed
|
|
||||||
- name: 🏗 Install Playwright browser binaries & OS dependencies
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
bun x playwright install --with-deps chromium
|
|
||||||
|
|
||||||
# Install only the OS dependencies if cache hit
|
|
||||||
- name: 🏗 Install Playwright OS dependencies
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
bun x playwright install-deps
|
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
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
|
||||||
|
outputs:
|
||||||
|
deployment-url:
|
||||||
|
description: 'Deployment URL'
|
||||||
|
value: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: ./.github/composite/setup-bun
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
|
- 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_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_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||||
|
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||||
|
GITBOOK_DISABLE_INSIGHTS: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||||
|
GITBOOK_DISABLE_LOOKUP_ALTERNATIVES: ${{ inputs.environment == 'staging' && 'true' || '' }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload the DO worker
|
||||||
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
workingDirectory: ./
|
||||||
|
wranglerVersion: '4.43.0'
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
|
||||||
|
|
||||||
|
- id: upload_server
|
||||||
|
name: Upload server to Cloudflare
|
||||||
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
workingDirectory: ./
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Extract server version worker ID
|
||||||
|
shell: bash
|
||||||
|
id: extract_server_version_id
|
||||||
|
run: |
|
||||||
|
version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||||
|
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Run updateWrangler scripts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
bun run ./packages/gitbook/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||||
|
|
||||||
|
- id: upload_middleware
|
||||||
|
name: Upload middleware to Cloudflare
|
||||||
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
workingDirectory: ./
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||||
|
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Deploy server and middleware to Cloudflare
|
||||||
|
if: ${{ inputs.deploy == 'true' }}
|
||||||
|
uses: ./.github/actions/gradual-deploy-cloudflare
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
opServiceAccount: ${{ inputs.opServiceAccount }}
|
||||||
|
opItem: ${{ inputs.opItem }}
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||||
|
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 }}
|
||||||
|
run: |
|
||||||
|
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
|
||||||
|
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
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
|
||||||
|
headSha:
|
||||||
|
description: 'Git ref to deploy, used for the deploymentId'
|
||||||
|
required: false
|
||||||
|
outputs:
|
||||||
|
deployment-url:
|
||||||
|
description: 'Deployment URL'
|
||||||
|
value: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: ./.github/composite/setup-bun
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
|
- name: Pull Vercel Environment Information
|
||||||
|
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 }}
|
||||||
|
- 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_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 "GITBOOK_DISABLE_INSIGHTS=true" >> .vercel/.env.${{ inputs.environment }}.local
|
||||||
|
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
|
||||||
|
cat .vercel/.env.${{ inputs.environment }}.local
|
||||||
|
- name: Inject staging build env vars
|
||||||
|
if: ${{ inputs.environment == 'staging' }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "GITBOOK_DISABLE_LOOKUP_ALTERNATIVES=true" >> .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
|
||||||
|
GITBOOK_HEAD_SHA: ${{ inputs.headSha }}
|
||||||
|
- name: Deploy Project Artifacts to Vercel
|
||||||
|
id: deploy
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
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 }}
|
||||||
|
- name: Outputs
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
name: 'Setup Bun'
|
||||||
|
description: 'Install Bun and cache dependencies'
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Setup bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version-file: 'package.json'
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
## Proposed changes
|
||||||
|
|
||||||
|
<!-- Briefly describe what changed. Include relevant validation. -->
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
<!-- Add screenshots or a recording when useful. Otherwise, remove this section. -->
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
<!-- Optional: explain why this matters to someone unfamiliar with the area, in non-technical terms. Keep each field to 1–2 short sentences. Remove this section if it repeats the changelog or the reason or impact is unclear. -->
|
||||||
|
|
||||||
|
**Problem:** What wasn't working or what was missing?
|
||||||
|
|
||||||
|
**Outcome:** What becomes possible or works differently after this PR?
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
<!-- Keep the relevant entries and remove unused placeholders. Feature = user-facing addition or improvement; Fix = user-facing bug fix; Chore = internal maintenance. -->
|
||||||
|
|
||||||
|
- [Feature] ...
|
||||||
|
- [Fix] ...
|
||||||
|
- [Chore] ...
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"display_information": {
|
||||||
|
"name": "PR Status",
|
||||||
|
"description": "Updates PR status reactions in engineering"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"bot_user": {
|
||||||
|
"display_name": "PR Status",
|
||||||
|
"always_online": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_config": {
|
||||||
|
"scopes": {
|
||||||
|
"bot": ["channels:history", "reactions:read", "reactions:write"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"org_deploy_enabled": false,
|
||||||
|
"socket_mode_enabled": false,
|
||||||
|
"token_rotation_enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Slack PR reactions
|
||||||
|
|
||||||
|
[DataDog/slapr](https://github.com/DataDog/slapr) updates reactions on existing PR-link messages in [#engineering](https://gitbook.slack.com/archives/C01NXGWJELS). It runs in GitHub Actions. It does not post messages, approve or merge PRs, or change branch protection.
|
||||||
|
|
||||||
|
## Rollout and ownership
|
||||||
|
|
||||||
|
Owner: Peter White.
|
||||||
|
|
||||||
|
The rollout targets `GitbookIO/gitbook` (GBO), `GitbookIO/gitbook-x` (GBX), and `GitbookIO/integrations`. Each repository contains `.github/workflows/slapr.yml` and this runbook.
|
||||||
|
|
||||||
|
| Configuration | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Channel | `#engineering`, `C01NXGWJELS` |
|
||||||
|
| App manifest | `.github/slapr-app-manifest.json` |
|
||||||
|
| Slack app ID | `A0C2AGLAD0T` |
|
||||||
|
| Bot user ID | `U0C2AGP16R1`, repository variable `SLAPR_BOT_USER_ID` |
|
||||||
|
| Bot token | Repository Actions secret `SLAPR_SLACK_BOT_TOKEN` |
|
||||||
|
| Action | `DataDog/slapr@1c074e1dfd433f91a679d5113f7f8b956b65d973` |
|
||||||
|
| Approval threshold | Stock default, one approval |
|
||||||
|
| Events | `pull_request_review: submitted`, `pull_request: closed` |
|
||||||
|
|
||||||
|
## Install and configure
|
||||||
|
|
||||||
|
1. In [Slack app management](https://api.slack.com/apps), create the **PR Status** app from `.github/slapr-app-manifest.json` in the GitBook workspace. Obtain workspace approval if required, then install it. The bot requests Slapr's documented `channels:history`, `reactions:read`, and `reactions:write` scopes. `#engineering` is public, so `groups:history` is unnecessary. No review map is configured, so `channels:read` is unnecessary.
|
||||||
|
2. Invite **PR Status** only to `#engineering`. Keep it dedicated to Slapr: stock Slapr removes obsolete reactions belonging to this bot on a matched message. It leaves other users' reactions alone.
|
||||||
|
3. Record the app ID and bot user ID above. The bot profile's member ID or Slack `auth.test` response's `user_id` is the required bot user ID, not the app ID or bot ID.
|
||||||
|
4. Store the bot OAuth token as `SLAPR_SLACK_BOT_TOKEN` in each of the three repositories' Actions secrets. Use the GitHub secret UI or `gh secret set SLAPR_SLACK_BOT_TOKEN --repo GitbookIO/<repository>` and its hidden prompt. Never paste the token into a command argument, chat, workflow, or committed file. Alternatively, an organization secret must use selected-repository visibility limited to these three repositories.
|
||||||
|
5. Set Actions variable `SLAPR_BOT_USER_ID` in each repository to that bot's user ID. The token and user ID must belong to the same installation. Use `gh variable set SLAPR_BOT_USER_ID --repo GitbookIO/<repository> --body '<user-id>'` or the GitHub UI.
|
||||||
|
6. Merge the workflows through normal review, then run the live checks below. Repository maintainers handle reviews and merges; Slapr has only read access to GitHub contents and pull requests.
|
||||||
|
|
||||||
|
GBX runs this workflow on `blacksmith-2vcpu-ubuntu-2404`. GBO and integrations use `ubuntu-latest`, matching their existing workflows.
|
||||||
|
|
||||||
|
The workflow hardcodes the engineering channel and does not check out PR code. It uses the stock event triggers and leaves the approval threshold unset. There are no listeners, schedules, review maps, custom discovery, or additional services.
|
||||||
|
|
||||||
|
## Reactions
|
||||||
|
|
||||||
|
Slapr's status selection stays unchanged. Missing custom emoji names are mapped through the action's supported inputs to existing equivalents. Workspace lookup on 2026-09-17 confirmed `pr-approved` and `merged`; the other mappings are standard Slack emojis.
|
||||||
|
|
||||||
|
| Stock status | Reaction |
|
||||||
|
| --- | --- |
|
||||||
|
| Review started | `:eyes:` |
|
||||||
|
| Partially approved | `:next_track_button:` |
|
||||||
|
| Approved | `:pr-approved:` |
|
||||||
|
| Changes requested | `:construction:` |
|
||||||
|
| Merged | `:merged:` |
|
||||||
|
| Closed without merging | `:no_entry_sign:` |
|
||||||
|
| Comment-only review | `:speech_balloon:` |
|
||||||
|
|
||||||
|
The partially-approved reaction is configured but cannot occur with the default threshold of one approval. It becomes relevant only if that threshold is raised.
|
||||||
|
|
||||||
|
On approval, expect review-started and approved reactions. On merge, stock Slapr removes review-started and adds merged; it can retain approved alongside merged. Approval and merging have distinct reactions. Other stock review-state behavior is accepted.
|
||||||
|
|
||||||
|
## Live acceptance test
|
||||||
|
|
||||||
|
For each repository after credentials are configured and the workflow is on the default branch:
|
||||||
|
|
||||||
|
1. Open a fresh same-repository test PR through normal review. Post its full `https://github.com/GitbookIO/<repository>/pull/<number>` link as a new top-level message in `#engineering` before the review event.
|
||||||
|
2. Have another contributor approve it. Open the **Slack PR reactions** Actions run and verify that the matched message has the bot's `pr-approved` reaction.
|
||||||
|
3. Have an authorized maintainer merge it. Verify the closed-event run succeeds and the same message has the bot's `merged` reaction. No new Slack status message should appear.
|
||||||
|
4. Record the PR URL, Slack message permalink, approval/merge workflow run URLs, date, and result below. A green workflow alone does not prove the message was found or reacted to.
|
||||||
|
|
||||||
|
| Repository | PR and Slack message | Approval run/result | Merge run/result |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| GitbookIO/gitbook | Pending | Not tested | Not tested |
|
||||||
|
| GitbookIO/gitbook-x | Pending | Not tested | Not tested |
|
||||||
|
| GitbookIO/integrations | Pending | Not tested | Not tested |
|
||||||
|
|
||||||
|
Only the first matching message in Slapr's recent-history response is updated. Older messages, thread replies, duplicate posts, fork PRs, and links posted after the event are outside this rollout's acceptance criteria. Slapr does not paginate history or reconcile missed events.
|
||||||
|
|
||||||
|
## Troubleshoot and disable
|
||||||
|
|
||||||
|
- No workflow run: confirm the workflow is merged, Actions permits the pinned action, and the event was a submitted review or PR closure. These are the only configured triggers.
|
||||||
|
- `No message found requesting review`: confirm the full PR link was posted as a recent top-level message in `#engineering` before the event. This is a successful no-op in stock Slapr.
|
||||||
|
- `not_in_channel` or `channel_not_found`: check the app installation, channel ID, and bot membership in `#engineering`.
|
||||||
|
- `missing_scope`: compare the installed bot scopes with the manifest and reinstall after changes.
|
||||||
|
- `invalid_auth` or `account_inactive`: replace the repository secrets with the current bot token. If the bot identity changed, update `SLAPR_BOT_USER_ID` too.
|
||||||
|
- `invalid_name`: check the emoji mappings still exist. If a custom emoji was removed, map that input to another existing emoji, keeping approval and merged distinct.
|
||||||
|
- Old reactions remain or removal fails: confirm `SLAPR_BOT_USER_ID` is the member ID for the token's bot.
|
||||||
|
- GitHub permission errors: keep `contents: read` and `pull-requests: read`; do not introduce a PAT, write permissions, or `pull_request_target` to cover forks. Fork and Dependabot secret restrictions are accepted limitations.
|
||||||
|
- Rate limits or transient API failures: inspect the Actions log and retry the failed run after the limit resets. There is no scheduled reconciliation.
|
||||||
|
|
||||||
|
Disable one repository with `gh workflow disable slapr.yml --repo GitbookIO/<repository>`, or disable **Slack PR reactions** in its Actions UI. Repeat for all three to stop the integration. Revoke the Slack app token to stop its access immediately, remove its repository secrets, and remove the bot from `#engineering` when retiring it. Existing reactions remain.
|
||||||
|
|
||||||
|
## Pin review
|
||||||
|
|
||||||
|
On 2026-09-17, the pinned action definition, Dockerfile, entry point, dependency declarations, GitHub reads, Slack history lookup and reaction writes were inspected. Without `review-map`, the action targets only the supplied channel. It reads PR/review state and adds/removes reactions; it does not post Slack messages or write to GitHub.
|
||||||
|
|
||||||
|
The action SHA pins Slapr's source. Its upstream Dockerfile still uses the mutable `python:3.14` image and installs dependencies, including `pyyaml>=5.0`, at build time. This rollout keeps stock Slapr unchanged. Review upstream source and dependency changes before updating the SHA in all three repositories.
|
||||||
+31
-127
@@ -4,169 +4,73 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- changeset-release/main
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
|
||||||
name: Deploy to Cloudflare Pages
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
deployments: write
|
|
||||||
outputs:
|
|
||||||
deployment_url: ${{ steps.cloudflare.outputs.url }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup bun
|
|
||||||
uses: oven-sh/setup-bun@v1
|
|
||||||
with:
|
|
||||||
bun-version: 1.0.33
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
env:
|
|
||||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
|
||||||
- name: Cache Next.js build
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ github.workspace }}/.next/cache
|
|
||||||
# Generate a new cache whenever packages or source files change.
|
|
||||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
|
||||||
# If source files changed but packages didn't, rebuild from a prior cache.
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-
|
|
||||||
- name: Sets env vars for production
|
|
||||||
run: |
|
|
||||||
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
|
|
||||||
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
|
|
||||||
if: startsWith(github.ref, 'refs/heads/main')
|
|
||||||
- name: Sets env vars for preview
|
|
||||||
run: |
|
|
||||||
echo "SENTRY_ENVIRONMENT=preview" >> $GITHUB_ENV
|
|
||||||
if: 1 && !startsWith(github.ref, 'refs/heads/main')
|
|
||||||
- name: Build Next.js with next-on-pages
|
|
||||||
run: bun run build:cloudflare
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
|
||||||
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
|
||||||
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
|
||||||
- id: cloudflare
|
|
||||||
name: Publish to Cloudflare Pages
|
|
||||||
uses: cloudflare/pages-action@v1
|
|
||||||
with:
|
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
|
|
||||||
directory: ./.vercel/output/static
|
|
||||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
workingDirectory: ./
|
|
||||||
- name: Outputs
|
|
||||||
run: |
|
|
||||||
echo "ID: ${{ steps.cloudflare.outputs.id }}"
|
|
||||||
echo "URL: ${{ steps.cloudflare.outputs.url }}"
|
|
||||||
echo "Environment: ${{ steps.cloudflare.outputs.environment }}"
|
|
||||||
echo "Alias: ${{ steps.cloudflare.outputs.alias }}"
|
|
||||||
- name: Archive build output
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-output
|
|
||||||
path: .vercel/
|
|
||||||
visual-testing:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Visual Testing
|
|
||||||
needs: deploy
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup bun
|
|
||||||
uses: oven-sh/setup-bun@v1
|
|
||||||
with:
|
|
||||||
bun-version: 1.0.33
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
- name: Setup Playwright
|
|
||||||
uses: ./.github/actions/setup-playwright
|
|
||||||
- name: Run Playwright tests
|
|
||||||
run: bun e2e
|
|
||||||
env:
|
|
||||||
BASE_URL: ${{needs.deploy.outputs.deployment_url}}
|
|
||||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
|
||||||
pagespeed-testing:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: PageSpeed Testing
|
|
||||||
needs: deploy
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup bun
|
|
||||||
uses: oven-sh/setup-bun@v1
|
|
||||||
with:
|
|
||||||
bun-version: 1.0.33
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
env:
|
|
||||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
|
||||||
- name: Run pagespeed tests
|
|
||||||
run: bun ./tests/pagespeed-testing.ts $DEPLOYMENT_URL
|
|
||||||
env:
|
|
||||||
DEPLOYMENT_URL: ${{needs.deploy.outputs.deployment_url}}
|
|
||||||
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
|
|
||||||
format:
|
format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Format
|
name: Format
|
||||||
|
timeout-minutes: 6
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: ./.github/composite/setup-bun
|
||||||
with:
|
|
||||||
bun-version: 1.0.33
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
env:
|
env:
|
||||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
- run: bun format --check .
|
- run: bun format:check
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Lint
|
name: Lint
|
||||||
|
timeout-minutes: 6
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: ./.github/composite/setup-bun
|
||||||
with:
|
|
||||||
bun-version: 1.0.33
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
env:
|
env:
|
||||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
- run: bun lint --no-cache
|
- run: bun lint
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Test
|
name: Test
|
||||||
|
timeout-minutes: 6
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: ./.github/composite/setup-bun
|
||||||
with:
|
|
||||||
bun-version: 1.0.33
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
env:
|
env:
|
||||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
- run: bun unit
|
- run: bun unit
|
||||||
typecheck:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Typecheck
|
name: Build
|
||||||
|
timeout-minutes: 6
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: ./.github/composite/setup-bun
|
||||||
with:
|
- name: Install dependencies
|
||||||
bun-version: 1.0.33
|
run: bun install
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
|
- run: bun run build
|
||||||
|
typecheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Typecheck
|
||||||
|
timeout-minutes: 6
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: ./.github/composite/setup-bun
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
name: Preview
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
push:
|
||||||
|
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 }}
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
environment: preview
|
||||||
|
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
|
||||||
|
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||||
|
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 }}
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
environment: preview
|
||||||
|
deploy: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
opItem: op://gitbook-open/2c-preview
|
||||||
|
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||||
|
commitTag: ${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
|
||||||
|
commitMessage: ${{ github.sha }}
|
||||||
|
- name: Extract Worker ID
|
||||||
|
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
|
||||||
|
- 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() && github.event_name != 'push'
|
||||||
|
needs:
|
||||||
|
- deploy-v2-vercel
|
||||||
|
- deploy-v2-cloudflare
|
||||||
|
steps:
|
||||||
|
- name: Find GitHub Comment
|
||||||
|
uses: peter-evans/find-comment@v3
|
||||||
|
id: fc
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
comment-author: 'github-actions[bot]'
|
||||||
|
body-includes: 'Summary of the deployments'
|
||||||
|
|
||||||
|
- name: Create or update GitHub comment
|
||||||
|
uses: peter-evans/create-or-update-comment@v4
|
||||||
|
with:
|
||||||
|
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||||
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
body: |
|
||||||
|
Summary of the deployments:
|
||||||
|
|
||||||
|
| Version | URL | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Vercel | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}](${{ needs.deploy-v2-vercel.outputs.deployment-url }}) | ${{ needs.deploy-v2-vercel.result == 'success' && '✅' || '❌' }} |
|
||||||
|
| Cloudflare | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v2-cloudflare.result == 'success' && '✅' || '❌' }} |
|
||||||
|
|
||||||
|
### Test content
|
||||||
|
|
||||||
|
| Site | `2v` | `2c` |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| GitBook | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs) |
|
||||||
|
| E2E | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
|
||||||
|
edit-mode: replace
|
||||||
|
visual-testing-v2-vercel:
|
||||||
|
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
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: bun e2e
|
||||||
|
env:
|
||||||
|
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
|
||||||
|
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
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: bun e2e
|
||||||
|
env:
|
||||||
|
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
|
||||||
|
visual-testing-customers-v2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Visual Testing Customers v2
|
||||||
|
needs: deploy-v2-vercel
|
||||||
|
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
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: bun e2e-customers
|
||||||
|
env:
|
||||||
|
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-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
|
||||||
|
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
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: bun e2e-customers
|
||||||
|
env:
|
||||||
|
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-cloudflare
|
||||||
|
browserless-testing-v2-vercel:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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 tests
|
||||||
|
run: cd ./packages/gitbook && bun e2e-browserless
|
||||||
|
env:
|
||||||
|
BASE_URL: ${{needs.deploy-v2-vercel.outputs.deployment-url}}
|
||||||
|
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/
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Production
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
deploy-v2-vercel:
|
||||||
|
name: Deploy v2 to Vercel (production)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: 2v-production
|
||||||
|
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
|
outputs:
|
||||||
|
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Deploy
|
||||||
|
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 }}
|
||||||
|
deploy-v2-cloudflare:
|
||||||
|
name: Deploy v2 to Cloudflare Worker (production)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: 2c-production
|
||||||
|
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
|
outputs:
|
||||||
|
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Deploy
|
||||||
|
id: deploy
|
||||||
|
uses: ./.github/composite/deploy-cloudflare
|
||||||
|
with:
|
||||||
|
environment: production
|
||||||
|
deploy: true
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
opItem: op://gitbook-open/2c-production
|
||||||
|
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||||
|
commitTag: main
|
||||||
|
commitMessage: ${{ github.sha }}
|
||||||
|
- name: Outputs
|
||||||
|
run: |
|
||||||
|
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Staging
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
deploy-v2-vercel:
|
||||||
|
name: Deploy v2 to Vercel (staging)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: 2v-staging
|
||||||
|
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
|
outputs:
|
||||||
|
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Deploy
|
||||||
|
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 }}
|
||||||
|
deploy-v2-cloudflare:
|
||||||
|
name: Deploy v2 to Cloudflare Worker (staging)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: 2c-staging
|
||||||
|
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
|
outputs:
|
||||||
|
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Deploy
|
||||||
|
id: deploy
|
||||||
|
uses: ./.github/composite/deploy-cloudflare
|
||||||
|
with:
|
||||||
|
environment: staging
|
||||||
|
deploy: true
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
opItem: op://gitbook-open/2c-staging
|
||||||
|
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||||
|
commitTag: main
|
||||||
|
commitMessage: ${{ github.sha }}
|
||||||
|
- name: Outputs
|
||||||
|
run: |
|
||||||
|
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
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
|
||||||
|
id: changesets
|
||||||
|
uses: changesets/action@v1
|
||||||
|
with:
|
||||||
|
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 }}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Slack PR reactions
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_review:
|
||||||
|
types: [submitted]
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run_slapr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Update PR reactions in engineering
|
||||||
|
uses: DataDog/slapr@1c074e1dfd433f91a679d5113f7f8b956b65d973
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
slack-api-token: ${{ secrets.SLAPR_SLACK_BOT_TOKEN }}
|
||||||
|
slack-channel-id: C01NXGWJELS
|
||||||
|
bot-user-id: ${{ vars.SLAPR_BOT_USER_ID }}
|
||||||
|
emoji-review-started: eyes
|
||||||
|
emoji-partially-approved: next_track_button
|
||||||
|
emoji-approved: pr-approved
|
||||||
|
emoji-changes-requested: construction
|
||||||
|
emoji-merged: merged
|
||||||
|
emoji-closed: no_entry_sign
|
||||||
|
emoji-commented: speech_balloon
|
||||||
+12
-26
@@ -5,16 +5,6 @@ node_modules
|
|||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
@@ -24,25 +14,21 @@ npm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# local env files
|
# Turbo
|
||||||
.env*.local
|
.turbo
|
||||||
|
|
||||||
# vercel
|
# Vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
# typescript
|
# Env files
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
.npmrc
|
||||||
|
|
||||||
# visual tests
|
# Bun pack artifacts
|
||||||
screenshots/
|
packages/*/*.tgz
|
||||||
|
|
||||||
# Sentry Config File
|
# Playwright MCP artifacts
|
||||||
.sentryclirc
|
.playwright-mcp/
|
||||||
/test-results/
|
|
||||||
/playwright-report/
|
|
||||||
/blob-report/
|
|
||||||
/playwright/.cache/
|
|
||||||
|
|
||||||
# Generated public files
|
|
||||||
/public/~gitbook/static/
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
.next
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 4
|
|
||||||
}
|
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["oxc.oxc-vscode"]
|
||||||
|
}
|
||||||
Vendored
+9
-1
@@ -7,5 +7,13 @@
|
|||||||
["style \\=([^;]*);", "\"([^\"]*)\""],
|
["style \\=([^;]*);", "\"([^\"]*)\""],
|
||||||
["style \\=([^;]*);", "\\`([^\\`]*)\\`"]
|
["style \\=([^;]*);", "\\`([^\\`]*)\\`"]
|
||||||
],
|
],
|
||||||
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"]
|
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.oxc": "always"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"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,11 +1,15 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="./assets/gitbook_icon_dark.svg" alt="GitBook" width="48" />
|
||||||
|
</p>
|
||||||
|
|
||||||
<h1 align="center">GitBook</h1>
|
<h1 align="center">GitBook</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://docs.gitbook.com/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a>
|
<a href="https://gitbook.com/docs/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a> - <a href="https://github.com/orgs/GitbookIO/discussions/categories/feature-requests">Feature requests</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iNjUiIGhlaWdodD0iNjUiIHZpZXdCb3g9IjAgMCA2NSA2NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI3LjM5NjQgMzMuNjc2NEMzMC41MjU1IDM1LjQ4MjQgMzIuMDkgMzYuMzg1NCAzMy44MDgzIDM2LjM4NjlDMzUuNTI2NSAzNi4zODg0IDM3LjA5MjYgMzUuNDg4MiA0MC4yMjQ5IDMzLjY4NzdMNjAuMTkxNCAyMi4yMTA0QzYxLjA5MjcgMjEuNjkyMiA2MS42NDg0IDIwLjczMTggNjEuNjQ4NCAxOS42OTIxQzYxLjY0ODQgMTguNjUyNCA2MS4wOTI3IDE3LjY5MiA2MC4xOTE0IDE3LjE3MzlMNDAuMjE3NyA1LjY5MjQ1QzM3LjA4ODggMy44OTM4NiAzNS41MjQzIDIuOTk0NTcgMzMuODA3OCAyLjk5NTI0QzMyLjA5MTIgMi45OTU5MSAzMC41Mjc1IDMuODk2NDIgMjcuNCA1LjY5NzQ0TDEwLjIyOTMgMTUuNTg1NUMxMC4xMDIgMTUuNjU4OCAxMC4wMzg0IDE1LjY5NTQgOS45NzkwOCAxNS43MzAxQzQuMTEzNzEgMTkuMTYzNyAwLjQ4OTg5MiAyNS40MzIzIDAuNDQxNDM4IDMyLjIyODZDMC40NDA5NDggMzIuMjk3MyAwLjQ0MDk0OCAzMi4zNzA4IDAuNDQwOTQ4IDMyLjUxNzZDMC40NDA5NDggMzIuNjY0MyAwLjQ0MDk0OCAzMi43Mzc2IDAuNDQxNDM3IDMyLjgwNjNDMC40ODk3ODUgMzkuNTk0OSA0LjEwNTUyIDQ1Ljg1NzcgOS45NjA0NCA0OS4yOTRDMTAuMDE5NiA0OS4zMjg3IDEwLjA4MzIgNDkuMzY1NCAxMC4yMTAyIDQ5LjQzODdMMjAuOTY1OSA1NS42NDg3QzI3LjIzMzIgNTkuMjY3MyAzMC4zNjY4IDYxLjA3NjYgMzMuODA4MSA2MS4wNzc3QzM3LjI0OTMgNjEuMDc4OSA0MC4zODQyIDU5LjI3MTcgNDYuNjUzOSA1NS42NTc0TDU4LjAwOCA0OS4xMTIxQzYxLjE0NzQgNDcuMzAyMyA2Mi43MTcxIDQ2LjM5NzQgNjMuNTc5IDQ0LjkwNTZDNjQuNDQwOSA0My40MTM5IDY0LjQ0MDkgNDEuNjAyIDY0LjQ0MDkgMzcuOTc4NFYzMC45NzgxQzY0LjQ0MDkgMjkuOTcyOCA2My44OTY1IDI5LjA0NjQgNjMuMDE4MiAyOC41NTczQzYyLjE2ODQgMjguMDgzOSA2MS4xMzI1IDI4LjA5MSA2MC4yODkxIDI4LjU3NThMMzcuMDA3NCA0MS45NTg4QzM1LjQ0NTQgNDIuODU2NyAzNC42NjQzIDQzLjMwNTYgMzMuODA3MyA0My4zMDU5QzMyLjk1MDIgNDMuMzA2MiAzMi4xNjg5IDQyLjg1NzcgMzAuNjA2MyA0MS45NjA3TDE0Ljg0ODcgMzIuOTE1NUMxNC4wNTk0IDMyLjQ2MjQgMTMuNjY0NyAzMi4yMzU5IDEzLjM0NzcgMzIuMTk1QzEyLjYyNSAzMi4xMDE3IDExLjkzMDEgMzIuNTA2NiAxMS42NTQ4IDMzLjE4MTNDMTEuNTM0MSAzMy40NzcyIDExLjUzNjUgMzMuOTMyMiAxMS41NDE0IDM0Ljg0MjRDMTEuNTQ1IDM1LjUxMjQgMTEuNTQ2OCAzNS44NDc0IDExLjYwOTQgMzYuMTU1NkMxMS43NDk3IDM2Ljg0NTYgMTIuMTEyNyAzNy40NzA1IDEyLjY0MjggMzcuOTM0MUMxMi44Nzk1IDM4LjE0MTEgMTMuMTY5NiAzOC4zMDg1IDEzLjc1IDM4LjY0MzVMMzAuNTk3NCA0OC4zNjcyQzMyLjE2NDEgNDkuMjcxNCAzMi45NDc0IDQ5LjcyMzUgMzMuODA3NSA0OS43MjM3QzM0LjY2NzcgNDkuNzIzOSAzNS40NTEzIDQ5LjI3MjMgMzcuMDE4NCA0OC4zNjg5TDU3LjY2ODQgMzYuNDY1NEM1OC4yMDM3IDM2LjE1NjkgNTguNDcxNCAzNi4wMDI2IDU4LjY3MjEgMzYuMTE4NUM1OC44NzI3IDM2LjIzNDUgNTguODcyNyAzNi41NDM0IDU4Ljg3MjcgMzcuMTYxM1Y0MC4zMzY1QzU4Ljg3MjcgNDEuMjQyNCA1OC44NzI3IDQxLjY5NTQgNTguNjU3MiA0Mi4wNjgzQzU4LjQ0MTggNDIuNDQxMyA1OC4wNDkzIDQyLjY2NzUgNTcuMjY0NCA0My4xMTk5TDQwLjIzMjIgNTIuOTM4QzM3LjA5NjYgNTQuNzQ1NCAzNS41Mjg4IDU1LjY0OTIgMzMuODA3OSA1NS42NDg0QzMyLjA4NjkgNTUuNjQ3NiAzMC41MTk5IDU0Ljc0MjQgMjcuMzg2IDUyLjkzMjFMMTEuNDUwOSA0My43MjdDMTEuNDAwMyA0My42OTc4IDExLjM3NSA0My42ODMyIDExLjM1MTQgNDMuNjY5NEM4LjAxMDIzIDQxLjcxNyA1Ljk0ODU5IDM4LjE0NTEgNS45MjkyNSAzNC4yNzU0QzUuOTI5MTIgMzQuMjQ4IDUuOTI5MTIgMzQuMjE4OCA1LjkyOTEyIDM0LjE2MDRWMzEuMjQ1OEM1LjkyOTEyIDI5LjEwOTUgNy4wNjY4OSAyNy4xMzQ5IDguOTE1MTMgMjYuMDYzNkMxMC41NDgzIDI1LjExNjkgMTIuNTYyOCAyNS4xMTUxIDE0LjE5NzcgMjYuMDU4N0wyNy4zOTY0IDMzLjY3NjRaIiBmaWxsPSIjRjJGN0Y3Ii8+Cjwvc3ZnPgo=&labelColor=5c5c5c&color=3F89A1&label=%20" alt="Documented on GitBook"></a>
|
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"></a>
|
||||||
<a href="#"><img src="https://img.shields.io/badge/Open_Source-❤️-FDA599?"/></a>
|
<a href="#"><img src="https://img.shields.io/badge/Open_Source-❤️-FDA599?"/></a>
|
||||||
<a href="/LICENSE"><img src="https://img.shields.io/badge/License-GNU_GPLv3-F4E28D"/></a>
|
<a href="/LICENSE"><img src="https://img.shields.io/badge/License-GNU_GPLv3-F4E28D"/></a>
|
||||||
<a href="/.github/CONTRIBUTING.md"><img src="https://img.shields.io/github/contributors/gitbookIO/gitbook"/></a>
|
<a href="/.github/CONTRIBUTING.md"><img src="https://img.shields.io/github/contributors/gitbookIO/gitbook"/></a>
|
||||||
@@ -14,7 +18,7 @@
|
|||||||
|
|
||||||
<p align="center">Welcome to GitBook, the platform for managing technical knowledge for teams.</p>
|
<p align="center">Welcome to GitBook, the platform for managing technical knowledge for teams.</p>
|
||||||
|
|
||||||
<p align="center">This repository contains the open-source code used to render GitBook's published content.</p>
|
<p align="center">This repository contains the open source code used to render GitBook's published content.</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="GitBook Open Published Site" src="./assets/published-site.png">
|
<img alt="GitBook Open Published Site" src="./assets/published-site.png">
|
||||||
@@ -35,8 +39,10 @@ To run a local version of this project, please follow these simple steps.
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js (Version: >=18.x)
|
- Node.js (Version: >= 22.3)
|
||||||
- Bun
|
- 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
|
||||||
|
|
||||||
### Set up
|
### Set up
|
||||||
|
|
||||||
@@ -46,32 +52,40 @@ To run a local version of this project, please follow these simple steps.
|
|||||||
git clone https://github.com/gitbookIO/gitbook.git
|
git clone https://github.com/gitbookIO/gitbook.git
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install the project's dependencies through Bun.
|
2. Ensure you are using the project's version of `node`. Running `nvm use` will change your local version to the correct one.
|
||||||
|
|
||||||
|
3. Install the project's dependencies through Bun.
|
||||||
|
|
||||||
```
|
```
|
||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start your local development server.
|
4. Start your local development server.
|
||||||
|
|
||||||
```
|
```
|
||||||
bun dev
|
bun dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open the space in your web browser, using http://localhost:3000/<host>/<path> (example: http://localhost:3000/docs.gitbook.com).
|
6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/url`.
|
||||||
|
|
||||||
### Other development commands
|
examples:
|
||||||
|
|
||||||
- `bun format`: format the code
|
- http://localhost:3000/url/gitbook.com/docs
|
||||||
- `bun lint`: lint the code
|
- http://localhost:3000/url/open-source.gitbook.io/midjourney
|
||||||
|
|
||||||
|
Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser.
|
||||||
|
|
||||||
### CI and testing
|
### CI and testing
|
||||||
|
|
||||||
All pull-requests will be tested against both visual and performances testing to prevent regressions.
|
All pull-requests will be tested against both visual and performances testing to prevent regressions.
|
||||||
|
|
||||||
|
## Fonts and Icons
|
||||||
|
|
||||||
|
GitBook Open uses fontawesome. For self-hosting and local development, for licensing reasons, only the icons from the free version should be used.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
GitBook's rendering engine is fully open-source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/.github/CONTRIBUTING.md) to learn more about the workflow on adding your first Pull Request.
|
GitBook's rendering engine is fully open source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to learn more about the workflow on adding your first Pull Request.
|
||||||
|
|
||||||
### Types of contributions
|
### Types of contributions
|
||||||
|
|
||||||
@@ -79,7 +93,7 @@ We encourage you to contribute to GitBook to help us build the best tool for doc
|
|||||||
|
|
||||||
#### Translations
|
#### Translations
|
||||||
|
|
||||||
The GitBook UI is rendered using a set of translation files found in [`src/intl/translations`](/src/intl/translations/). We welcome all additional translations for the UI.
|
The GitBook UI is rendered using a set of translation files found in [`packages/gitbook/src/intl/translations`](/packages/gitbook/src/intl/translations/). We welcome all additional translations for the UI.
|
||||||
|
|
||||||
#### Bugs
|
#### Bugs
|
||||||
|
|
||||||
@@ -90,7 +104,7 @@ Encounter a bug or find an issue you'd like to fix? Helping us fix issues relate
|
|||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> While it is possible to self-host this project, we do not recommend this unless you are certain this option fits your need.
|
> While it is possible to self-host this project, we do not recommend this unless you are certain this option fits your need.
|
||||||
>
|
>
|
||||||
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](/.github/CONTRIBUTING.md) to get started._
|
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to get started._
|
||||||
>
|
>
|
||||||
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
|
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
|
||||||
>
|
>
|
||||||
@@ -106,10 +120,30 @@ On the con side, you become responsible for the reliability of your published si
|
|||||||
|
|
||||||
Distributed under the [GNU GPLv3 License](https://github.com/GitBookIO/gitbook/blob/main/LICENSE).
|
Distributed under the [GNU GPLv3 License](https://github.com/GitBookIO/gitbook/blob/main/LICENSE).
|
||||||
|
|
||||||
If you plan to distribute the code, you must the source code public to comply with GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
|
If you plan to distribute the code, you must make the source code public to comply with the GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
|
||||||
|
|
||||||
See `LICENSE` for more information.
|
See `LICENSE` for more information.
|
||||||
|
|
||||||
|
## Badges
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"></a>
|
||||||
|
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=F4E28D"></a>
|
||||||
|
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=FDA599"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
```md
|
||||||
|
[](https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link)
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a href="https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link">
|
||||||
|
<img
|
||||||
|
src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
GitBook wouldn't be possible without these projects:
|
GitBook wouldn't be possible without these projects:
|
||||||
|
|||||||
@@ -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 |
@@ -1,22 +0,0 @@
|
|||||||
# Caching
|
|
||||||
|
|
||||||
## Revalidating the cache
|
|
||||||
|
|
||||||
Invalidate cache can be done at two levels using tags:
|
|
||||||
|
|
||||||
- Data fetching cache
|
|
||||||
- Rendering cache
|
|
||||||
|
|
||||||
To invalidate and refetch the data cache, you can execute a POST request to `/~/gitbook/revalidate`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl --location --request POST 'https://gitbook/mycompany.com/~gitbook/revalidate' \
|
|
||||||
--header 'Content-Type: application/json' \
|
|
||||||
--data-raw '{"tags": ["space.id"]}'
|
|
||||||
```
|
|
||||||
|
|
||||||
To invalidate the rendering cache, the implementation mainly depends on the infrastructure serving the content, GitBook outputs a `Cache-Tag` header on every requests. The value of the header is a comma separated list of tags.
|
|
||||||
|
|
||||||
## Purging the cache
|
|
||||||
|
|
||||||
Purging the cache, without revalidating, is done by passing `"purge": true` in the request body.
|
|
||||||
@@ -1,576 +0,0 @@
|
|||||||
import { argosScreenshot } from '@argos-ci/playwright';
|
|
||||||
import {
|
|
||||||
CustomizationHeaderPreset,
|
|
||||||
CustomizationLocale,
|
|
||||||
CustomizationSettings,
|
|
||||||
} from '@gitbook/api';
|
|
||||||
import { test, expect, Page } from '@playwright/test';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import rison from 'rison';
|
|
||||||
|
|
||||||
import { getContentTestURL } from '../tests/utils';
|
|
||||||
|
|
||||||
interface Test {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
run?: (page: Page) => Promise<unknown>;
|
|
||||||
fullPage?: boolean;
|
|
||||||
screenshot?: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TestsCase {
|
|
||||||
name: string;
|
|
||||||
baseUrl: string;
|
|
||||||
tests: Array<Test>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allLocales: CustomizationLocale[] = [
|
|
||||||
CustomizationLocale.Fr,
|
|
||||||
CustomizationLocale.Es,
|
|
||||||
CustomizationLocale.Ja,
|
|
||||||
CustomizationLocale.Zh,
|
|
||||||
];
|
|
||||||
|
|
||||||
async function waitForCookiesDialog(page: Page) {
|
|
||||||
const dialog = page.getByRole('dialog', { name: 'Cookies' });
|
|
||||||
const accept = dialog.getByRole('button', { name: 'Accept' });
|
|
||||||
const reject = dialog.getByRole('button', { name: 'Reject' });
|
|
||||||
await expect(accept).toBeVisible();
|
|
||||||
await expect(reject).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
const testCases: TestsCase[] = [
|
|
||||||
{
|
|
||||||
name: 'GitBook Site (Single Variant)',
|
|
||||||
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'Home',
|
|
||||||
url: '',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'No variants dropdown',
|
|
||||||
url: '',
|
|
||||||
run: async (page) => {
|
|
||||||
await expect(page.locator('[data-testid="space-dropdown-button"]')).toHaveCount(
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Search',
|
|
||||||
url: '?q=',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Search Results',
|
|
||||||
url: '?q=gitbook',
|
|
||||||
run: async (page) => {
|
|
||||||
await page.waitForSelector('[data-test="search-results"]');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'AI Search',
|
|
||||||
url: '?q=What+is+GitBook%3F&ask=true',
|
|
||||||
run: async (page) => {
|
|
||||||
await page.waitForSelector('[data-test="search-ask-answer"]');
|
|
||||||
},
|
|
||||||
screenshot: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Not found',
|
|
||||||
url: 'content-not-found',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'GitBook Site (Multi Variants)',
|
|
||||||
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/multi-variants/',
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'Variants dropdown',
|
|
||||||
url: '',
|
|
||||||
run: async (page) => {
|
|
||||||
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
|
|
||||||
await spaceDrowpdown.waitFor();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Default variant',
|
|
||||||
url: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'RFC variant',
|
|
||||||
url: 'v/rfcs',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'GitBook',
|
|
||||||
baseUrl: 'https://docs.gitbook.com',
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'Home',
|
|
||||||
url: '',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Search',
|
|
||||||
url: '?q=',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Search Results',
|
|
||||||
url: '?q=gitbook',
|
|
||||||
run: async (page) => {
|
|
||||||
await page.waitForSelector('[data-test="search-results"]');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'AI Search',
|
|
||||||
url: '?q=What+is+GitBook%3F&ask=true',
|
|
||||||
run: async (page) => {
|
|
||||||
await page.waitForSelector('[data-test="search-ask-answer"]');
|
|
||||||
},
|
|
||||||
screenshot: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Not found',
|
|
||||||
url: 'content-not-found',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Versioning',
|
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'Revision',
|
|
||||||
url: '~/revisions/S55pwsEr5UVoroaOiWnP/blocks/headings',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'PDF',
|
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'PDF',
|
|
||||||
url: '~gitbook/pdf?limit=10',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Content tests',
|
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'Text',
|
|
||||||
url: 'text-page',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Long text',
|
|
||||||
url: 'text-page/long-text',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Images',
|
|
||||||
url: 'blocks/block-images',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
fullPage: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Inline Images',
|
|
||||||
url: 'blocks/inline-images',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Tabs',
|
|
||||||
url: 'blocks/tabs',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Hints',
|
|
||||||
url: 'blocks/hints',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Integration Blocks',
|
|
||||||
url: 'blocks/integrations',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Tables',
|
|
||||||
url: 'blocks/tables',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
fullPage: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Expandables',
|
|
||||||
url: 'blocks/expandables',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'API Blocks',
|
|
||||||
url: 'blocks/api-blocks',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Headings',
|
|
||||||
url: 'blocks/headings',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Marks',
|
|
||||||
url: 'blocks/marks',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Emojis',
|
|
||||||
url: 'blocks/emojis',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Links',
|
|
||||||
url: 'blocks/links',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Lists',
|
|
||||||
url: 'blocks/lists',
|
|
||||||
fullPage: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Code',
|
|
||||||
url: 'blocks/code',
|
|
||||||
fullPage: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Cards',
|
|
||||||
url: 'blocks/cards',
|
|
||||||
fullPage: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Math',
|
|
||||||
url: 'blocks/math',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Embeds',
|
|
||||||
url: 'blocks/embeds',
|
|
||||||
fullPage: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Annotations',
|
|
||||||
url: 'blocks/annotations',
|
|
||||||
run: async (page) => {
|
|
||||||
await page.waitForSelector('[data-testid="annotation-button"]');
|
|
||||||
await page.click('[data-testid="annotation-button"]');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Page options',
|
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'With cover',
|
|
||||||
url: 'page-options/page-with-cover',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'With hero cover',
|
|
||||||
url: 'page-options/page-with-hero-cover',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'With cover and no TOC',
|
|
||||||
url: 'page-options/page-with-cover-and-no-toc',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Customization',
|
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'Without header',
|
|
||||||
url: getCustomizationURL({
|
|
||||||
header: {
|
|
||||||
preset: CustomizationHeaderPreset.None,
|
|
||||||
links: [],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Share links',
|
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-share-links/',
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'Valid link',
|
|
||||||
url: 'Fc6mMII9FKgnwm7qqynx/',
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Invalid link',
|
|
||||||
url: 'invalid/',
|
|
||||||
run: async (page) => {
|
|
||||||
await expect(
|
|
||||||
page.getByText('Authentication missing to access this content'),
|
|
||||||
).toBeVisible();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Visitor Auth - Space',
|
|
||||||
baseUrl: `https://gitbook.gitbook.io/gbo-va-space/`,
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'First',
|
|
||||||
url: (() => {
|
|
||||||
const privateKey = '70b844d0-c519-4532-8586-5970ce48c537';
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
name: 'gitbook-open-tests',
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
{
|
|
||||||
expiresIn: '24h',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return `first?jwt_token=${token}`;
|
|
||||||
})(),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Second',
|
|
||||||
url: (() => {
|
|
||||||
const privateKey = '70b844d0-c519-4532-8586-5970ce48c537';
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
name: 'gitbook-open-tests',
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
{
|
|
||||||
expiresIn: '24h',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return `second?jwt_token=${token}`;
|
|
||||||
})(),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Visitor Auth - Collection',
|
|
||||||
baseUrl: `https://gitbook.gitbook.io/gbo-va-collection/`,
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'Root',
|
|
||||||
url: (() => {
|
|
||||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
name: 'gitbook-open-tests',
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
{
|
|
||||||
expiresIn: '24h',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return `?jwt_token=${token}`;
|
|
||||||
})(),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Primary (Space A)',
|
|
||||||
url: (() => {
|
|
||||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
name: 'gitbook-open-tests',
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
{
|
|
||||||
expiresIn: '24h',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return `v/spacea?jwt_token=${token}`;
|
|
||||||
})(),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Space B',
|
|
||||||
url: (() => {
|
|
||||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
name: 'gitbook-open-tests',
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
{
|
|
||||||
expiresIn: '24h',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return `v/spaceb?jwt_token=${token}`;
|
|
||||||
})(),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Space C',
|
|
||||||
url: (() => {
|
|
||||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
name: 'gitbook-open-tests',
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
{
|
|
||||||
expiresIn: '24h',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return `v/spacec?jwt_token=${token}`;
|
|
||||||
})(),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Visitor Auth - Space (custom domain)',
|
|
||||||
baseUrl: `https://test.gitbook.community/`,
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
name: 'Root',
|
|
||||||
url: (() => {
|
|
||||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
name: 'gitbook-open-tests',
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
{
|
|
||||||
expiresIn: '24h',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return `?jwt_token=${token}`;
|
|
||||||
})(),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'First',
|
|
||||||
url: (() => {
|
|
||||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
name: 'gitbook-open-tests',
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
{
|
|
||||||
expiresIn: '24h',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return `first?jwt_token=${token}`;
|
|
||||||
})(),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Custom page',
|
|
||||||
url: (() => {
|
|
||||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
name: 'gitbook-open-tests',
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
{
|
|
||||||
expiresIn: '24h',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return `custom-page?jwt_token=${token}`;
|
|
||||||
})(),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Inner page',
|
|
||||||
url: (() => {
|
|
||||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
name: 'gitbook-open-tests',
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
{
|
|
||||||
expiresIn: '24h',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return `custom-page/inner-page?jwt_token=${token}`;
|
|
||||||
})(),
|
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Languages',
|
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
|
||||||
tests: allLocales.map((locale) => ({
|
|
||||||
name: locale,
|
|
||||||
url: getCustomizationURL({
|
|
||||||
internationalization: {
|
|
||||||
locale,
|
|
||||||
inherit: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
run: async (page) => {
|
|
||||||
const dialog = page.getByTestId('cookies-dialog');
|
|
||||||
await expect(dialog).toBeVisible();
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const testCase of testCases) {
|
|
||||||
test.describe(testCase.name, () => {
|
|
||||||
for (const testEntry of testCase.tests) {
|
|
||||||
test(testEntry.name, async ({ page, baseURL }) => {
|
|
||||||
const contentUrl = new URL(testEntry.url, testCase.baseUrl);
|
|
||||||
const url = getContentTestURL(contentUrl.toString(), baseURL);
|
|
||||||
await page.goto(url);
|
|
||||||
if (testEntry.run) {
|
|
||||||
await testEntry.run(page);
|
|
||||||
}
|
|
||||||
if (testEntry.screenshot !== false) {
|
|
||||||
await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, {
|
|
||||||
viewports: ['macbook-16', 'macbook-13', 'iphone-x', 'ipad-2'],
|
|
||||||
argosCSS: `
|
|
||||||
/* Hide Intercom */
|
|
||||||
.intercom-lightweight-app {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
fullPage: testEntry.fullPage ?? false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a URL with customization settings.
|
|
||||||
*/
|
|
||||||
function getCustomizationURL(partial: Partial<CustomizationSettings>): string {
|
|
||||||
const encoded = rison.encode_object(partial);
|
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
searchParams.set('customization', encoded);
|
|
||||||
|
|
||||||
return `?${searchParams.toString()}`;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
const { withSentryConfig } = require('@sentry/nextjs');
|
|
||||||
|
|
||||||
module.exports = withSentryConfig(
|
|
||||||
{
|
|
||||||
env: {
|
|
||||||
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
|
|
||||||
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
|
|
||||||
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
|
|
||||||
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
|
||||||
},
|
|
||||||
|
|
||||||
webpack(config) {
|
|
||||||
config.resolve.fallback = {
|
|
||||||
...config.resolve.fallback,
|
|
||||||
|
|
||||||
// Required for `swagger2openapi` to work:
|
|
||||||
fs: false,
|
|
||||||
path: false,
|
|
||||||
http: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
|
|
||||||
async headers() {
|
|
||||||
return [
|
|
||||||
// Cache all static assets for 1 year
|
|
||||||
{
|
|
||||||
source: '/~gitbook/static/:path*',
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: 'Cache-Control',
|
|
||||||
value: 'public, max-age=31536000, immutable',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
|
|
||||||
poweredByHeader: false,
|
|
||||||
|
|
||||||
images: {
|
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
protocol: 'https',
|
|
||||||
hostname: '*.gitbook.io',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
silent: true,
|
|
||||||
org: process.env.SENTRY_ORG,
|
|
||||||
project: process.env.SENTRY_PROJECT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
|
||||||
widenClientFileUpload: true,
|
|
||||||
transpileClientSDK: false,
|
|
||||||
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
|
|
||||||
tunnelRoute: '/~gitbook/monitoring',
|
|
||||||
// Don't hide source maps from generated client bundles
|
|
||||||
hideSourceMaps: false,
|
|
||||||
disableLogger: true,
|
|
||||||
automaticVercelMonitors: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
+66
-85
@@ -1,92 +1,73 @@
|
|||||||
{
|
{
|
||||||
"name": "gitbook",
|
"name": "gitbook",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"engines": {
|
||||||
"scripts": {
|
"node": "^22.3.0"
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
|
||||||
"build:cloudflare": "next-on-pages",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint",
|
|
||||||
"format": "prettier ./ --ignore-unknown --write",
|
|
||||||
"format:check": "prettier ./ --ignore-unknown --list-different",
|
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"unit": "bun test {src,packages}",
|
|
||||||
"e2e": "playwright test",
|
|
||||||
"postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2"
|
|
||||||
},
|
|
||||||
"workspaces": [
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"@geist-ui/icons": "^1.0.2",
|
|
||||||
"@gitbook/api": "^0.51.0",
|
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
|
||||||
"@sentry/nextjs": "^7.94.1",
|
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
|
||||||
"@upstash/redis": "^1.27.1",
|
|
||||||
"ajv": "^8.12.0",
|
|
||||||
"assert-never": "^1.2.1",
|
|
||||||
"bun-types": "^1.0.7",
|
|
||||||
"classnames": "^2.5.1",
|
|
||||||
"content-security-policy-merger": "^1.0.0",
|
|
||||||
"framer-motion": "^10.16.14",
|
|
||||||
"js-cookie": "^3.0.5",
|
|
||||||
"jsontoxml": "^1.0.1",
|
|
||||||
"katex": "^0.16.9",
|
|
||||||
"mathjax": "^3.2.2",
|
|
||||||
"memoizee": "^0.4.15",
|
|
||||||
"next": "^14.1.3",
|
|
||||||
"next-themes": "^0.2.1",
|
|
||||||
"nuqs": "^1.15.4",
|
|
||||||
"object-hash": "^3.0.0",
|
|
||||||
"openapi-types": "^12.1.3",
|
|
||||||
"p-map": "^7.0.0",
|
|
||||||
"parse-cache-control": "^1.0.1",
|
|
||||||
"react": "^18",
|
|
||||||
"react-dom": "^18",
|
|
||||||
"react-hotkeys-hook": "^4.4.1",
|
|
||||||
"recoil": "^0.7.7",
|
|
||||||
"rehype-sanitize": "^6.0.0",
|
|
||||||
"rehype-stringify": "^10.0.0",
|
|
||||||
"remark-gfm": "^4.0.0",
|
|
||||||
"remark-parse": "^11.0.0",
|
|
||||||
"remark-rehype": "^11.1.0",
|
|
||||||
"rison": "^0.1.1",
|
|
||||||
"server-only": "^0.0.1",
|
|
||||||
"shiki": "^1.2.0",
|
|
||||||
"tailwind-merge": "^2.2.0",
|
|
||||||
"tailwind-shades": "^1.1.2",
|
|
||||||
"unified": "^11.0.4",
|
|
||||||
"url-join": "^5.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@argos-ci/playwright": "^2.0.0",
|
"@changesets/cli": "^2.31.1",
|
||||||
"@cloudflare/next-on-pages": "^1.11.3",
|
"oxfmt": "^0.62.0",
|
||||||
"@cloudflare/workers-types": "^4.20231218.0",
|
"oxlint": "^1.85.0",
|
||||||
"@playwright/test": "^1.42.1",
|
"turbo": "^2.11.3",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"vercel": "50.37.3"
|
||||||
"@types/jsontoxml": "^1.0.5",
|
},
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"packageManager": "bun@1.3.7",
|
||||||
"@types/katex": "^0.16.5",
|
"overrides": {
|
||||||
"@types/node": "^20",
|
"@codemirror/state": "6.4.1",
|
||||||
"@types/object-hash": "^3.0.6",
|
"@types/react": "catalog:",
|
||||||
"@types/parse-cache-control": "^1.0.4",
|
"@types/react-dom": "catalog:",
|
||||||
"@types/psi": "^4.1.6",
|
"react": "catalog:",
|
||||||
"@types/react": "^18",
|
"react-dom": "catalog:",
|
||||||
"@types/react-dom": "^18",
|
"esbuild": "0.27.3",
|
||||||
"@types/rison": "^0.0.9",
|
"axios": "1.8.4",
|
||||||
"autoprefixer": "^10",
|
"@opennextjs/aws": "4.1.5"
|
||||||
"eslint": "^8",
|
},
|
||||||
"eslint-config-next": "13.5.6",
|
"private": true,
|
||||||
"eslint-plugin-import": "^2.29.0",
|
"scripts": {
|
||||||
"jsonwebtoken": "^9.0.2",
|
"dev": "turbo run dev --concurrency 20",
|
||||||
"postcss": "^8",
|
"build": "turbo run build",
|
||||||
"prettier": "^3.0.3",
|
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||||
"psi": "^4.1.0",
|
"typecheck": "turbo run typecheck",
|
||||||
"tailwindcss": "^3.4.0",
|
"lint": "oxlint --quiet",
|
||||||
"typescript": "^5"
|
"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 && 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/*"
|
||||||
|
],
|
||||||
|
"catalog": {
|
||||||
|
"@tsconfig/strictest": "^2.0.6",
|
||||||
|
"@tsconfig/node20": "^20.1.6",
|
||||||
|
"@base-ui/react": "^1.7.0",
|
||||||
|
"@gitbook/api": "0.202.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",
|
||||||
|
"next@16.3.3": "patches/next@16.3.3.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
dist/
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# @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
|
||||||
|
|
||||||
|
- cbc71a5: First version of the public package for typing script integrations.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 854c448: Custom assistants followup
|
||||||
|
- Updated dependencies [25e2b40]
|
||||||
|
- @gitbook/icons@0.3.0
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# `@gitbook/browser-types`
|
||||||
|
|
||||||
|
Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@gitbook/browser-types",
|
||||||
|
"description": "Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"version": "0.1.6",
|
||||||
|
"dependencies": {
|
||||||
|
"@gitbook/api": "catalog:",
|
||||||
|
"@gitbook/icons": "workspace:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"bun-types": "catalog:",
|
||||||
|
"tsdown": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/GitbookIO/gitbook"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
|
||||||
|
import type { IconName } from '@gitbook/icons';
|
||||||
|
|
||||||
|
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?:
|
||||||
|
| GitBookIntegrationToolConfirmation
|
||||||
|
| ((input: object) => GitBookIntegrationToolConfirmation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when the tool is executed.
|
||||||
|
* The input is provided by the AI assistant following the input schema of the tool.
|
||||||
|
*/
|
||||||
|
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitBookAssistant = {
|
||||||
|
/**
|
||||||
|
* Name of the assistant displayed in the UI.
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon of the assistant displayed in the UI.
|
||||||
|
* Any FontAwesome icon name is supported.
|
||||||
|
* @example 'sparkle'
|
||||||
|
*/
|
||||||
|
icon: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when the assistant is opened.
|
||||||
|
*/
|
||||||
|
open: (query?: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to display the triggers for this assistant in the UI.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
ui?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitBookCookieBannerHandler = (options: {
|
||||||
|
onApprove: () => void;
|
||||||
|
onReject: () => void;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
export type GitBookGlobal = {
|
||||||
|
/**
|
||||||
|
* Register an event listener.
|
||||||
|
*/
|
||||||
|
addEventListener: (
|
||||||
|
type: GitBookIntegrationEvent,
|
||||||
|
func: GitBookIntegrationEventCallback
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an event listener.
|
||||||
|
*/
|
||||||
|
removeEventListener: (
|
||||||
|
type: GitBookIntegrationEvent,
|
||||||
|
func: GitBookIntegrationEventCallback
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a custom tool to be exposed to the AI assistant.
|
||||||
|
*/
|
||||||
|
registerTool: (tool: GitBookIntegrationTool) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
interface Window {
|
||||||
|
/**
|
||||||
|
* Global `window.GitBook` object accessible by integrations.
|
||||||
|
*/
|
||||||
|
GitBook?: GitBookGlobal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,26 +5,21 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
},
|
|
||||||
"types": [
|
"types": [
|
||||||
"bun-types" // add Bun global
|
"bun-types" // add Bun global
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
dist/
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# @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
|
||||||
|
|
||||||
|
- 77397ca: Fix version of @gitbook/api referenced in package.json
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 116575c: Improve typing of getComputedContentSourceCacheTags to match latest API specification
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- f32bf1f: Export function `getCacheTagForURL` to easily get the cache tag for a URL.
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 05ffd0e: Initial version of the package
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# `@gitbook/cache-tags`
|
||||||
|
|
||||||
|
Utility to generate cache tags for GitBook Open.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@gitbook/cache-tags",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"version": "0.3.4",
|
||||||
|
"dependencies": {
|
||||||
|
"@gitbook/api": "catalog:",
|
||||||
|
"assert-never": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"bun-types": "catalog:",
|
||||||
|
"tsdown": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/GitbookIO/gitbook"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
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
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'user';
|
||||||
|
user: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* All data related to a space
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'space';
|
||||||
|
space: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* All data related to an integration.
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'integration';
|
||||||
|
integration: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* All data related to a change request
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'change-request';
|
||||||
|
space: string;
|
||||||
|
changeRequest: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Immutable data related to a revision
|
||||||
|
* @deprecated - in v2, no tag as this is an immutable data
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'revision';
|
||||||
|
space: string;
|
||||||
|
revision: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Immutable data related to a document
|
||||||
|
* @deprecated - in v2, no tag as this is an immutable data
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'document';
|
||||||
|
space: string;
|
||||||
|
document: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Immutable data related to a computed document
|
||||||
|
* @deprecated - in v2, no tag as this is an immutable data
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'computed-document';
|
||||||
|
space: string;
|
||||||
|
sourceType: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* All data related to the URL of a content
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'url';
|
||||||
|
hostname: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* All data related to a site
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'site';
|
||||||
|
site: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* All data related to an OpenAPI spec
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'openapi';
|
||||||
|
organization: string;
|
||||||
|
openAPISpec: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* All data related to a translation
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'translation';
|
||||||
|
organization: string;
|
||||||
|
translation: string;
|
||||||
|
}
|
||||||
|
): string {
|
||||||
|
switch (spec.tag) {
|
||||||
|
case 'user':
|
||||||
|
return `user:${spec.user}`;
|
||||||
|
case 'url':
|
||||||
|
return `url:${spec.hostname}`;
|
||||||
|
case 'space':
|
||||||
|
return `space:${spec.space}`;
|
||||||
|
case 'change-request':
|
||||||
|
return `space:${spec.space}:change-request:${spec.changeRequest}`;
|
||||||
|
case 'revision':
|
||||||
|
return `space:${spec.space}:revision:${spec.revision}`;
|
||||||
|
case 'document':
|
||||||
|
return `space:${spec.space}:document:${spec.document}`;
|
||||||
|
case 'computed-document':
|
||||||
|
return `space:${spec.space}:computed-document:${spec.sourceType}`;
|
||||||
|
case 'site':
|
||||||
|
return `site:${spec.site}`;
|
||||||
|
case 'integration':
|
||||||
|
return `integration:${spec.integration}`;
|
||||||
|
case 'openapi':
|
||||||
|
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
|
||||||
|
case 'translation':
|
||||||
|
return `organization:${spec.organization}:translation:${spec.translation}`;
|
||||||
|
default:
|
||||||
|
assertNever(spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tags for a computed content source.
|
||||||
|
*/
|
||||||
|
export function getComputedContentSourceCacheTags(
|
||||||
|
inContext: {
|
||||||
|
spaceId: string;
|
||||||
|
organizationId: string;
|
||||||
|
},
|
||||||
|
source: ComputedContentSource
|
||||||
|
) {
|
||||||
|
const tags: string[] = [];
|
||||||
|
|
||||||
|
if (!('dependencies' in source)) {
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We add the dependencies as tags, to ensure that the computed content is invalidated
|
||||||
|
// when the dependencies are updated.
|
||||||
|
const dependencies = Object.values(source.dependencies ?? {});
|
||||||
|
if (dependencies.length > 0) {
|
||||||
|
dependencies.forEach((dependency) => {
|
||||||
|
switch (dependency.ref.kind) {
|
||||||
|
case 'space':
|
||||||
|
tags.push(
|
||||||
|
getCacheTag({
|
||||||
|
tag: 'space',
|
||||||
|
space: dependency.ref.space,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'openapi':
|
||||||
|
tags.push(
|
||||||
|
getCacheTag({
|
||||||
|
tag: 'openapi',
|
||||||
|
organization: inContext.organizationId,
|
||||||
|
openAPISpec: dependency.ref.spec,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'translation':
|
||||||
|
tags.push(
|
||||||
|
getCacheTag({
|
||||||
|
tag: 'translation',
|
||||||
|
organization: inContext.organizationId,
|
||||||
|
translation: dependency.ref.translation,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Do not throw for unknown dependency types
|
||||||
|
// as it might mean we are lagging behind the API version
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Push a dummy tag, as the v1 is only using the first tag
|
||||||
|
tags.push(
|
||||||
|
getCacheTag({
|
||||||
|
tag: 'computed-document',
|
||||||
|
space: inContext.spaceId,
|
||||||
|
sourceType: source.type,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]!;
|
||||||
|
tags.push(
|
||||||
|
getCacheTag({
|
||||||
|
tag: 'integration',
|
||||||
|
integration,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"types": [
|
||||||
|
"bun-types" // add Bun global
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
dist/
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# @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
|
||||||
|
|
||||||
|
- c3f6b8c: Update chroma ratio per step
|
||||||
|
- 5e975ab: Fix code highlighting for HTTP
|
||||||
|
- f7a3470: Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
|
||||||
|
|
||||||
|
## 0.3.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- cdffd7c: Desaturate text colors by decreasing chroma for the last steps of the color scale
|
||||||
|
|
||||||
|
## 0.3.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- fb90eb0: Reduce chroma of first color scale step
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 4f0a772: Override tint lightness if supplied color is out of bounds
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 445baaa: Initial release
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# `@gitbook/colors`
|
||||||
|
|
||||||
|
A set of default colors and transformation functions used throughout the GitBook Open and app.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "@gitbook/colors",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"version": "0.4.4",
|
||||||
|
"devDependencies": {
|
||||||
|
"bun-types": "catalog:",
|
||||||
|
"tsdown": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsdown",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"unit": "bun test",
|
||||||
|
"dev": "bun run build -- --watch ./src",
|
||||||
|
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md",
|
||||||
|
"CHANGELOG.md"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/GitbookIO/gitbook"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Default primary color throughout the GitBook ecosystem.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_PRIMARY_COLOR = '#346DDB';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The darkest color that exists in GitBook, used as the relative minimum of every generated color scale.
|
||||||
|
*/
|
||||||
|
export const DARK_BASE = '#1D1D1D';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lightest color that exists in GitBook, used as the relative maximum of every generated color scale.
|
||||||
|
*/
|
||||||
|
export const LIGHT_BASE = '#FFFFFF';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as the basis of all UI elements that are not colored by the primary color. Neutral gray by default, overridden by site customization.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_TINT_COLOR = '#787878';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for informational messages and neutral alerts.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_HINT_INFO_COLOR = '#787878';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for showing important information or non-critical warnings.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_HINT_WARNING_COLOR = '#FE9A00';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for destructive actions or raising attention to critical information.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_HINT_DANGER_COLOR = '#FB2C36';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for showing positive actions or achievements.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_HINT_SUCCESS_COLOR = '#00C950';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './colors';
|
||||||
|
export * from './transformations';
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,493 @@
|
|||||||
|
import { DARK_BASE, DEFAULT_TINT_COLOR, LIGHT_BASE } from './colors';
|
||||||
|
|
||||||
|
type ColorShades = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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] as const; // Reference white (D65)
|
||||||
|
|
||||||
|
export enum ColorCategory {
|
||||||
|
backgrounds = 'backgrounds',
|
||||||
|
components = 'components',
|
||||||
|
borders = 'borders',
|
||||||
|
accents = 'accents',
|
||||||
|
text = 'text',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColorSubScale = {
|
||||||
|
[key: string]: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main color scale object.
|
||||||
|
*
|
||||||
|
* Each `ColorCategory` can be in/excluded in Tailwind's utility classes generation.
|
||||||
|
* Each subitem maps a semantic name within that category to a step in the scale.
|
||||||
|
*/
|
||||||
|
export const scale: Record<ColorCategory, ColorSubScale> = {
|
||||||
|
[ColorCategory.backgrounds]: {
|
||||||
|
/** Base background */
|
||||||
|
base: 1,
|
||||||
|
/** Accent background */
|
||||||
|
subtle: 2,
|
||||||
|
},
|
||||||
|
[ColorCategory.components]: {
|
||||||
|
/** Component background */
|
||||||
|
DEFAULT: 3,
|
||||||
|
/** Component hover background */
|
||||||
|
hover: 4,
|
||||||
|
/** Component active background */
|
||||||
|
active: 5,
|
||||||
|
},
|
||||||
|
[ColorCategory.borders]: {
|
||||||
|
/** Subtle borders, separators */
|
||||||
|
subtle: 6,
|
||||||
|
/** Element border, focus rings */
|
||||||
|
DEFAULT: 7,
|
||||||
|
/** Element hover border */
|
||||||
|
hover: 8,
|
||||||
|
},
|
||||||
|
[ColorCategory.accents]: {
|
||||||
|
/** Solid backgrounds */
|
||||||
|
solid: 9,
|
||||||
|
/** Hovered solid backgrounds */
|
||||||
|
'solid-hover': 10,
|
||||||
|
/** Original color */
|
||||||
|
original: 'original',
|
||||||
|
},
|
||||||
|
[ColorCategory.text]: {
|
||||||
|
/** Very low-contrast text
|
||||||
|
* Caution: this contrast does not meet accessiblity guidelines.
|
||||||
|
* Always check if you need to include a mitigating contrast-more style for users who need it. */
|
||||||
|
subtle: 9,
|
||||||
|
/** Low-contrast text */
|
||||||
|
DEFAULT: 11,
|
||||||
|
/** High-contrast text */
|
||||||
|
strong: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mix of foreground and background for every step in a colour scale.
|
||||||
|
* 0: 100% of the background color's luminosity, white in light mode
|
||||||
|
* 1: 100% of the foreground color's luminosity, black in light mode
|
||||||
|
*/
|
||||||
|
export const colorMixMapping = {
|
||||||
|
// bgs |components |borders |solid |text
|
||||||
|
light: [0, 0.02, 0.03, 0.05, 0.07, 0.1, 0.15, 0.2, 0.5, 0.55, 0.6, 1],
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
export function hexToRgb(hex: string): string {
|
||||||
|
const [r, g, b] = hexToRgbArray(hex);
|
||||||
|
// Return the RGB values separated by spaces
|
||||||
|
return `${r} ${g} ${b}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a hex color to a RGBA color.
|
||||||
|
*/
|
||||||
|
export function hexToRgba(hex: string, alpha: number): string {
|
||||||
|
const [r, g, b] = hexToRgbArray(hex);
|
||||||
|
// Return the RGBA values separated by spaces
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate Tailwind-compatible shades from a single color
|
||||||
|
* @param {string} hex The hex code to generate shades from
|
||||||
|
* @param {boolean} halfShades Generate additional shades, e.g. at 150
|
||||||
|
* @returns {{[key: number]: string}}
|
||||||
|
*/
|
||||||
|
export function shadesOfColor(hex: string, halfShades = false) {
|
||||||
|
const baseColor = hex;
|
||||||
|
|
||||||
|
const shades = [
|
||||||
|
50,
|
||||||
|
100,
|
||||||
|
200,
|
||||||
|
300,
|
||||||
|
400,
|
||||||
|
500,
|
||||||
|
600,
|
||||||
|
700,
|
||||||
|
800,
|
||||||
|
900,
|
||||||
|
...(halfShades ? [150, 250, 350, 450, 550, 650, 750, 850] : []),
|
||||||
|
].sort();
|
||||||
|
|
||||||
|
const result: ColorShades = {};
|
||||||
|
|
||||||
|
for (const shade of shades) {
|
||||||
|
const key = shade.toString();
|
||||||
|
|
||||||
|
if (shade === 500) {
|
||||||
|
result[key] = hex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shadeIndex = shade;
|
||||||
|
const isDarkShade = shadeIndex > 500;
|
||||||
|
if (isDarkShade) {
|
||||||
|
shadeIndex -= 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percentage = shadeIndex / 500;
|
||||||
|
const startColor = isDarkShade ? DARK_BASE : baseColor;
|
||||||
|
const endColor = isDarkShade ? baseColor : LIGHT_BASE;
|
||||||
|
|
||||||
|
result[key] = getColor(percentage, hexToRgbArray(startColor), hexToRgbArray(endColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColorScaleOptions = {
|
||||||
|
/** If set to `true`, inverts the scale (so 1 is black instead of white) and uses `colorMixMapping.dark` with different mix ratios per step. */
|
||||||
|
darkMode?: boolean;
|
||||||
|
|
||||||
|
/** Define a custom background color to use. If left undefined, the global `light`/`dark` values (in `colors.ts`) will be used. */
|
||||||
|
background?: string;
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
|
||||||
|
/** Define a custom mix ratio to mix the `mix` color with. If left undefined, the default ratio will be used. */
|
||||||
|
ratio: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a [Radix-like](https://www.radix-ui.com/colors/docs/palette-composition/understanding-the-scale) colour scale based of a hex colour.
|
||||||
|
* @param {string} hex The hex code to generate shades from
|
||||||
|
* @param {object} options
|
||||||
|
*/
|
||||||
|
export function colorScale(
|
||||||
|
hex: string,
|
||||||
|
{
|
||||||
|
darkMode = false,
|
||||||
|
background = darkMode ? DARK_BASE : LIGHT_BASE,
|
||||||
|
foreground = darkMode ? LIGHT_BASE : DARK_BASE,
|
||||||
|
baseStep,
|
||||||
|
mix,
|
||||||
|
}: ColorScaleOptions = {}
|
||||||
|
) {
|
||||||
|
const baseColor = rgbToOklch(hexToRgbArray(hex));
|
||||||
|
const mixColor = mix?.color ? rgbToOklch(hexToRgbArray(mix.color)) : null;
|
||||||
|
const foregroundColor = rgbToOklch(hexToRgbArray(foreground));
|
||||||
|
const backgroundColor = rgbToOklch(hexToRgbArray(background));
|
||||||
|
let mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
|
||||||
|
|
||||||
|
// 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 (isExactBase) {
|
||||||
|
const difference = (backgroundColor.L - baseColor.L) / backgroundColor.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++) {
|
||||||
|
if (isExactBase && index === exactBaseIndex) {
|
||||||
|
result.push(hex);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = mapping[index]!;
|
||||||
|
const targetL = foregroundColor.L * step + backgroundColor.L * (1 - step);
|
||||||
|
|
||||||
|
if (
|
||||||
|
index === 8 &&
|
||||||
|
!mix &&
|
||||||
|
(darkMode ? targetL - baseColor.L < 0.2 : baseColor.L - targetL < 0.2)
|
||||||
|
) {
|
||||||
|
// Original colour is close enough to target, so let's use the original colour as step 9.
|
||||||
|
result.push(hex);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chromaRatio = (() => {
|
||||||
|
switch (index) {
|
||||||
|
// Step 9 and 10 have max chroma, meaning they are fully saturated.
|
||||||
|
case 8:
|
||||||
|
case 9:
|
||||||
|
return 1;
|
||||||
|
// Step 11 and 12 have a reduced chroma
|
||||||
|
case 10:
|
||||||
|
return 0.4;
|
||||||
|
case 11:
|
||||||
|
return 0.1;
|
||||||
|
default:
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const shade = {
|
||||||
|
L: targetL, // Blend lightness
|
||||||
|
C: baseColor.C * chromaRatio,
|
||||||
|
H: baseColor.H, // Maintain the hue from the base color
|
||||||
|
};
|
||||||
|
|
||||||
|
const newHex = rgbArrayToHex(oklchToRgb(shade));
|
||||||
|
|
||||||
|
result.push(newHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a hex color to an RGB color set.
|
||||||
|
*/
|
||||||
|
export function hexToRgbArray(hex: string): RGBColor {
|
||||||
|
const originalHex = hex;
|
||||||
|
|
||||||
|
let value = hex.replace('#', '');
|
||||||
|
if (hex.length === 3) value = value + value;
|
||||||
|
|
||||||
|
const r = value.substring(0, 2);
|
||||||
|
const g = value.substring(2, 4);
|
||||||
|
const b = value.substring(4, 6);
|
||||||
|
|
||||||
|
const rgb = [r, g, b].map((channel) => {
|
||||||
|
try {
|
||||||
|
const channelInt = Number.parseInt(channel, 16);
|
||||||
|
if (channelInt < 0 || channelInt > 255) throw new Error();
|
||||||
|
return channelInt;
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Invalid hex color provided: ${originalHex}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rgb as RGBColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a RGB color set to a hex color.
|
||||||
|
*/
|
||||||
|
export function rgbArrayToHex(rgb: RGBColor): string {
|
||||||
|
return `#${rgb
|
||||||
|
.map((channel) => {
|
||||||
|
const component = channel.toString(16);
|
||||||
|
if (component.length === 1) return `0${component}`;
|
||||||
|
return component;
|
||||||
|
})
|
||||||
|
.join('')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
|
||||||
|
const rgb = end.map((channel, index) => {
|
||||||
|
return Math.round(channel + percentage * (start[index]! - channel));
|
||||||
|
});
|
||||||
|
|
||||||
|
return rgbArrayToHex(rgb as RGBColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility constants and helper functions
|
||||||
|
export function rgbToLinear(rgb: RGBColor): [number, number, number] {
|
||||||
|
return rgb.map((v) => {
|
||||||
|
const scaled = v / 255;
|
||||||
|
return scaled <= 0.04045 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4;
|
||||||
|
}) as [number, number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function linearToRgb(linear: [number, number, number]): RGBColor {
|
||||||
|
return linear.map((v) => {
|
||||||
|
const scaled = v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055;
|
||||||
|
return Math.round(Math.max(0, Math.min(1, scaled)) * 255);
|
||||||
|
}) as RGBColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgbToOklab(rgb: RGBColor): OKLABColor {
|
||||||
|
const [r, g, b] = rgbToLinear(rgb);
|
||||||
|
|
||||||
|
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
||||||
|
const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
||||||
|
const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
||||||
|
|
||||||
|
const lRoot = Math.cbrt(l);
|
||||||
|
const mRoot = Math.cbrt(m);
|
||||||
|
const sRoot = Math.cbrt(s);
|
||||||
|
|
||||||
|
return {
|
||||||
|
L: 0.2104542553 * lRoot + 0.793617785 * mRoot - 0.0040720468 * sRoot,
|
||||||
|
A: 1.9779984951 * lRoot - 2.428592205 * mRoot + 0.4505937099 * sRoot,
|
||||||
|
B: 0.0259040371 * lRoot + 0.7827717662 * mRoot - 0.808675766 * sRoot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function oklabToRgb(oklab: OKLABColor): RGBColor {
|
||||||
|
const { L, A, B } = oklab;
|
||||||
|
|
||||||
|
const lRoot = L + 0.3963377774 * A + 0.2158037573 * B;
|
||||||
|
const mRoot = L - 0.1055613458 * A - 0.0638541728 * B;
|
||||||
|
const sRoot = L - 0.0894841775 * A - 1.291485548 * B;
|
||||||
|
|
||||||
|
const l = lRoot ** 3;
|
||||||
|
const m = mRoot ** 3;
|
||||||
|
const s = sRoot ** 3;
|
||||||
|
|
||||||
|
const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
|
||||||
|
const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
|
||||||
|
const b = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
|
||||||
|
|
||||||
|
return linearToRgb([r, g, b]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function oklabToOklch(oklab: OKLABColor): OKLCHColor {
|
||||||
|
const { L, A, B } = oklab;
|
||||||
|
const C = Math.sqrt(A ** 2 + B ** 2);
|
||||||
|
const H = (Math.atan2(B, A) * 180) / Math.PI;
|
||||||
|
return { L, C, H: H < 0 ? H + 360 : H };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function oklchToOklab(oklch: OKLCHColor): OKLABColor {
|
||||||
|
const { L, C, H } = oklch;
|
||||||
|
const rad = (H * Math.PI) / 180;
|
||||||
|
return {
|
||||||
|
L,
|
||||||
|
A: C * Math.cos(rad),
|
||||||
|
B: C * Math.sin(rad),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgbToOklch(rgb: RGBColor): OKLCHColor {
|
||||||
|
return oklabToOklch(rgbToOklab(rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function oklchToRgb(oklch: OKLCHColor): RGBColor {
|
||||||
|
return oklabToRgb(oklchToOklab(oklch));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgbToXyz(rgb: RGBColor): [number, number, number] {
|
||||||
|
const [r, g, b] = rgbToLinear(rgb);
|
||||||
|
return [
|
||||||
|
(r * 0.4124564 + g * 0.3575761 + b * 0.1804375) * 100,
|
||||||
|
(r * 0.2126729 + g * 0.7151522 + b * 0.072175) * 100,
|
||||||
|
(r * 0.0193339 + g * 0.119192 + b * 0.9503041) * 100,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function xyzToLab65(xyz: [number, number, number]): {
|
||||||
|
L: number;
|
||||||
|
A: number;
|
||||||
|
B: number;
|
||||||
|
} {
|
||||||
|
const [x, y, z] = xyz.map((v, 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!),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } {
|
||||||
|
return xyzToLab65(rgbToXyz(rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Delta Phi Star perceptual lightness contrast by Andrew Somers:
|
||||||
|
https://github.com/Myndex/deltaphistar
|
||||||
|
*/
|
||||||
|
export const PHI = 0.5 + Math.sqrt(1.25);
|
||||||
|
|
||||||
|
export function dpsContrast(a: RGBColor, b: RGBColor) {
|
||||||
|
const dps = Math.abs(rgbTolab65(a).L ** PHI - rgbTolab65(b).L ** PHI);
|
||||||
|
const contrast = dps ** (1 / PHI) * Math.SQRT2 - 40;
|
||||||
|
return contrast < 7.5 ? 0 : contrast;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorContrast(
|
||||||
|
background: string,
|
||||||
|
foreground: string[] = [LIGHT_BASE, DARK_BASE]
|
||||||
|
): string {
|
||||||
|
const bg = hexToRgbArray(background);
|
||||||
|
|
||||||
|
const best: { color?: RGBColor; contrast: number } = {
|
||||||
|
color: undefined,
|
||||||
|
contrast: 0,
|
||||||
|
};
|
||||||
|
for (const color of foreground) {
|
||||||
|
const c = hexToRgbArray(color);
|
||||||
|
|
||||||
|
const contrast = dpsContrast(c, bg);
|
||||||
|
if (contrast > best.contrast) {
|
||||||
|
best.color = c;
|
||||||
|
best.contrast = contrast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best.color ? rgbArrayToHex(best.color) : foreground[0] || LIGHT_BASE;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"types": [
|
||||||
|
"bun-types" // add Bun global
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
standalone/
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# @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
|
||||||
|
|
||||||
|
- 81a6bd7: Improve API to control the GitBook embed
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 8927e8f: Initial version of the embed SDK.
|
||||||
|
- Updated dependencies [25e2b40]
|
||||||
|
- @gitbook/icons@0.3.0
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
# GitBook Docs Embed (`@gitbook/embed`)
|
||||||
|
|
||||||
|
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 standalone script from your docs site
|
||||||
|
|
||||||
|
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 `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('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:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createGitBook } from '@gitbook/embed';
|
||||||
|
|
||||||
|
const gitbook = createGitBook({
|
||||||
|
siteURL: 'https://docs.company.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an iframe and get its URL
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
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
|
||||||
|
|
||||||
|
After installing the NPM package, you can import prebuilt React components:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
|
||||||
|
|
||||||
|
<GitBookProvider siteURL="https://docs.company.com">
|
||||||
|
<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'
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "@gitbook/embed",
|
||||||
|
"description": "Embeddable components for GitBook",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./react": {
|
||||||
|
"types": "./dist/react/index.d.ts",
|
||||||
|
"default": "./dist/react/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"version": "0.5.2",
|
||||||
|
"dependencies": {
|
||||||
|
"@gitbook/api": "catalog:",
|
||||||
|
"@gitbook/icons": "workspace:",
|
||||||
|
"bidc": "catalog:"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"bun-types": "catalog:",
|
||||||
|
"@types/react": "catalog:",
|
||||||
|
"tsdown": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"react": "catalog:"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { type GitBookFrameClient, createGitBookFrame } from './createGitBookFrame';
|
||||||
|
|
||||||
|
export type CreateGitBookOptions = {
|
||||||
|
/**
|
||||||
|
* URL of the GitBook site to embed.
|
||||||
|
*/
|
||||||
|
siteURL: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
visitor?: {
|
||||||
|
/**
|
||||||
|
* Signed JWT token for Adaptive Content or Visitor Authentication to use.
|
||||||
|
*/
|
||||||
|
token?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsigned claims to pass to the frame.
|
||||||
|
* You can use these claims in dynamic expressions using `visitor.claims.unsigned.<claim-name>`.
|
||||||
|
*/
|
||||||
|
unsignedClaims?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitBookClient = {
|
||||||
|
/**
|
||||||
|
* Get the URL for a GitBook frame.
|
||||||
|
*/
|
||||||
|
getFrameURL: (options: GetFrameURLOptions) => string;
|
||||||
|
/**
|
||||||
|
* Create a new GitBook frame.
|
||||||
|
*/
|
||||||
|
createFrame: (iframe: HTMLIFrameElement) => GitBookFrameClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
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`;
|
||||||
|
|
||||||
|
if (frameOptions.colorScheme) {
|
||||||
|
url.searchParams.set('theme', frameOptions.colorScheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameOptions.visitor?.token) {
|
||||||
|
url.searchParams.set('jwt_token', frameOptions.visitor.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameOptions.visitor?.unsignedClaims) {
|
||||||
|
Object.entries(frameOptions.visitor.unsignedClaims).forEach(([key, value]) => {
|
||||||
|
url.searchParams.set(`visitor.${key}`, String(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
},
|
||||||
|
createFrame: (iframe) => createGitBookFrame(iframe),
|
||||||
|
};
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { createChannel } from 'bidc';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
FrameToParentMessage,
|
||||||
|
GitBookEmbeddableConfiguration,
|
||||||
|
ParentToFrameMessage,
|
||||||
|
} from './protocol';
|
||||||
|
|
||||||
|
export type GitBookFrameClient = {
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the assistant.
|
||||||
|
*/
|
||||||
|
navigateToAssistant: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post a message to the chat.
|
||||||
|
*/
|
||||||
|
postUserMessage: (message: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the chat.
|
||||||
|
*/
|
||||||
|
clearChat: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the placeholder settings.
|
||||||
|
*/
|
||||||
|
configure: (settings: Partial<GitBookEmbeddableConfiguration>) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an event listener.
|
||||||
|
*/
|
||||||
|
on: (event: string, listener: (...args: any[]) => void) => () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a client to communicate with the GitBook Assistant frame.
|
||||||
|
*/
|
||||||
|
export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClient {
|
||||||
|
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) => {
|
||||||
|
console.log('[gitbook:embed] received message', message);
|
||||||
|
if (message.type === 'close') {
|
||||||
|
const listeners = events.get('close') || [];
|
||||||
|
if (listeners) {
|
||||||
|
listeners.forEach((listener) => listener());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendToFrame = (message: ParentToFrameMessage) => {
|
||||||
|
console.log('[gitbook:embed] send message', message);
|
||||||
|
channel.send(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = new Map<string, ((...args: any[]) => void)[]>();
|
||||||
|
|
||||||
|
const configuration: GitBookEmbeddableConfiguration = {
|
||||||
|
tabs: ['assistant', 'search', 'docs'],
|
||||||
|
actions: [],
|
||||||
|
greeting: { title: '', subtitle: '' },
|
||||||
|
suggestions: [],
|
||||||
|
tools: [],
|
||||||
|
trademark: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
navigateToPage: (pagePath) => {
|
||||||
|
sendToFrame({ type: 'navigateToPage', pagePath });
|
||||||
|
},
|
||||||
|
navigateToAssistant: () => {
|
||||||
|
sendToFrame({ type: 'navigateToAssistant' });
|
||||||
|
},
|
||||||
|
postUserMessage: (message) => sendToFrame({ type: 'postUserMessage', message }),
|
||||||
|
configure: (settings) => {
|
||||||
|
Object.assign(configuration, settings);
|
||||||
|
sendToFrame({ type: 'configure', settings: configuration });
|
||||||
|
},
|
||||||
|
clearChat: () => sendToFrame({ type: 'clearChat' }),
|
||||||
|
on: (event, listener) => {
|
||||||
|
const listeners = events.get(event) || [];
|
||||||
|
listeners.push(listener);
|
||||||
|
events.set(event, listeners);
|
||||||
|
return () => {
|
||||||
|
events.set(
|
||||||
|
event,
|
||||||
|
listeners.filter((l) => l !== listener)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './createGitBook';
|
||||||
|
export * from './createGitBookFrame';
|
||||||
|
export * from './protocol';
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
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?: GitBookToolConfirmation | ((input: object) => GitBookToolConfirmation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when the tool is executed.
|
||||||
|
* The input is provided by the AI assistant following the input schema of the tool.
|
||||||
|
*/
|
||||||
|
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom button definition to be passed to the embeddable GitBook.
|
||||||
|
*/
|
||||||
|
export type GitBookEmbeddableActionDefinition = {
|
||||||
|
/**
|
||||||
|
* Icon to be displayed in the button.
|
||||||
|
*/
|
||||||
|
icon: IconName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label to be displayed in the button.
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when the button is clicked.
|
||||||
|
*/
|
||||||
|
onClick: () => void | Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional buttons to be displayed in the header of the GitBook embed.
|
||||||
|
* @deprecated Use `actions` instead.
|
||||||
|
*/
|
||||||
|
buttons?: GitBookEmbeddableActionDefinition[];
|
||||||
|
|
||||||
|
/** Message to be displayed in the welcome page. */
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Messages sent from the parent to the frame.
|
||||||
|
*/
|
||||||
|
export type ParentToFrameMessage =
|
||||||
|
| {
|
||||||
|
type: 'postUserMessage';
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'clearChat';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'configure';
|
||||||
|
settings: GitBookEmbeddableConfiguration;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'navigateToPage';
|
||||||
|
pagePath: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'navigateToAssistant';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Messages sent from the frame to the parent.
|
||||||
|
*/
|
||||||
|
export type FrameToParentMessage = {
|
||||||
|
type: 'close';
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './client';
|
||||||
|
export * from './standalone';
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
GetFrameURLOptions,
|
||||||
|
GitBookEmbeddableConfiguration,
|
||||||
|
GitBookFrameClient,
|
||||||
|
} from '../client';
|
||||||
|
import { useGitBook } from './GitBookProvider';
|
||||||
|
|
||||||
|
export type GitBookFrameProps = {
|
||||||
|
className?: string;
|
||||||
|
} & GetFrameURLOptions &
|
||||||
|
Partial<GitBookEmbeddableConfiguration>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a frame with the GitBook Assistant in it.
|
||||||
|
*/
|
||||||
|
export function GitBookFrame(props: GitBookFrameProps) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
colorScheme,
|
||||||
|
visitor,
|
||||||
|
actions = [],
|
||||||
|
greeting,
|
||||||
|
suggestions = [],
|
||||||
|
tools = [],
|
||||||
|
tabs = ['assistant', 'search', 'docs'],
|
||||||
|
trademark = true,
|
||||||
|
closeButton = false,
|
||||||
|
assistantName,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
const gitbook = useGitBook();
|
||||||
|
const [gitbookFrame, setGitbookFrame] = useState<GitBookFrameClient | null>(null);
|
||||||
|
|
||||||
|
const frameURL = useMemo(
|
||||||
|
() => gitbook.getFrameURL({ visitor, colorScheme }),
|
||||||
|
[gitbook, visitor, colorScheme]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (frameRef.current) {
|
||||||
|
setGitbookFrame(gitbook.createFrame(frameRef.current));
|
||||||
|
}
|
||||||
|
}, [gitbook]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
gitbookFrame?.configure({
|
||||||
|
tabs,
|
||||||
|
actions,
|
||||||
|
greeting,
|
||||||
|
suggestions,
|
||||||
|
tools,
|
||||||
|
closeButton,
|
||||||
|
trademark,
|
||||||
|
assistantName,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
gitbookFrame,
|
||||||
|
actions,
|
||||||
|
greeting,
|
||||||
|
suggestions,
|
||||||
|
tools,
|
||||||
|
tabs,
|
||||||
|
closeButton,
|
||||||
|
trademark,
|
||||||
|
assistantName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
title="GitBook"
|
||||||
|
ref={frameRef}
|
||||||
|
src={frameURL}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
allow="clipboard-write"
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { type CreateGitBookOptions, createGitBook } from '../client';
|
||||||
|
import { GitBookContext } from './context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider for the GitBook client.
|
||||||
|
*/
|
||||||
|
export function GitBookProvider(props: React.PropsWithChildren<CreateGitBookOptions>) {
|
||||||
|
const { siteURL, children } = props;
|
||||||
|
|
||||||
|
const options = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
siteURL,
|
||||||
|
}),
|
||||||
|
[siteURL]
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = React.useMemo(() => createGitBook(options), [options]);
|
||||||
|
|
||||||
|
return <GitBookContext.Provider value={client}>{children}</GitBookContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to access the GitBook client.
|
||||||
|
*/
|
||||||
|
export function useGitBook() {
|
||||||
|
const context = React.useContext(GitBookContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('This component must be used within a <GitBookProvider />');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import type { GitBookClient } from '../client';
|
||||||
|
|
||||||
|
export const GitBookContext = React.createContext<GitBookClient | null>(null);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './GitBookProvider';
|
||||||
|
export * from './GitBookFrame';
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import './style.css';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CreateGitBookOptions,
|
||||||
|
type GetFrameURLOptions,
|
||||||
|
type GitBookClient,
|
||||||
|
type GitBookEmbeddableConfiguration,
|
||||||
|
type GitBookFrameClient,
|
||||||
|
createGitBook,
|
||||||
|
} from '../client';
|
||||||
|
|
||||||
|
export type GitBook = () => void;
|
||||||
|
|
||||||
|
type StandaloneCalls =
|
||||||
|
// Initialize the widget
|
||||||
|
| ['init', CreateGitBookOptions, GetFrameURLOptions]
|
||||||
|
// Unload the widget
|
||||||
|
| ['unload']
|
||||||
|
// Show the widget
|
||||||
|
| ['show']
|
||||||
|
// Hide the widget
|
||||||
|
| ['hide']
|
||||||
|
// Open the window
|
||||||
|
| ['open']
|
||||||
|
// Close the window
|
||||||
|
| ['close']
|
||||||
|
// Toggle the window
|
||||||
|
| ['toggle']
|
||||||
|
// Post a user message
|
||||||
|
| ['postUserMessage', string]
|
||||||
|
// Clear the chat
|
||||||
|
| ['clearChat']
|
||||||
|
// Configure the embed
|
||||||
|
| ['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" data-icon="${frameConfiguration.button.icon}"></span>
|
||||||
|
<span id="gitbook-widget-button-label">${frameConfiguration.button.label}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const widgetWindow = document.createElement('div');
|
||||||
|
widgetWindow.id = 'gitbook-widget-window';
|
||||||
|
widgetWindow.classList.add('hidden');
|
||||||
|
|
||||||
|
document.body.appendChild(widgetButton);
|
||||||
|
document.body.appendChild(widgetWindow);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
throw new Error(
|
||||||
|
'GitBook client not initialized. Call GitBook("init", { siteURL: "..." }) first.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _client;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIframe() {
|
||||||
|
if (!widgetIframe || !_frame) {
|
||||||
|
const client = getClient();
|
||||||
|
|
||||||
|
widgetIframe?.remove();
|
||||||
|
widgetIframe = document.createElement('iframe');
|
||||||
|
widgetIframe.id = 'gitbook-widget-iframe';
|
||||||
|
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': {
|
||||||
|
// `~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 = {
|
||||||
|
// 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':
|
||||||
|
widgetButton.classList.remove('hidden');
|
||||||
|
break;
|
||||||
|
case 'hide':
|
||||||
|
widgetButton.classList.add('hidden');
|
||||||
|
break;
|
||||||
|
case 'open':
|
||||||
|
widgetWindow.classList.remove('hidden');
|
||||||
|
widgetButton.classList.add('open');
|
||||||
|
getIframe();
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
widgetWindow.classList.toggle('hidden');
|
||||||
|
widgetButton.classList.toggle('open');
|
||||||
|
getIframe();
|
||||||
|
break;
|
||||||
|
case 'close':
|
||||||
|
widgetWindow.classList.add('hidden');
|
||||||
|
widgetButton.classList.remove('open');
|
||||||
|
break;
|
||||||
|
case 'postUserMessage':
|
||||||
|
getIframe().frame.postUserMessage(args[1]);
|
||||||
|
break;
|
||||||
|
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,
|
||||||
|
...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,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'clearChat':
|
||||||
|
getIframe().frame.clearChat();
|
||||||
|
break;
|
||||||
|
case 'navigateToPage':
|
||||||
|
getIframe().frame.navigateToPage(args[1]);
|
||||||
|
break;
|
||||||
|
case 'navigateToAssistant':
|
||||||
|
getIframe().frame.navigateToAssistant();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error - GitBook is not defined in the global scope
|
||||||
|
const precalls = (window.GitBook as GitBookStandalone | undefined)?.q ?? [];
|
||||||
|
|
||||||
|
// @ts-expect-error - GitBook is not defined in the global scope
|
||||||
|
window.GitBook = GitBook;
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
:root {
|
||||||
|
--gitbook-widget-top: 1rem;
|
||||||
|
--gitbook-widget-bottom: 1rem;
|
||||||
|
--gitbook-widget-right: 1rem;
|
||||||
|
--gitbook-widget-left: 1rem;
|
||||||
|
|
||||||
|
--gitbook-widget-button-height: 46px;
|
||||||
|
|
||||||
|
--gitbook-widget-radius: .5rem;
|
||||||
|
--gitbook-widget-text-size: 1rem;
|
||||||
|
--gitbook-widget-text-color: #656973;
|
||||||
|
--gitbook-widget-border-color: #e5e5e5;
|
||||||
|
|
||||||
|
--gitbook-widget-background-translucent: rgba(255, 255, 255, 0.9);
|
||||||
|
--gitbook-widget-background-translucent-hover: rgba(250, 250, 250, 0.9);
|
||||||
|
--gitbook-widget-background-solid: #FFFFFF;
|
||||||
|
--gitbook-widget-background-solid-hover: #FBFBFB;
|
||||||
|
|
||||||
|
--gitbook-widget-icon-size: 1.25rem;
|
||||||
|
|
||||||
|
--gitbook-widget-window-width: 28rem; /* 448px */
|
||||||
|
--gitbook-widget-window-height: 40rem; /* 640px */
|
||||||
|
--gitbook-widget-window-spacing: .5rem; /* Spacing between the button and the window */
|
||||||
|
--gitbook-widget-window-bottom: calc(var(--gitbook-widget-bottom) + var(--gitbook-widget-button-height) + var(--gitbook-widget-window-spacing));
|
||||||
|
|
||||||
|
--gitbook-widget-transition-duration-fast: 0.2s;
|
||||||
|
--gitbook-widget-transition-duration-slow: 0.5s;
|
||||||
|
--gitbook-widget-easing: cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
#gitbook-widget-button {
|
||||||
|
position: fixed;
|
||||||
|
bottom: var(--gitbook-widget-bottom);
|
||||||
|
right: var(--gitbook-widget-right);
|
||||||
|
height: var(--gitbook-widget-button-height);
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
gap: .5rem;
|
||||||
|
padding: .75rem 1rem;
|
||||||
|
|
||||||
|
border-radius: 100px;
|
||||||
|
border: 1px solid var(--gitbook-widget-border-color);
|
||||||
|
background-color: var(--gitbook-widget-background-translucent);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
|
||||||
|
font-size: var(--gitbook-widget-text-size);
|
||||||
|
color: var(--gitbook-widget-text-color);
|
||||||
|
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.05), 0 1px 2px -1px rgba(0,0,0,0.05);
|
||||||
|
transition: all var(--gitbook-widget-transition-duration-fast) var(--gitbook-widget-easing);
|
||||||
|
|
||||||
|
cursor:pointer;
|
||||||
|
animation: gitbook-widget-present var(--gitbook-widget-transition-duration-slow) var(--gitbook-widget-easing-bounce);
|
||||||
|
}
|
||||||
|
|
||||||
|
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
|
||||||
|
background-color: var(--gitbook-widget-background-translucent-hover);
|
||||||
|
}
|
||||||
|
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
#gitbook-widget-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.05), 0 1px 2px -1px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
#gitbook-widget-button {
|
||||||
|
background-color: var(--gitbook-widget-background-solid);
|
||||||
|
}
|
||||||
|
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
|
||||||
|
background-color: var(--gitbook-widget-background-solid-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#gitbook-widget-button.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gitbook-widget-button.open {
|
||||||
|
padding: .75rem;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button: Icon */
|
||||||
|
#gitbook-widget-button-icon {
|
||||||
|
width: var(--gitbook-widget-icon-size);
|
||||||
|
height: var(--gitbook-widget-icon-size);
|
||||||
|
mask-image: url("https://static-2v.gitbook.com/~gitbook/static/icons/svgs/custom-icons/gitbook-assistant.svg?v=2");
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button: Label */
|
||||||
|
#gitbook-widget-button.open #gitbook-widget-button-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Window */
|
||||||
|
#gitbook-widget-window {
|
||||||
|
position: fixed;
|
||||||
|
bottom: var(--gitbook-widget-window-bottom);
|
||||||
|
right: var(--gitbook-widget-right);
|
||||||
|
z-index: 9998;
|
||||||
|
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-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);
|
||||||
|
overflow: hidden;
|
||||||
|
transition-property: transform, opacity, display;
|
||||||
|
transition-duration: var(--gitbook-widget-transition-duration-slow);
|
||||||
|
transition-timing-function: var(--gitbook-widget-easing-bounce);
|
||||||
|
transform-origin: bottom right;
|
||||||
|
transition-behavior: allow-discrete;
|
||||||
|
}
|
||||||
|
|
||||||
|
@starting-style {
|
||||||
|
#gitbook-widget-window {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:has(#gitbook-widget-button.hidden) #gitbook-widget-window {
|
||||||
|
bottom: var(--gitbook-widget-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
#gitbook-widget-window.hidden {
|
||||||
|
transition-property: transform, opacity, display;
|
||||||
|
transition-duration: var(--gitbook-widget-transition-duration-fast);
|
||||||
|
transition-timing-function: var(--gitbook-widget-easing);
|
||||||
|
transition-behavior: allow-discrete;
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#gitbook-widget-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gitbook-widget-present {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"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 +1 @@
|
|||||||
index.ts
|
dist/
|
||||||
|
|||||||
@@ -0,0 +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
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 5f8a8fe: Initial release
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import emojisRaws from 'emoji-assets/emoji.json';
|
import emojisRaws from 'emoji-assets/emoji.json';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
interface EmojiData {
|
interface EmojiData {
|
||||||
code_points: {
|
code_points: {
|
||||||
@@ -16,11 +17,11 @@ Object.entries(emojis).forEach(([key, value]) => {
|
|||||||
if (emoji && key !== emoji) {
|
if (emoji && key !== emoji) {
|
||||||
output[key] = emoji;
|
output[key] = emoji;
|
||||||
} else if (!emoji) {
|
} else if (!emoji) {
|
||||||
console.log('No emoji for', key);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fs.mkdirSync(path.resolve(__dirname, 'dist'), { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
'index.ts',
|
path.resolve(__dirname, 'dist/index.ts'),
|
||||||
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`,
|
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@gitbook/emoji-codepoints",
|
"name": "@gitbook/emoji-codepoints",
|
||||||
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
||||||
|
"version": "0.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"exports": "./index.ts",
|
"exports": "./dist/index.ts",
|
||||||
"dependencies": {},
|
"sideEffects": false,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"emoji-assets": "^8.0.0"
|
"emoji-assets": "^9.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "bun ./build.ts"
|
"generate": "bun ./build.ts",
|
||||||
|
"clean": "rm -rf ./dist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# @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
|
||||||
|
|
||||||
|
- ada195d: Publish gitbook/expr package to help evaluate user defined expressions.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# `@gitbook/expr`
|
||||||
|
|
||||||
|
Safely evaluate & parse user-defined GitBook expressions.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "@gitbook/expr",
|
||||||
|
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||||
|
"version": "1.3.1",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.15.0",
|
||||||
|
"acorn-loose": "^8.5.2",
|
||||||
|
"acorn-walk": "^8.3.4",
|
||||||
|
"assert-never": "catalog:",
|
||||||
|
"escodegen": "^2.1.0",
|
||||||
|
"eval-estree-expression": "^3.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/strictest": "catalog:",
|
||||||
|
"@tsconfig/node20": "catalog:",
|
||||||
|
"@types/escodegen": "^0.0.10",
|
||||||
|
"@types/estree": "^1.0.6",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
|
"bun-types": "catalog:",
|
||||||
|
"tsdown": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsdown",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"unit": "bun test",
|
||||||
|
"clean": "rm -rf ./dist",
|
||||||
|
"dev": "bun run build -- --watch ./src",
|
||||||
|
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md",
|
||||||
|
"CHANGELOG.md"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/GitbookIO/gitbook"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,932 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
|
||||||
|
import { ExpressionRuntime } from '../runtime';
|
||||||
|
import {
|
||||||
|
SymbolArray,
|
||||||
|
SymbolBoolean,
|
||||||
|
SymbolNumber,
|
||||||
|
SymbolObject,
|
||||||
|
SymbolString,
|
||||||
|
SymbolType,
|
||||||
|
SymbolsTable,
|
||||||
|
} from '../symbols';
|
||||||
|
import {
|
||||||
|
type AutocompleteSuggestions,
|
||||||
|
type AutocompleteSymbolSuggestion,
|
||||||
|
SUPPORTED_BINARY_OPERATORS,
|
||||||
|
SUPPORTED_CONDITIONAL_OPERATORS,
|
||||||
|
SUPPORTED_LOGICAL_OPERATORS,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
describe('autocomplete', () => {
|
||||||
|
const runtime = new ExpressionRuntime();
|
||||||
|
const visitorClaimsHelloArraySymbol = SymbolArray({
|
||||||
|
name: 'hello',
|
||||||
|
description: 'An array of string',
|
||||||
|
items: SymbolString(),
|
||||||
|
});
|
||||||
|
const visitorClaimsKeyPropStringSymbol = SymbolString({ name: 'key' });
|
||||||
|
const symbols = {
|
||||||
|
visitor: SymbolObject({
|
||||||
|
name: 'visitor',
|
||||||
|
properties: {
|
||||||
|
claims: SymbolObject({
|
||||||
|
name: 'claims',
|
||||||
|
description: 'The claims contained in the visitor JWT token',
|
||||||
|
properties: {
|
||||||
|
key: visitorClaimsKeyPropStringSymbol,
|
||||||
|
flags: SymbolObject({
|
||||||
|
name: 'flags',
|
||||||
|
properties: {
|
||||||
|
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
hello: visitorClaimsHelloArraySymbol,
|
||||||
|
role: SymbolString({
|
||||||
|
name: 'role',
|
||||||
|
enum: ['admin', 'editor', 'reader'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const context = new SymbolsTable(symbols);
|
||||||
|
const SCENARIOS: {
|
||||||
|
expressionWithCursor: string;
|
||||||
|
expectedSuggestions: AutocompleteSuggestions;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visit<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolObject({
|
||||||
|
name: 'visitor',
|
||||||
|
properties: {
|
||||||
|
claims: SymbolObject({
|
||||||
|
name: 'claims',
|
||||||
|
description: 'The claims contained in the visitor JWT token',
|
||||||
|
properties: {
|
||||||
|
key: SymbolString({ name: 'key' }),
|
||||||
|
flags: SymbolObject({
|
||||||
|
name: 'flags',
|
||||||
|
properties: {
|
||||||
|
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
hello: SymbolArray({
|
||||||
|
name: 'hello',
|
||||||
|
description: 'An array of string',
|
||||||
|
items: SymbolString(),
|
||||||
|
}),
|
||||||
|
role: SymbolString({
|
||||||
|
name: 'role',
|
||||||
|
enum: ['admin', 'editor', 'reader'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
ref: 'visitor',
|
||||||
|
parentRef: undefined,
|
||||||
|
childrenRefs: ['visitor.claims'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor<cur>',
|
||||||
|
expectedSuggestions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolObject({
|
||||||
|
name: 'claims',
|
||||||
|
description: 'The claims contained in the visitor JWT token',
|
||||||
|
properties: {
|
||||||
|
key: SymbolString({ name: 'key' }),
|
||||||
|
flags: SymbolObject({
|
||||||
|
name: 'flags',
|
||||||
|
properties: {
|
||||||
|
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
hello: SymbolArray({
|
||||||
|
name: 'hello',
|
||||||
|
description: 'An array of string',
|
||||||
|
items: SymbolString(),
|
||||||
|
}),
|
||||||
|
role: SymbolString({
|
||||||
|
name: 'role',
|
||||||
|
enum: ['admin', 'editor', 'reader'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
ref: 'visitor.claims',
|
||||||
|
parentRef: 'visitor',
|
||||||
|
childrenRefs: [
|
||||||
|
'visitor.claims.key',
|
||||||
|
'visitor.claims.flags',
|
||||||
|
'visitor.claims.hello',
|
||||||
|
'visitor.claims.role',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolString({ name: 'key' }),
|
||||||
|
ref: 'visitor.claims.key',
|
||||||
|
parentRef: 'visitor.claims',
|
||||||
|
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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolObject({
|
||||||
|
name: 'flags',
|
||||||
|
properties: {
|
||||||
|
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
ref: 'visitor.claims.flags',
|
||||||
|
parentRef: 'visitor.claims',
|
||||||
|
childrenRefs: [
|
||||||
|
'visitor.claims.flags.FLAG1',
|
||||||
|
'visitor.claims.flags.FLAG2',
|
||||||
|
'visitor.claims.flags.FLAG3',
|
||||||
|
'visitor.claims.flags.FLAG4',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolArray({
|
||||||
|
name: 'hello',
|
||||||
|
description: 'An array of string',
|
||||||
|
items: SymbolString(),
|
||||||
|
}),
|
||||||
|
ref: 'visitor.claims.hello',
|
||||||
|
parentRef: 'visitor.claims',
|
||||||
|
childrenRefs: [
|
||||||
|
'visitor.claims.hello.length',
|
||||||
|
'visitor.claims.hello.at',
|
||||||
|
'visitor.claims.hello.includes',
|
||||||
|
'visitor.claims.hello.some',
|
||||||
|
'visitor.claims.hello.every',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolString({
|
||||||
|
name: 'role',
|
||||||
|
enum: ['admin', 'editor', 'reader'],
|
||||||
|
}),
|
||||||
|
ref: 'visitor.claims.role',
|
||||||
|
parentRef: 'visitor.claims',
|
||||||
|
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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.ke<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolString({ name: 'key' }),
|
||||||
|
ref: 'visitor.claims.key',
|
||||||
|
parentRef: 'visitor.claims',
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolArray({
|
||||||
|
name: 'hello',
|
||||||
|
description: 'An array of string',
|
||||||
|
items: SymbolString(),
|
||||||
|
}),
|
||||||
|
ref: 'visitor.claims.hello',
|
||||||
|
parentRef: 'visitor.claims',
|
||||||
|
childrenRefs: [
|
||||||
|
'visitor.claims.hello.length',
|
||||||
|
'visitor.claims.hello.at',
|
||||||
|
'visitor.claims.hello.includes',
|
||||||
|
'visitor.claims.hello.some',
|
||||||
|
'visitor.claims.hello.every',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.hello.<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolNumber({
|
||||||
|
name: 'length',
|
||||||
|
description: `The length data property of an Array instance represents the number of elements in that array.
|
||||||
|
The value is an unsigned, 32-bit integer that is always numerically greater than the highest index in the array.`,
|
||||||
|
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length',
|
||||||
|
}),
|
||||||
|
ref: 'visitor.claims.hello.length',
|
||||||
|
parentRef: 'visitor.claims.hello',
|
||||||
|
childrenRefs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...visitorClaimsHelloArraySymbol.methods.map<AutocompleteSymbolSuggestion>(
|
||||||
|
(method) => ({
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: method,
|
||||||
|
ref: `visitor.claims.hello.${method.name}`,
|
||||||
|
parentRef: 'visitor.claims.hello',
|
||||||
|
childrenRefs: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.f<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolObject({
|
||||||
|
name: 'flags',
|
||||||
|
properties: {
|
||||||
|
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
ref: 'visitor.claims.flags',
|
||||||
|
parentRef: 'visitor.claims',
|
||||||
|
childrenRefs: [
|
||||||
|
'visitor.claims.flags.FLAG1',
|
||||||
|
'visitor.claims.flags.FLAG2',
|
||||||
|
'visitor.claims.flags.FLAG3',
|
||||||
|
'visitor.claims.flags.FLAG4',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.fl<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolObject({
|
||||||
|
name: 'flags',
|
||||||
|
properties: {
|
||||||
|
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
ref: 'visitor.claims.flags',
|
||||||
|
parentRef: 'visitor.claims',
|
||||||
|
childrenRefs: [
|
||||||
|
'visitor.claims.flags.FLAG1',
|
||||||
|
'visitor.claims.flags.FLAG2',
|
||||||
|
'visitor.claims.flags.FLAG3',
|
||||||
|
'visitor.claims.flags.FLAG4',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
ref: 'visitor.claims.flags.FLAG1',
|
||||||
|
parentRef: 'visitor.claims.flags',
|
||||||
|
childrenRefs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
ref: 'visitor.claims.flags.FLAG2',
|
||||||
|
parentRef: 'visitor.claims.flags',
|
||||||
|
childrenRefs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
ref: 'visitor.claims.flags.FLAG3',
|
||||||
|
parentRef: 'visitor.claims.flags',
|
||||||
|
childrenRefs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
ref: 'visitor.claims.flags.FLAG4',
|
||||||
|
parentRef: 'visitor.claims.flags',
|
||||||
|
childrenRefs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FL<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
ref: 'visitor.claims.flags.FLAG1',
|
||||||
|
parentRef: 'visitor.claims.flags',
|
||||||
|
childrenRefs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
ref: 'visitor.claims.flags.FLAG2',
|
||||||
|
parentRef: 'visitor.claims.flags',
|
||||||
|
childrenRefs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
ref: 'visitor.claims.flags.FLAG3',
|
||||||
|
parentRef: 'visitor.claims.flags',
|
||||||
|
childrenRefs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
ref: 'visitor.claims.flags.FLAG4',
|
||||||
|
parentRef: 'visitor.claims.flags',
|
||||||
|
childrenRefs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1<cur>',
|
||||||
|
expectedSuggestions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.key <cur>',
|
||||||
|
expectedSuggestions: [...SUPPORTED_BINARY_OPERATORS].map((op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 <cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
...[...SUPPORTED_BINARY_OPERATORS, ...SUPPORTED_LOGICAL_OPERATORS].filter((op) =>
|
||||||
|
['==', '!=', '===', '!==', '&&', '||'].includes(op.operator)
|
||||||
|
),
|
||||||
|
...SUPPORTED_CONDITIONAL_OPERATORS,
|
||||||
|
].map((op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 =<cur>',
|
||||||
|
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter((op) =>
|
||||||
|
['==', '==='].includes(op.operator)
|
||||||
|
).map((op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 ==<cur>',
|
||||||
|
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter(
|
||||||
|
(op) => op.operator === '==='
|
||||||
|
).map((op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 !<cur>',
|
||||||
|
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter((op) =>
|
||||||
|
['!=', '!=='].includes(op.operator)
|
||||||
|
).map((op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 !=<cur>',
|
||||||
|
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter(
|
||||||
|
(op) => op.operator === '!=='
|
||||||
|
).map((op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 == <cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 == t<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 == tr<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 == true<cur>',
|
||||||
|
expectedSuggestions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 == f<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 == fa<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 == non<cur>',
|
||||||
|
expectedSuggestions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 == false<cur>',
|
||||||
|
expectedSuggestions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.role == <cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.String, data: 'reader' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.role == ad<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.role == admin<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.role == "ad<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.role == "admin<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.role == "admin"<cur>',
|
||||||
|
expectedSuggestions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.role == edit<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.role == "edit<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.role == editor<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.role == "editor"<cur>',
|
||||||
|
expectedSuggestions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.hello == <cur>',
|
||||||
|
expectedSuggestions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.hello[1] == <cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: {
|
||||||
|
kind: 'in-array',
|
||||||
|
srcSymbol: visitorClaimsHelloArraySymbol,
|
||||||
|
matchedLiteralString: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.hello[1] == "test<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: {
|
||||||
|
kind: 'in-array',
|
||||||
|
srcSymbol: visitorClaimsHelloArraySymbol,
|
||||||
|
matchedLiteralString: '"test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 == true <cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
...SUPPORTED_LOGICAL_OPERATORS.filter((op) => ['&&', '||'].includes(op.operator)),
|
||||||
|
...SUPPORTED_CONDITIONAL_OPERATORS,
|
||||||
|
].map((op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 !== true && v<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolObject({
|
||||||
|
name: 'visitor',
|
||||||
|
properties: {
|
||||||
|
claims: SymbolObject({
|
||||||
|
name: 'claims',
|
||||||
|
description: 'The claims contained in the visitor JWT token',
|
||||||
|
properties: {
|
||||||
|
key: SymbolString({ name: 'key' }),
|
||||||
|
flags: SymbolObject({
|
||||||
|
name: 'flags',
|
||||||
|
properties: {
|
||||||
|
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
hello: SymbolArray({
|
||||||
|
name: 'hello',
|
||||||
|
description: 'An array of string',
|
||||||
|
items: SymbolString(),
|
||||||
|
}),
|
||||||
|
role: SymbolString({
|
||||||
|
name: 'role',
|
||||||
|
enum: ['admin', 'editor', 'reader'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
ref: 'visitor',
|
||||||
|
parentRef: undefined,
|
||||||
|
childrenRefs: ['visitor.claims'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 ? <cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolObject({
|
||||||
|
name: 'visitor',
|
||||||
|
properties: {
|
||||||
|
claims: SymbolObject({
|
||||||
|
name: 'claims',
|
||||||
|
description: 'The claims contained in the visitor JWT token',
|
||||||
|
properties: {
|
||||||
|
key: SymbolString({ name: 'key' }),
|
||||||
|
flags: SymbolObject({
|
||||||
|
name: 'flags',
|
||||||
|
properties: {
|
||||||
|
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
hello: SymbolArray({
|
||||||
|
name: 'hello',
|
||||||
|
description: 'An array of string',
|
||||||
|
items: SymbolString(),
|
||||||
|
}),
|
||||||
|
role: SymbolString({
|
||||||
|
name: 'role',
|
||||||
|
enum: ['admin', 'editor', 'reader'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
ref: 'visitor',
|
||||||
|
parentRef: undefined,
|
||||||
|
childrenRefs: ['visitor.claims'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 ? visit<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolObject({
|
||||||
|
name: 'visitor',
|
||||||
|
properties: {
|
||||||
|
claims: SymbolObject({
|
||||||
|
name: 'claims',
|
||||||
|
description: 'The claims contained in the visitor JWT token',
|
||||||
|
properties: {
|
||||||
|
key: SymbolString({ name: 'key' }),
|
||||||
|
flags: SymbolObject({
|
||||||
|
name: 'flags',
|
||||||
|
properties: {
|
||||||
|
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
hello: SymbolArray({
|
||||||
|
name: 'hello',
|
||||||
|
description: 'An array of string',
|
||||||
|
items: SymbolString(),
|
||||||
|
}),
|
||||||
|
role: SymbolString({
|
||||||
|
name: 'role',
|
||||||
|
enum: ['admin', 'editor', 'reader'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
ref: 'visitor',
|
||||||
|
parentRef: undefined,
|
||||||
|
childrenRefs: ['visitor.claims'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expressionWithCursor: 'visitor.claims.flags.FLAG1 ? visitor.claims.fl<cur>',
|
||||||
|
expectedSuggestions: [
|
||||||
|
{
|
||||||
|
type: 'symbol',
|
||||||
|
symbol: {
|
||||||
|
definition: SymbolObject({
|
||||||
|
name: 'flags',
|
||||||
|
properties: {
|
||||||
|
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||||
|
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||||
|
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||||
|
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||||
|
},
|
||||||
|
methods: [],
|
||||||
|
}),
|
||||||
|
ref: 'visitor.claims.flags',
|
||||||
|
parentRef: 'visitor.claims',
|
||||||
|
childrenRefs: [
|
||||||
|
'visitor.claims.flags.FLAG1',
|
||||||
|
'visitor.claims.flags.FLAG2',
|
||||||
|
'visitor.claims.flags.FLAG3',
|
||||||
|
'visitor.claims.flags.FLAG4',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
it.each(SCENARIOS)(
|
||||||
|
'should provide matching suggestion for expression with cursor: $expressionWithCursor',
|
||||||
|
({ expressionWithCursor, expectedSuggestions }) => {
|
||||||
|
const { expression, cursorOffset } = extractCursorPosition(
|
||||||
|
expressionWithCursor,
|
||||||
|
'<cur>'
|
||||||
|
);
|
||||||
|
const { suggestions } = runtime.autocomplete(expression, cursorOffset, context);
|
||||||
|
expect(suggestions).toStrictEqual(expectedSuggestions);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function extractCursorPosition(
|
||||||
|
expressionWithCursor: string,
|
||||||
|
cursorPlaceholder: string
|
||||||
|
): { expression: string; cursorOffset: number } {
|
||||||
|
const cursorOffset = expressionWithCursor.indexOf(cursorPlaceholder);
|
||||||
|
if (cursorOffset === -1) {
|
||||||
|
throw new Error(
|
||||||
|
`Cursor position (${cursorPlaceholder}) not found in the expression string.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expression = expressionWithCursor.replace(cursorPlaceholder, '');
|
||||||
|
return { expression, cursorOffset };
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
|
||||||
|
import { inferDefaultInputValuesFromObjectJSONSchema } from '../input-values';
|
||||||
|
|
||||||
|
describe('inferDefaultInputValuesFromObjectJSONSchema', () => {
|
||||||
|
it('should infer properly the default input value based on the JSON schema of an object', () => {
|
||||||
|
const defaultInputValues = inferDefaultInputValuesFromObjectJSONSchema({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
claims: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
key: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
FLAG1: { type: 'string' },
|
||||||
|
FLAG2: { type: 'string' },
|
||||||
|
FLAG3: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isAlphaUser: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
hello: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['enumValue1', 'enumValue2', 'enumValue3'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(defaultInputValues).toMatchObject({
|
||||||
|
claims: {
|
||||||
|
key: 'default',
|
||||||
|
flags: {
|
||||||
|
FLAG1: 'default',
|
||||||
|
FLAG2: 'default',
|
||||||
|
FLAG3: 'default',
|
||||||
|
},
|
||||||
|
isAlphaUser: true,
|
||||||
|
hello: 'enumValue1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
|
||||||
|
import { ExpressionError } from '../errors';
|
||||||
|
import { ExpressionRuntime } from '../runtime';
|
||||||
|
import type { Logger } from '../types';
|
||||||
|
|
||||||
|
const SILENT_LOGGER: Logger = {
|
||||||
|
debug: () => {},
|
||||||
|
info: () => {},
|
||||||
|
error: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ExpressionRuntime', () => {
|
||||||
|
const runtime = new ExpressionRuntime(SILENT_LOGGER);
|
||||||
|
|
||||||
|
describe('evaluate', () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
scenario: 'simple condition',
|
||||||
|
condition: 'isBetaUser === true',
|
||||||
|
inputs: { isBetaUser: false },
|
||||||
|
expectedResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: 'simple condition with multiple inputs variables',
|
||||||
|
condition: 'useProductA && !isBetaUser',
|
||||||
|
inputs: {
|
||||||
|
useProductA: true,
|
||||||
|
isBetaUser: false,
|
||||||
|
},
|
||||||
|
expectedResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: 'condition with objects in inputs variables',
|
||||||
|
condition: 'products.includes("productA") && userSegments.alpha',
|
||||||
|
inputs: {
|
||||||
|
products: ['productA', 'productB'],
|
||||||
|
userSegments: {
|
||||||
|
alpha: true,
|
||||||
|
beta: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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)).toEqual(expectedResult);
|
||||||
|
expect(runtime.safeEvaluate(condition, inputs).value).toEqual(expectedResult);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const INVALID_EXPRESSSIONS = [
|
||||||
|
{
|
||||||
|
scenario: 'invalid syntax',
|
||||||
|
condition: 't}=d',
|
||||||
|
inputs: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: 'non conditional expression',
|
||||||
|
condition: 'const a = 1;',
|
||||||
|
inputs: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: 'unsafe expression',
|
||||||
|
condition: 'while (1) {}',
|
||||||
|
inputs: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: 'unsafe expression',
|
||||||
|
condition: '[1, 2, 3].map(() => { while (1) {}})',
|
||||||
|
inputs: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(INVALID_EXPRESSSIONS)(
|
||||||
|
'should return an object with the error for non conditional expression or syntax errors when using safeEvaluate is on (default): $scenario',
|
||||||
|
({ condition, inputs }) => {
|
||||||
|
const result = runtime.safeEvaluate(condition, inputs);
|
||||||
|
expect(result.value).toBeUndefined();
|
||||||
|
expect(result.error instanceof ExpressionError).toBe(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(INVALID_EXPRESSSIONS)(
|
||||||
|
'should throw an error when using evaluate with invalid expressions',
|
||||||
|
({ condition, inputs }) => {
|
||||||
|
expect(() => runtime.evaluate(condition, inputs)).toThrowError(ExpressionError);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parse', () => {
|
||||||
|
it('should produce a valid ESTree compatible AST node for conditional expressions', () => {
|
||||||
|
const ast = runtime.parse('isBetaUser === true');
|
||||||
|
|
||||||
|
expect(ast.result).toEqual({
|
||||||
|
type: 'BinaryExpression',
|
||||||
|
start: 0,
|
||||||
|
end: 19,
|
||||||
|
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 19 } },
|
||||||
|
left: {
|
||||||
|
type: 'Identifier',
|
||||||
|
start: 0,
|
||||||
|
end: 10,
|
||||||
|
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
|
||||||
|
name: 'isBetaUser',
|
||||||
|
},
|
||||||
|
operator: '===',
|
||||||
|
right: {
|
||||||
|
type: 'Literal',
|
||||||
|
start: 15,
|
||||||
|
end: 19,
|
||||||
|
loc: { start: { line: 1, column: 15 }, end: { line: 1, column: 19 } },
|
||||||
|
value: true,
|
||||||
|
raw: 'true',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
scenario: 'invalid syntax',
|
||||||
|
condition: 't}=d',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: 'non conditional expression',
|
||||||
|
condition: 'const a = 1;',
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
'should throw an error for non conditional expressions or syntax errors: $scenario',
|
||||||
|
({ condition }) => {
|
||||||
|
expect(() => runtime.parse(condition)).toThrowError(ExpressionError);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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([
|
||||||
|
{
|
||||||
|
scenario: 'simple condition',
|
||||||
|
condition: 'isBetaUser === true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: 'simple condition with multiple inputs variables',
|
||||||
|
condition: 'useProductA && !isBetaUser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: 'condition with objects in inputs variables',
|
||||||
|
condition: 'products.includes("productA") && userSegments.alpha',
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
'should produce the original expression using an AST node produced by parse: $scenario',
|
||||||
|
({ condition }) => {
|
||||||
|
const { result } = runtime.parse(condition);
|
||||||
|
expect(runtime.generate(result)).toStrictEqual(condition);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
|
||||||
|
import { ExpressionRuntime, parseTemplate } from '../';
|
||||||
|
|
||||||
|
describe('template expressions', () => {
|
||||||
|
it('should parse template into parts', () => {
|
||||||
|
const parts = parseTemplate('Hello {{ user.name }}!');
|
||||||
|
expect(parts).toEqual([
|
||||||
|
{ type: 'text', value: 'Hello ', start: 0, end: 6 },
|
||||||
|
{ type: 'expression', value: 'user.name', start: 8, end: 19 },
|
||||||
|
{ type: 'text', value: '!', start: 21, end: 22 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse template starting with an expression', () => {
|
||||||
|
const parts = parseTemplate('{{ user.name }} is cool');
|
||||||
|
expect(parts).toEqual([
|
||||||
|
{ type: 'expression', value: 'user.name', start: 2, end: 13 },
|
||||||
|
{ type: 'text', value: ' is cool', start: 15, end: 23 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse template without expressions', () => {
|
||||||
|
const parts = parseTemplate('Hello world');
|
||||||
|
expect(parts).toEqual([{ type: 'text', value: 'Hello world', start: 0, end: 11 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should evaluate template', () => {
|
||||||
|
const runtime = new ExpressionRuntime();
|
||||||
|
const result = runtime.evaluateTemplate('Hello {{ user.name }}!', {
|
||||||
|
user: { name: 'John' },
|
||||||
|
});
|
||||||
|
expect(result).toBe('Hello John!');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,567 @@
|
|||||||
|
import type {
|
||||||
|
Node as AcornNode,
|
||||||
|
AnyNode,
|
||||||
|
BinaryExpression,
|
||||||
|
Expression,
|
||||||
|
Identifier,
|
||||||
|
Literal,
|
||||||
|
MemberExpression,
|
||||||
|
PrivateIdentifier,
|
||||||
|
Super,
|
||||||
|
} 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';
|
||||||
|
import {
|
||||||
|
type AutocompleteLiteralValueSuggestion,
|
||||||
|
type AutocompleteOperatorSuggestion,
|
||||||
|
type AutocompleteSuggestions,
|
||||||
|
type AutocompleteSymbolSuggestion,
|
||||||
|
type DirectLiteralValueSuggestion,
|
||||||
|
type ExpressionParserResult,
|
||||||
|
type Logger,
|
||||||
|
SUPPORTED_BINARY_OPERATORS,
|
||||||
|
SUPPORTED_CONDITIONAL_OPERATORS,
|
||||||
|
SUPPORTED_LOGICAL_OPERATORS,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
interface ExpressionParser {
|
||||||
|
parse(expr: string, options: { loose?: boolean }): ExpressionParserResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AutoComplete {
|
||||||
|
#parser: ExpressionParser;
|
||||||
|
#logger: Logger;
|
||||||
|
|
||||||
|
constructor(parser: ExpressionParser, logger: Logger = console) {
|
||||||
|
this.#parser = parser;
|
||||||
|
this.#logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates autocomplete suggestions based on the input expression and cursor offset position.
|
||||||
|
*/
|
||||||
|
public getSuggestions(
|
||||||
|
expr: string,
|
||||||
|
cursorOffset: number,
|
||||||
|
context: SymbolsTable
|
||||||
|
): AutocompleteSuggestions {
|
||||||
|
if (!expr.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { result, invalidNodes } = this.#parser.parse(expr, { loose: true });
|
||||||
|
|
||||||
|
// Locate the node at the cursor position.
|
||||||
|
const nodeAtCursorFound = walk.findNodeAround(result, cursorOffset, (_type, node) =>
|
||||||
|
isNodeAtCursor(node, cursorOffset)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!nodeAtCursorFound) {
|
||||||
|
// When we can't find one we might be in the boundary of the program/expression.
|
||||||
|
// We could possibly be in a whitespace at the end of the expression or in a situation where the parsed
|
||||||
|
// tree may contain 2 top level ExpressionStatement (second one returned as invalid node by the parser).
|
||||||
|
//
|
||||||
|
// In this case we want to provide operators as suggestions and refine the search of the "cursor" node to either:
|
||||||
|
// - using the end position of the whole expression AST (e.g white space at the very end of the expression string)
|
||||||
|
// - or using the second top level ExpressionStatement node found by the parser as the cursor is at the end of that node.
|
||||||
|
if (cursorOffset > result.end) {
|
||||||
|
const ast = invalidNodes.length > 0 ? invalidNodes[0]?.expression : result;
|
||||||
|
|
||||||
|
if (!ast) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastNodeFound = walk.findNodeAround(ast, ast.end, (_type, node) =>
|
||||||
|
isNodeAtCursor(node, ast.end)
|
||||||
|
);
|
||||||
|
if (!lastNodeFound) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const { node } = lastNodeFound;
|
||||||
|
|
||||||
|
if (!isAnyNode(node)) {
|
||||||
|
throw Error(`Unexpected node type ${node.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getOperatorSuggestionsForNode(ast, node, result.end, context);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { node } = nodeAtCursorFound;
|
||||||
|
|
||||||
|
if (!isAnyNode(node)) {
|
||||||
|
throw Error(`Unexpected node type ${node.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getSuggestionsForNode(result, node, expr, cursorOffset, context);
|
||||||
|
} catch (error) {
|
||||||
|
this.#logger.error('Error while computing autocomplete suggestions', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete suggestions for a specific node in the AST.
|
||||||
|
*/
|
||||||
|
private getSuggestionsForNode(
|
||||||
|
ast: Expression,
|
||||||
|
node: AnyNode,
|
||||||
|
expr: string,
|
||||||
|
cursorOffset: number,
|
||||||
|
context: SymbolsTable
|
||||||
|
): AutocompleteSuggestions {
|
||||||
|
// When the node is an identifier look up the parent to get more context for the suggestions.
|
||||||
|
let inferNode: AnyNode = node;
|
||||||
|
if (node.type === 'Identifier' || node.type === 'Literal') {
|
||||||
|
const parent = findParentNode(node, ast);
|
||||||
|
inferNode =
|
||||||
|
parent &&
|
||||||
|
!['ExpressionStatement', 'LogicalExpression', 'ConditionalExpression'].includes(
|
||||||
|
parent.type
|
||||||
|
)
|
||||||
|
? parent
|
||||||
|
: node;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (inferNode.type) {
|
||||||
|
case 'Identifier':
|
||||||
|
case 'MemberExpression': {
|
||||||
|
const pathParts = this.getSymbolsPathPartsForMemberExpressionNode(
|
||||||
|
inferNode,
|
||||||
|
cursorOffset
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch suggestions from the symbol table
|
||||||
|
const candidatesKeys = context.getMatchingSymbolsKeys(pathParts);
|
||||||
|
|
||||||
|
const suggestions: AutocompleteSymbolSuggestion[] = [];
|
||||||
|
for (const candidate of candidatesKeys) {
|
||||||
|
const symbolInfo = context.getSymbolInfo(candidate);
|
||||||
|
if (symbolInfo) {
|
||||||
|
suggestions.push({ type: 'symbol', symbol: symbolInfo });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suggestions.length === 1) {
|
||||||
|
const lastPathSegment = pathParts.at(-1)?.replace(/\*$/, '');
|
||||||
|
|
||||||
|
// Return no suggestion when the only match is an exact match of the
|
||||||
|
// typed token.
|
||||||
|
return lastPathSegment !== suggestions[0]?.symbol.definition.name
|
||||||
|
? suggestions
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
case 'AssignmentExpression':
|
||||||
|
case 'UnaryExpression': {
|
||||||
|
// Provide suggestions for binary or logical operators based on the parsed operator
|
||||||
|
// of the partial expression (e.g suggest "==" when typing "=" (parsed as AssignmentExpression)).
|
||||||
|
return this.getOperatorSuggestionsForNode(ast, inferNode, cursorOffset, context);
|
||||||
|
}
|
||||||
|
case 'BinaryExpression': {
|
||||||
|
const { left, right } = inferNode;
|
||||||
|
|
||||||
|
const isOperatorBinaryOp = SUPPORTED_BINARY_OPERATORS.some(
|
||||||
|
(op) => op.operator === inferNode.operator
|
||||||
|
);
|
||||||
|
|
||||||
|
const operatorIndex = expr.indexOf(inferNode.operator, left.end);
|
||||||
|
const operatorOffset = operatorIndex + inferNode.operator.length;
|
||||||
|
const isCursorAfterOperator = cursorOffset > operatorOffset;
|
||||||
|
|
||||||
|
const shouldSuggestValues =
|
||||||
|
isCursorAfterOperator &&
|
||||||
|
isOperatorBinaryOp &&
|
||||||
|
isNodeAtCursor(right, cursorOffset);
|
||||||
|
|
||||||
|
if (shouldSuggestValues) {
|
||||||
|
return this.getLiteralValueSuggestionsForNode(inferNode, cursorOffset, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide suggestions for binary operators based on the parsed operator
|
||||||
|
return this.getOperatorSuggestionsForNode(ast, inferNode, cursorOffset, context);
|
||||||
|
}
|
||||||
|
case 'ConditionalExpression': {
|
||||||
|
const { consequent, alternate } = inferNode;
|
||||||
|
|
||||||
|
if (isNodeAtCursor(consequent, cursorOffset)) {
|
||||||
|
return this.getSuggestionsForNode(ast, consequent, expr, cursorOffset, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNodeAtCursor(alternate, cursorOffset)) {
|
||||||
|
return this.getSuggestionsForNode(ast, alternate, expr, cursorOffset, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a path corresponding to the MemberExpression node that can be used to lookup matching symbols in the symbol table.
|
||||||
|
*/
|
||||||
|
private getSymbolsPathPartsForMemberExpressionNode(
|
||||||
|
node: MemberExpression | Identifier,
|
||||||
|
cursorOffset: number,
|
||||||
|
options?: { withWildcardMatches: boolean }
|
||||||
|
): string[] {
|
||||||
|
const withWildcardMatches = options?.withWildcardMatches ?? true;
|
||||||
|
const pathParts: string[] = [];
|
||||||
|
|
||||||
|
switch (node.type) {
|
||||||
|
case 'MemberExpression':
|
||||||
|
{
|
||||||
|
const memberProperty = node.property;
|
||||||
|
|
||||||
|
// Only support identifier or literal expressions as member properties.
|
||||||
|
if (!isSupportedMemberProperty(memberProperty)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the path part corresponding to the member property (e.g b in a.b or a['b'])
|
||||||
|
const propertyPathPart = this.getSymbolsPathPartFromMemberProperty(
|
||||||
|
memberProperty,
|
||||||
|
cursorOffset
|
||||||
|
);
|
||||||
|
|
||||||
|
if (propertyPathPart) {
|
||||||
|
pathParts.push(
|
||||||
|
withWildcardMatches ? `${propertyPathPart}*` : propertyPathPart
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go through the parent(s) in the chain and add their path parts as well
|
||||||
|
let parent: Expression | Super | undefined = node.object;
|
||||||
|
while (parent) {
|
||||||
|
const parentProperty = 'property' in parent ? parent.property : parent;
|
||||||
|
|
||||||
|
// Only support identifier or literal expressions as parent property.
|
||||||
|
const parentPropertyPathPart = isSupportedMemberProperty(parentProperty)
|
||||||
|
? this.getSymbolsPathPartFromMemberProperty(
|
||||||
|
parentProperty,
|
||||||
|
cursorOffset
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (parentPropertyPathPart) {
|
||||||
|
pathParts.unshift(parentPropertyPathPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = 'object' in parent ? parent.object : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Identifier': {
|
||||||
|
const propertyPathPart = this.getSymbolsPathPartFromMemberProperty(
|
||||||
|
node,
|
||||||
|
cursorOffset
|
||||||
|
);
|
||||||
|
if (propertyPathPart) {
|
||||||
|
pathParts.push(withWildcardMatches ? `${propertyPathPart}*` : propertyPathPart);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
assertNever(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathParts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a part of a symbol path corresponding to a MemberExpression node property.
|
||||||
|
*/
|
||||||
|
private getSymbolsPathPartFromMemberProperty(
|
||||||
|
node: Identifier | Literal,
|
||||||
|
cursorOffset: number
|
||||||
|
): string {
|
||||||
|
switch (node.type) {
|
||||||
|
case 'Identifier': {
|
||||||
|
if (isDummy(node)) {
|
||||||
|
return '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
return isNodeAtCursor(node, cursorOffset)
|
||||||
|
? node.name.slice(0, cursorOffset)
|
||||||
|
: node.name;
|
||||||
|
}
|
||||||
|
case 'Literal': {
|
||||||
|
if (isDummy(node) || !node.value) {
|
||||||
|
return '*';
|
||||||
|
}
|
||||||
|
const value = String(node.value);
|
||||||
|
return isNodeAtCursor(node, cursorOffset) ? value.slice(0, cursorOffset) : value;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
assertNever(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete literal value suggestions for a specific node in the AST.
|
||||||
|
*/
|
||||||
|
private getLiteralValueSuggestionsForNode(
|
||||||
|
node: BinaryExpression,
|
||||||
|
cursorOffset: number,
|
||||||
|
context: SymbolsTable
|
||||||
|
): AutocompleteLiteralValueSuggestion[] {
|
||||||
|
const { left, right } = node;
|
||||||
|
|
||||||
|
if (left.type !== 'MemberExpression' && left.type !== 'Identifier') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right.type !== 'Identifier' && right.type !== 'Literal') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftSymbolPath = this.getSymbolsPathPartsForMemberExpressionNode(left, cursorOffset, {
|
||||||
|
withWildcardMatches: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLeftComputedMember = left.type === 'MemberExpression' && left.computed;
|
||||||
|
const leftSymbolInfo = context.getSymbolInfo(
|
||||||
|
isLeftComputedMember ? leftSymbolPath.slice(0, -1) : leftSymbolPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!leftSymbolInfo) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (leftSymbolInfo.definition.type) {
|
||||||
|
case SymbolType.Boolean: {
|
||||||
|
const suggestions: DirectLiteralValueSuggestion[] = [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return isDummy(right)
|
||||||
|
? suggestions
|
||||||
|
: suggestions.filter((literalValue) => {
|
||||||
|
const literalValueString = String(literalValue.value.data);
|
||||||
|
const rightNodeValue =
|
||||||
|
right.type === 'Identifier' ? right.name : (right.raw ?? '');
|
||||||
|
return (
|
||||||
|
literalValueString !== rightNodeValue &&
|
||||||
|
literalValueString.startsWith(rightNodeValue)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case SymbolType.String: {
|
||||||
|
if (!leftSymbolInfo.definition.enum) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightNodeValue = (() => {
|
||||||
|
if (right.type === 'Identifier') {
|
||||||
|
return right.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeValue = right.raw ?? '';
|
||||||
|
if (/^(['"])(.*)\1$/.test(nodeValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeValue.replaceAll(/["']/g, '');
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (rightNodeValue === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestions = isDummy(right)
|
||||||
|
? leftSymbolInfo.definition.enum
|
||||||
|
: leftSymbolInfo.definition.enum.filter((enumValue) =>
|
||||||
|
enumValue.startsWith(rightNodeValue)
|
||||||
|
);
|
||||||
|
|
||||||
|
return suggestions.map((value) => ({
|
||||||
|
type: 'literal-value',
|
||||||
|
value: {
|
||||||
|
kind: 'direct',
|
||||||
|
type: SymbolType.String,
|
||||||
|
data: value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
case SymbolType.Array: {
|
||||||
|
// Only return a literal in array value suggestion when the left hand side of the binary expression
|
||||||
|
// is computed, e.g myArray[1]
|
||||||
|
return isLeftComputedMember
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: 'literal-value',
|
||||||
|
value: {
|
||||||
|
kind: 'in-array',
|
||||||
|
srcSymbol: leftSymbolInfo.definition,
|
||||||
|
matchedLiteralString: !isDummy(right)
|
||||||
|
? right.type === 'Identifier'
|
||||||
|
? right.name
|
||||||
|
: (right.raw ?? '')
|
||||||
|
: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete operator suggestions for a specific node in the AST.
|
||||||
|
*/
|
||||||
|
private getOperatorSuggestionsForNode(
|
||||||
|
ast: Expression,
|
||||||
|
node: AnyNode,
|
||||||
|
cursorOffset: number,
|
||||||
|
context: SymbolsTable
|
||||||
|
): AutocompleteOperatorSuggestion[] {
|
||||||
|
if (node.type === 'Literal') {
|
||||||
|
const parent = findParentNode(node, ast);
|
||||||
|
|
||||||
|
if (parent?.type === 'BinaryExpression') {
|
||||||
|
return [...SUPPORTED_LOGICAL_OPERATORS, ...SUPPORTED_CONDITIONAL_OPERATORS].map(
|
||||||
|
(op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const literalSymbol = SymbolsTable.inferSymbolFromValue(node.raw);
|
||||||
|
return this.getOperatorSuggestionsForSymbol(literalSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the node is an identifier look the parent to get more context for the suggestions
|
||||||
|
let inferNode: AnyNode = node;
|
||||||
|
if (node.type === 'Identifier') {
|
||||||
|
const parent = findParentNode(node, ast);
|
||||||
|
inferNode = parent && parent.type !== 'ExpressionStatement' ? parent : node;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (inferNode.type) {
|
||||||
|
case 'MemberExpression': {
|
||||||
|
const pathParts = this.getSymbolsPathPartsForMemberExpressionNode(
|
||||||
|
inferNode,
|
||||||
|
cursorOffset,
|
||||||
|
{
|
||||||
|
withWildcardMatches: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const symbolInfo = context.getSymbolInfo(pathParts);
|
||||||
|
return symbolInfo
|
||||||
|
? this.getOperatorSuggestionsForSymbol(symbolInfo.definition)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
case 'AssignmentExpression':
|
||||||
|
case 'BinaryExpression':
|
||||||
|
case 'UnaryExpression': {
|
||||||
|
// Starting to write a binary/logical operator so suggest operator matching the already
|
||||||
|
// typed character as operator.
|
||||||
|
const operator = inferNode.operator;
|
||||||
|
return (
|
||||||
|
[...SUPPORTED_BINARY_OPERATORS, ...SUPPORTED_LOGICAL_OPERATORS]
|
||||||
|
.filter((op) => op.operator.startsWith(operator))
|
||||||
|
// No need to include the operator that match exactly
|
||||||
|
.filter((op) => op.operator !== operator)
|
||||||
|
.map((op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete operator suggestions based on the type of a symbol.
|
||||||
|
*/
|
||||||
|
private getOperatorSuggestionsForSymbol(
|
||||||
|
symbol: ExtractSymbolDef<SymbolType>
|
||||||
|
): AutocompleteOperatorSuggestion[] {
|
||||||
|
switch (symbol.type) {
|
||||||
|
case SymbolType.Number:
|
||||||
|
case SymbolType.Boolean:
|
||||||
|
case SymbolType.Null:
|
||||||
|
case SymbolType.Undefined:
|
||||||
|
case SymbolType.Object:
|
||||||
|
case SymbolType.Array: {
|
||||||
|
const equalityOps = SUPPORTED_BINARY_OPERATORS.slice(0, 4);
|
||||||
|
const finalSuggestions =
|
||||||
|
symbol.type === SymbolType.Boolean
|
||||||
|
? [
|
||||||
|
...equalityOps,
|
||||||
|
...SUPPORTED_LOGICAL_OPERATORS,
|
||||||
|
...SUPPORTED_CONDITIONAL_OPERATORS,
|
||||||
|
]
|
||||||
|
: equalityOps;
|
||||||
|
return finalSuggestions.map((op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
case SymbolType.String:
|
||||||
|
return SUPPORTED_BINARY_OPERATORS.map((op) => ({
|
||||||
|
type: 'operator',
|
||||||
|
...op,
|
||||||
|
}));
|
||||||
|
case SymbolType.Function:
|
||||||
|
return this.getOperatorSuggestionsForSymbol(symbol.returns);
|
||||||
|
case SymbolType.Union: {
|
||||||
|
return symbol.members.reduce<AutocompleteOperatorSuggestion[]>((prev, cur) => {
|
||||||
|
prev.push(...this.getOperatorSuggestionsForSymbol(cur));
|
||||||
|
return prev;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
assertNever(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the parent of child node in the provided AST.
|
||||||
|
*/
|
||||||
|
function findParentNode(child: AnyNode, ast: Expression): AnyNode | undefined {
|
||||||
|
let foundParent: AnyNode | undefined;
|
||||||
|
|
||||||
|
walk.ancestor(ast, {
|
||||||
|
[child.type]: (node: AcornNode, _state: undefined, ancestors: AnyNode[]) => {
|
||||||
|
if (node.start === child.start && node.end === child.end) {
|
||||||
|
// The parent is the second last ancestor in the stack (last one is the actual node)
|
||||||
|
foundParent = ancestors[ancestors.length - 2];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return foundParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSupportedMemberProperty(node: Expression | PrivateIdentifier | Super) {
|
||||||
|
return node.type === 'Identifier' || node.type === 'Literal';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNodeAtCursor(node: AcornNode, cursorOffset: number) {
|
||||||
|
return cursorOffset >= node.start && cursorOffset <= node.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAnyNode(node: AcornNode): node is AnyNode {
|
||||||
|
return 'type' in node;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Position, Token } from 'acorn';
|
||||||
|
|
||||||
|
export class ExpressionError extends Error {
|
||||||
|
/**
|
||||||
|
* The location of the error in the parsed expression.
|
||||||
|
*/
|
||||||
|
public location?: Position | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expression token with the error
|
||||||
|
*/
|
||||||
|
public token?: Token;
|
||||||
|
|
||||||
|
constructor(message: string, loc?: Position | null, token?: Token) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, ExpressionError);
|
||||||
|
}
|
||||||
|
this.name = 'ExpressionError';
|
||||||
|
this.location = loc;
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './errors';
|
||||||
|
export * from './input-values';
|
||||||
|
export * from './runtime';
|
||||||
|
export * from './symbols';
|
||||||
|
export * from './template';
|
||||||
|
export * from './types';
|
||||||
|
export * from './utils';
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import type { JSONSchema7 } from 'json-schema';
|
||||||
|
|
||||||
|
import { filterOutNullable } from './utils';
|
||||||
|
|
||||||
|
type InputValuesType =
|
||||||
|
| null
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| { [key: string]: InputValuesType }
|
||||||
|
| InputValuesType[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infers a default inputValues object based on the JSON schema of an object.
|
||||||
|
*/
|
||||||
|
export function inferDefaultInputValuesFromObjectJSONSchema(
|
||||||
|
schema: JSONSchema7
|
||||||
|
): Record<string, InputValuesType> {
|
||||||
|
if (schema.type !== 'object' || !schema.properties) {
|
||||||
|
throw new Error(`Expected schema of object to be provided: ${schema.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<string, InputValuesType> = {};
|
||||||
|
|
||||||
|
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
||||||
|
if (typeof propertySchema === 'boolean') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[key] = inferDefaultInputValueFromJSONSchema(propertySchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferDefaultInputValueFromJSONSchema(schema: JSONSchema7): InputValuesType {
|
||||||
|
switch (schema.type) {
|
||||||
|
case 'object':
|
||||||
|
return inferDefaultInputValuesFromObjectJSONSchema(schema);
|
||||||
|
case 'array': {
|
||||||
|
if (schema.items && Array.isArray(schema.items)) {
|
||||||
|
return schema.items.map((itemSchema) => {
|
||||||
|
if (typeof itemSchema === 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return inferDefaultInputValueFromJSONSchema(itemSchema);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
case 'string':
|
||||||
|
case 'number':
|
||||||
|
case 'integer':
|
||||||
|
case 'boolean':
|
||||||
|
case 'null':
|
||||||
|
return inferDefaultInputValueFromPrimitive(schema);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported schema type: ${schema.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferDefaultInputValueFromPrimitive(schema: JSONSchema7): InputValuesType {
|
||||||
|
switch (schema.type) {
|
||||||
|
case 'boolean':
|
||||||
|
return true;
|
||||||
|
case 'number':
|
||||||
|
case 'integer':
|
||||||
|
return 1234;
|
||||||
|
case 'string': {
|
||||||
|
const enumValues = schema.enum?.filter(filterOutNullable);
|
||||||
|
return enumValues?.[0] ?? 'default';
|
||||||
|
}
|
||||||
|
case 'null':
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported schema type: ${schema.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user