mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 14:00:28 +00:00
Compare commits
789 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd5d7a979a | |||
| 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 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix UX issue about highlighting the search term in search result sections
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix three small visual issues
|
||||
|
||||
- Fix sidebar showing on `no-toc` pages in the gradient theme
|
||||
- Fix variant selector truncating incorrectly in header when sections are present
|
||||
- Fix page cover alignment on `lg` screens without TOC
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Respect fullWidth and defaultWidth for images
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
fix nested a tag causing hydration error
|
||||
@@ -4,7 +4,7 @@
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
fix href being empty in TOC
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix navigation between sections/variants when previewing a site in v2
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add support for inline icons.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add circular corners and depth styling
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix crash when integration script fails to render block.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Add authorization header for OAuth2
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve support for OAuth2 security type
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Adds Columns layout block to GBO
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Support dark-mode specific page cover image
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Update Models page styling
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Optimize performances by using a smarter per-request cache arround data cached functions
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
add a force-revalidate api route to force bust the cache in case of errors
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Fix rendering of ogimage with SVG logos.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Best effort at preserving current variant when navigating between sections by matching the pathname against site spaces in the new section.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
encode customization header
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Rework full-width layout, add support for full-width page option
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
fix ISR on preview env
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix concurrent execution in Vercel causing pages to not be attached to the proper tags.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/react-contentkit": patch
|
||||
---
|
||||
|
||||
Add basic error handling when transitioning between states.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Generate a llms-full.txt version of the docs site
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/fonts": minor
|
||||
---
|
||||
|
||||
Initial version of the package
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix rendering of ogimage when logo or icon are AVIF images.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add margin to adjacent buttons
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Don't cache unexpected API errors for more than a few minutes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
add a global error boundary
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Indent JSON python code sample
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix missing title on button to close the announcement banner.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Make icons for page groups more contrasting
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Ignore case while highlighting search results.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Make TOC height dynamic based on visible header and footer elements
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix bold header links hover color
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
cache fonts and static image used in OGImage in memory
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix border being added to cards
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Handle nested deprecated properties in generateSchemaExample
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
apply customization for dynamic context
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Reverse order of feedback smileys
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Deduplicate path parameters from OpenAPI spec
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix ogimage using incorrect Google Font depending on language.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
remove trailing slash from linker
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
+49
-7
@@ -1,6 +1,8 @@
|
||||
# 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
|
||||
|
||||
@@ -45,13 +47,53 @@ 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.
|
||||
|
||||
##### 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
|
||||
|
||||
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 (Version: >=20.6)
|
||||
- Use `nvm` for easy Node management
|
||||
- [Bun](https://bun.sh/) (Version: >=1.2.15)
|
||||
- We use a text-based lockfile which isn't supported below 1.2.15
|
||||
|
||||
##### 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. Build the project:
|
||||
```bash
|
||||
bun build:v2
|
||||
```
|
||||
|
||||
4. Start the development server:
|
||||
```bash
|
||||
bun dev:v2
|
||||
```
|
||||
|
||||
Additional development commands:
|
||||
- `bun format`: Format the code using Biome
|
||||
- `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/`.
|
||||
|
||||
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/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
|
||||
|
||||
@@ -71,4 +113,4 @@ When you're finished with the changes, [create a pull request](https://docs.gith
|
||||
|
||||
### 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.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deployments status --config ./packages/gitbook-v2/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.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||
- id: deploy_middleware
|
||||
name: Deploy middleware to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Deploy server to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook-v2/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 }}"
|
||||
@@ -7,12 +7,12 @@ runs:
|
||||
- name: 🏗 Prepare Playwright env
|
||||
shell: bash
|
||||
run: |
|
||||
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["@playwright/test"].version')
|
||||
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["gitbook"].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
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: '~/.cache/ms-playwright'
|
||||
@@ -24,6 +24,7 @@ runs:
|
||||
- name: 🏗 Install Playwright browser binaries & OS dependencies
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
working-directory: packages/gitbook
|
||||
run: |
|
||||
bun x playwright install --with-deps chromium
|
||||
|
||||
@@ -31,5 +32,6 @@ runs:
|
||||
- name: 🏗 Install Playwright OS dependencies
|
||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
working-directory: packages/gitbook
|
||||
run: |
|
||||
bun x playwright install-deps
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
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_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
|
||||
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
|
||||
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
- name: Build worker
|
||||
run: bun run turbo build:v2:cloudflare
|
||||
env:
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
shell: bash
|
||||
|
||||
- name: Upload the DO worker
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deploy --config ./packages/gitbook-v2/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.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/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-v2/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.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/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,80 @@
|
||||
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
|
||||
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_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
|
||||
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
|
||||
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
- 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
|
||||
- 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'
|
||||
+30
-136
@@ -4,169 +4,63 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
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.1.18
|
||||
- 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.1.18
|
||||
- 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.1.18
|
||||
- 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:
|
||||
runs-on: ubuntu-latest
|
||||
name: Format
|
||||
timeout-minutes: 6
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.1.18
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun format:check
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: Lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.1.18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun lint --no-cache
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test
|
||||
timeout-minutes: 6
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.1.18
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun unit
|
||||
typecheck:
|
||||
build-oss:
|
||||
# CI to check that the repository builds correctly on a machine without the credentials
|
||||
runs-on: ubuntu-latest
|
||||
name: Typecheck
|
||||
name: Build (Open Source)
|
||||
timeout-minutes: 6
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ''
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.1.18
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
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
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
name: Preview
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
deploy-v1-cloudflare:
|
||||
name: Deploy v1 to Cloudflare Pages
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: ${{ github.ref == 'refs/heads/main' && '1c-production' || '1c-preview' }}
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
checks: write
|
||||
statuses: write
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
deployment-alias-url: ${{ steps.deploy.outputs.deployment-alias-url }}
|
||||
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: Sets env vars for production
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
|
||||
- name: Build Next.js with next-on-pages
|
||||
run: bun run turbo gitbook#build:cloudflare
|
||||
env:
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
- id: deploy
|
||||
name: Deploy to Cloudflare
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '3.112.0'
|
||||
command: pages deploy ./packages/gitbook/.vercel/output/static --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
echo "Alias URL: ${{ steps.deploy.outputs.deployment-alias-url }}"
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (preview)
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
- 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 }}
|
||||
deploy-v2-cloudflare:
|
||||
name: Deploy v2 to Cloudflare Worker (preview)
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
- 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() && !startsWith(github.ref, 'refs/heads/main')
|
||||
needs:
|
||||
- deploy-v1-cloudflare
|
||||
- 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 1
|
||||
|
||||
| Version | URL | Status |
|
||||
| --- | --- | --- |
|
||||
| Latest commit | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} |
|
||||
| PR | [${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} |
|
||||
|
||||
### Version 2
|
||||
|
||||
| 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 | `v1` | `2v` | `2c` |
|
||||
| --- | --- | --- | --- |
|
||||
| GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs) | [${{ 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-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ 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-v1:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v1
|
||||
needs: deploy-v1-cloudflare
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- 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-v1-cloudflare.outputs.deployment-url }}
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
visual-testing-v2-vercel:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2
|
||||
needs: deploy-v2-vercel
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- 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-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
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- 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-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-v1:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v1
|
||||
needs: deploy-v1-cloudflare
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Run Playwright tests
|
||||
run: bun e2e-customers
|
||||
env:
|
||||
BASE_URL: ${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
ARGOS_BUILD_NAME: 'customers-v1'
|
||||
visual-testing-customers-v2:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2
|
||||
needs: deploy-v2-vercel
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- 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'
|
||||
visual-testing-customers-v2-cloudflare:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- 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'
|
||||
pagespeed-testing-v1:
|
||||
runs-on: ubuntu-latest
|
||||
name: PageSpeed Testing v1
|
||||
needs: deploy-v1-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 pagespeed tests
|
||||
run: bun ./packages/gitbook/tests/pagespeed-testing.ts
|
||||
env:
|
||||
BASE_URL: ${{needs.deploy-v1-cloudflare.outputs.deployment-url}}
|
||||
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Production
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (production)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
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,55 @@
|
||||
name: Staging
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (staging)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
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 }}"
|
||||
@@ -5,6 +5,9 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
@@ -17,10 +20,8 @@ jobs:
|
||||
with:
|
||||
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
||||
fetch-depth: 0
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.1.18
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
@@ -30,6 +31,30 @@ jobs:
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
publish: npm run release
|
||||
version: npm run changeset-version
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
|
||||
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
release-preview:
|
||||
# For now it releases the cache-do to both preview and production
|
||||
# Once we changed to deploy the app only on release, we should change `release:preview` in `cache-do`
|
||||
name: Release Preview
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Release preview packages
|
||||
run: bun run release:preview
|
||||
env:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
+7
-28
@@ -5,16 +5,6 @@ node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
@@ -24,25 +14,14 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
# Turbo
|
||||
.turbo
|
||||
|
||||
# vercel
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
# Env files
|
||||
.env.local
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# visual tests
|
||||
screenshots/
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# Generated public files
|
||||
/public/~gitbook/static/
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.next
|
||||
|
||||
# Generated
|
||||
packages/emoji-codepoints/index.ts
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["biomejs.biome"]
|
||||
}
|
||||
Vendored
+11
-1
@@ -7,5 +7,15 @@
|
||||
["style \\=([^;]*);", "\"([^\"]*)\""],
|
||||
["style \\=([^;]*);", "\\`([^\\`]*)\\`"]
|
||||
],
|
||||
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"]
|
||||
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<h1 align="center">GitBook</h1>
|
||||
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<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="/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>
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<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">
|
||||
<img alt="GitBook Open Published Site" src="./assets/published-site.png">
|
||||
@@ -35,8 +35,10 @@ To run a local version of this project, please follow these simple steps.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (Version: >=18.x)
|
||||
- Bun
|
||||
- Node.js (Version: >=20.6)
|
||||
- Use nvm for easy Node management
|
||||
- Bun (Version: >=1.2.1)
|
||||
- We use a text-based lockfile which isn't supported below 1.2.1
|
||||
|
||||
### Set up
|
||||
|
||||
@@ -46,32 +48,62 @@ To run a local version of this project, please follow these simple steps.
|
||||
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
|
||||
```
|
||||
|
||||
3. Start your local development server.
|
||||
4. Run build.
|
||||
|
||||
```
|
||||
bun dev
|
||||
bun build:v2
|
||||
```
|
||||
|
||||
Then open the space in your web browser, using http://localhost:3000/<host>/<path> (example: http://localhost:3000/docs.gitbook.com).
|
||||
5. Start your local development server.
|
||||
|
||||
### Other development commands
|
||||
```
|
||||
bun dev:v2
|
||||
```
|
||||
|
||||
- `bun format`: format the code
|
||||
- `bun lint`: lint the code
|
||||
6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
|
||||
|
||||
examples:
|
||||
|
||||
- http://localhost:3000/url/gitbook.com/docs
|
||||
- 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
|
||||
|
||||
All pull-requests will be tested against both visual and performances testing to prevent regressions.
|
||||
|
||||
## Fonts and Icons
|
||||
|
||||
GitBook Open uses fontawesome. During development, your local environment will use the free version. However, only the pro version will be accepted by CI. If you see the following error:
|
||||
|
||||
```
|
||||
The GitBook icon is missing. It indicates that the dependencies were installed without the correct font-awesome package. These changes have probably been persisted in the Bun lockfile. Read the README for more information.
|
||||
```
|
||||
|
||||
It means that you've changed the GBO dependencies and bundled in the free version. Only GitBook staff can help with this - if you're not on the GitBook team, please ping us in the PR and we'll help get things moving.
|
||||
|
||||
If you are GitBook staff, you'll need our NPM token in your local environment.
|
||||
|
||||
```
|
||||
.env.local
|
||||
|
||||
NPM_TOKEN_READONLY=xxx
|
||||
```
|
||||
|
||||
and then reinstall dependencies.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -79,7 +111,7 @@ We encourage you to contribute to GitBook to help us build the best tool for doc
|
||||
|
||||
#### 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
|
||||
|
||||
@@ -90,7 +122,7 @@ Encounter a bug or find an issue you'd like to fix? Helping us fix issues relate
|
||||
> [!WARNING]
|
||||
> 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.
|
||||
>
|
||||
@@ -106,10 +138,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).
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
GitBook wouldn't be possible without these projects:
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": [
|
||||
"**/node_modules/**/*",
|
||||
"**/dist/**/*",
|
||||
"**/build/**/*",
|
||||
"**/public/**/*",
|
||||
"**/.next/**/*",
|
||||
"**/.open-next/**/*",
|
||||
"**/.turbo/**/*",
|
||||
"**/.vercel/**/*",
|
||||
"**/.cache/**/*",
|
||||
"**/.wrangler/**/*",
|
||||
"packages/openapi-parser/src/fixtures/**/*",
|
||||
"packages/emoji-codepoints/index.ts",
|
||||
"packages/icons/src/data/*.json",
|
||||
"packages/cache-do/worker-configuration.d.ts"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"useEditorconfig": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 100,
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"performance": {
|
||||
"noDelete": "warn"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"noUselessFragments": "warn",
|
||||
"noBannedTypes": "warn"
|
||||
},
|
||||
"correctness": {
|
||||
"noUndeclaredVariables": "error",
|
||||
"noUnusedVariables": "error",
|
||||
"useArrayLiterals": "error",
|
||||
"useHookAtTopLevel": "error",
|
||||
"noUnusedImports": "error",
|
||||
"noVoidElementsWithChildren": "warn",
|
||||
"useJsxKeyInIterable": "warn",
|
||||
"useExhaustiveDependencies": "warn",
|
||||
"noUnknownFunction": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "warn",
|
||||
"noParameterAssign": "off",
|
||||
"useThrowOnlyError": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noConsole": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"allow": ["assert", "error", "warn"]
|
||||
}
|
||||
},
|
||||
"noExplicitAny": "warn",
|
||||
"noImplicitAnyLet": "warn",
|
||||
"noConfusingVoidType": "warn",
|
||||
"noControlCharactersInRegex": "warn",
|
||||
"noPrototypeBuiltins": "warn",
|
||||
"noAssignInExpressions": "warn",
|
||||
"noArrayIndexKey": "warn"
|
||||
},
|
||||
"a11y": {
|
||||
"useSemanticElements": "warn",
|
||||
"useKeyWithClickEvents": "warn",
|
||||
"noSvgWithoutTitle": "warn",
|
||||
"useButtonType": "warn",
|
||||
"useIframeTitle": "warn",
|
||||
"useAltText": "warn",
|
||||
"noPositiveTabindex": "warn",
|
||||
"useFocusableInteractive": "warn",
|
||||
"useAriaPropsForRole": "warn",
|
||||
"useValidAnchor": "warn",
|
||||
"noLabelWithoutControl": "warn",
|
||||
"noNoninteractiveTabindex": "warn"
|
||||
},
|
||||
"nursery": {
|
||||
"useSortedClasses": {
|
||||
"level": "error",
|
||||
"fix": "safe",
|
||||
"options": {
|
||||
"attributes": ["class", "className", "style"],
|
||||
"functions": ["clsx", "tw"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "es5",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": [
|
||||
"packages/gitbook/**/*",
|
||||
"packages/gitbook-v2/**/*",
|
||||
"packages/react-openapi/**/*",
|
||||
"packages/react-math/**/*",
|
||||
"packages/react-contentkit/**/*",
|
||||
"packages/icons/**/*"
|
||||
],
|
||||
"javascript": {
|
||||
"globals": ["React"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["packages/gitbook/**/*"],
|
||||
"javascript": {
|
||||
"globals": ["React", "GitBookIntegrationEvent"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.css"],
|
||||
"javascript": {
|
||||
"globals": ["theme"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.test.ts", "packages/gitbook/tests/**/*"],
|
||||
"javascript": {
|
||||
"globals": ["Bun"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"packages/cache-do/**/*",
|
||||
"packages/gitbook/cf-env.d.ts",
|
||||
"packages/gitbook/src/cloudflare-entrypoint.ts"
|
||||
],
|
||||
"javascript": {
|
||||
"globals": [
|
||||
"DurableObjectLocationHint",
|
||||
"DurableObjectNamespace",
|
||||
"DurableObjectStub",
|
||||
"ContinentCode",
|
||||
"Fetcher",
|
||||
"ExportedHandler"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[install.scopes]
|
||||
"gitbook" = { token = "$NPM_TOKEN_READONLY", url = "https://registry.npmjs.org" }
|
||||
@@ -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,668 +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 Site (Navigation when switching variant)',
|
||||
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Keep navigation path/route when switching variant (Public)',
|
||||
url: 'api-multi-versions/reference/api-reference/pets',
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
const spaceDrowpdown = await page.waitForSelector(
|
||||
'[data-testid="space-dropdown-button"]',
|
||||
);
|
||||
await spaceDrowpdown.click();
|
||||
|
||||
// Click the second variant in the dropdown
|
||||
await page
|
||||
.getByRole('link', {
|
||||
name: '2.0',
|
||||
})
|
||||
.click();
|
||||
|
||||
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
|
||||
await page.waitForURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets?fallback=true',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Keep navigation path/route when switching variant (Share link)',
|
||||
url: 'api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/reference/api-reference/pets',
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
const spaceDrowpdown = await page.waitForSelector(
|
||||
'[data-testid="space-dropdown-button"]',
|
||||
);
|
||||
await spaceDrowpdown.click();
|
||||
|
||||
// Click the second variant in the dropdown
|
||||
await page
|
||||
.getByRole('link', {
|
||||
name: '2.0',
|
||||
})
|
||||
.click();
|
||||
|
||||
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
|
||||
await page.waitForURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets?fallback=true',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Keep navigation path/route when switching variant (VA)',
|
||||
screenshot: false,
|
||||
url: (() => {
|
||||
const privateKey = 'c26190fc-74b2-4b54-9fc7-df9941104953';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`;
|
||||
})(),
|
||||
run: async (page) => {
|
||||
const spaceDrowpdown = await page.waitForSelector(
|
||||
'[data-testid="space-dropdown-button"]',
|
||||
);
|
||||
await spaceDrowpdown.click();
|
||||
|
||||
// Click the second variant in the dropdown
|
||||
await page
|
||||
.getByRole('link', {
|
||||
name: '2.0',
|
||||
})
|
||||
.click();
|
||||
|
||||
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
|
||||
await page.waitForURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets?fallback=true',
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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: 'Hidden',
|
||||
url: 'page-options/page-hidden',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
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,83 +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, { dev, webpack }) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
|
||||
// Required for `swagger2openapi` to work:
|
||||
fs: false,
|
||||
path: false,
|
||||
http: false,
|
||||
};
|
||||
|
||||
// Tree shake debug code for Sentry
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-with-nextjs
|
||||
if (!dev) {
|
||||
config.plugins.push(
|
||||
new webpack.DefinePlugin({
|
||||
__SENTRY_DEBUG__: false,
|
||||
// We always init Sentry with enableTracing: false for now, so this is useless
|
||||
__SENTRY_TRACING__: false,
|
||||
__RRWEB_EXCLUDE_IFRAME__: true,
|
||||
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
|
||||
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
);
|
||||
+36
-86
@@ -1,96 +1,46 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"turbo": "^2.5.0",
|
||||
"vercel": "^39.3.0"
|
||||
},
|
||||
"packageManager": "bun@1.2.15",
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"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",
|
||||
"dev": "turbo run dev",
|
||||
"dev:v2": "turbo run dev:v2",
|
||||
"build": "turbo run build",
|
||||
"build:v2": "turbo run build:v2",
|
||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"format": "biome check --write ./",
|
||||
"format:check": "biome check --diagnostic-level=error ./",
|
||||
"unit": "turbo run unit",
|
||||
"e2e": "turbo run e2e",
|
||||
"e2e-customers": "turbo run e2e-customers",
|
||||
"changeset": "changeset",
|
||||
"release": "changeset publish",
|
||||
"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"
|
||||
"changeset-version": "changeset version && bun run format",
|
||||
"release": "turbo run release && changeset publish",
|
||||
"release:preview": "turbo run release:preview",
|
||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
||||
"clean": "turbo run clean"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@geist-ui/icons": "^1.0.2",
|
||||
"@gitbook/api": "^0.56.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.94.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@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.17.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"
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.121.0"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^2.0.0",
|
||||
"@changesets/cli": "^2.27.7",
|
||||
"@cloudflare/next-on-pages": "^1.11.3",
|
||||
"@cloudflare/workers-types": "^4.20231218.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsontoxml": "^1.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/katex": "^0.16.5",
|
||||
"@types/node": "^20",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/parse-cache-control": "^1.0.4",
|
||||
"@types/psi": "^4.1.6",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/rison": "^0.0.9",
|
||||
"autoprefixer": "^10",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "13.5.6",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.0.3",
|
||||
"psi": "^4.1.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.wrangler
|
||||
worker-configuration.d.ts
|
||||
dist/
|
||||
@@ -0,0 +1,18 @@
|
||||
# @gitbook/cache-do
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b7a5106: Disable cloudflare observability in production
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 9b8d519: Experiment with optimizing billable duration in Cloudflare by using multiple RPC sessions instead of one
|
||||
- 636b868: First version of a new cache backend powered by Cloudflare Durable Objects
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 56f5fa1: Enable Workers observability with a sampling of 0.1
|
||||
@@ -0,0 +1,22 @@
|
||||
# `@gitbook/cache-do`
|
||||
|
||||
Cache backend, powered by Cloudflare Durable Objects. The cache is optimized for GitBook use-cases.
|
||||
|
||||
### Performances
|
||||
|
||||
The cache backend is optimized for performances by being distributed and accessible close to the worker locations that are reading it.
|
||||
|
||||
### Geo-distribution
|
||||
|
||||
To achieve a good balance between **performances** and **consistency**, cache objects are distributed over 7 locations, representing continents.
|
||||
|
||||
It makes it possible to purge all 7 locations in one go and achieve fast consistency.
|
||||
|
||||
### Concepts
|
||||
|
||||
**Cache tag**: unique tag in the cache environment. A cache tag groups multiple keys that should be purged together in one operation.
|
||||
Cache tags should not contain a large set of unique keys. Exceeding thousands could lead to performances or reliability issues.
|
||||
|
||||
**Cache key**: unique key in the cache environment. Each key should be assigned to a `tag`.
|
||||
|
||||
**Location**: cache is distributed over 7 unique locations, one for each continent.
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@gitbook/cache-do",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./api": {
|
||||
"types": "./dist/api.d.ts",
|
||||
"development": "./src/api.ts",
|
||||
"default": "./dist/api.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"lru_map": "^0.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
"wrangler": "^4.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "wrangler types",
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w",
|
||||
"release": "wrangler deploy",
|
||||
"release:preview": "wrangler deploy && wrangler deploy --env preview"
|
||||
},
|
||||
"files": ["dist", "src", "bin", "data", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
import { decode, encode } from '@msgpack/msgpack';
|
||||
import { LRUMap } from 'lru_map';
|
||||
|
||||
export interface CacheObjectDescriptor {
|
||||
get: <Value = unknown>(key: string) => Promise<Value | undefined>;
|
||||
set: <Value = unknown>(key: string, value: Value, expiresAt: number) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value stored in a chunked binary msgpack format.
|
||||
* Stored under the key `prop.${key}.${index}`.
|
||||
*/
|
||||
interface CacheObjectProp<Value = unknown> {
|
||||
value: Value;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expiration clock stored under the key `exp.${expiresAt}.${key}`.
|
||||
*/
|
||||
interface CacheObjectExp {
|
||||
/** Key of the property */
|
||||
k: string;
|
||||
/** Number of chunks */
|
||||
c: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Durable Object class being deployed as a distributed cache.
|
||||
*/
|
||||
export class CacheObject extends DurableObject {
|
||||
private lru = new LRUMap<string, { match: CacheObjectProp | undefined }>(500);
|
||||
|
||||
/**
|
||||
* Open a descriptor to access the cache object.
|
||||
* The goal is to minimize the amount of RPC sessions between the client and the cache object.
|
||||
* One session is opened per request on the client side and used to perform multiple operations.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/rpc/#return-functions-from-rpc-methods
|
||||
*/
|
||||
public open(): CacheObjectDescriptor {
|
||||
return {
|
||||
get: async <Value = unknown>(key: string) => {
|
||||
return this.get<Value>(key);
|
||||
},
|
||||
set: async <Value = unknown>(key: string, value: Value, expiresAt: number) => {
|
||||
await this.set(key, value, expiresAt);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a property.
|
||||
*/
|
||||
public async get<Value = unknown>(key: string) {
|
||||
return this.logOperation({ operation: 'get', key }, async (setLog) => {
|
||||
// Try the memory state first.
|
||||
const memoryEntry = this.lru.get(key);
|
||||
if (memoryEntry) {
|
||||
setLog({ memory: true });
|
||||
setLog({ memoryMatch: !!memoryEntry.match });
|
||||
if (!memoryEntry.match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExpired = memoryEntry.match.expiresAt < Date.now();
|
||||
setLog({ memoryExpired: isExpired });
|
||||
|
||||
if (!isExpired) {
|
||||
return memoryEntry.match.value as Value;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.getFromStorage<Value>(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a property from the DO storage.
|
||||
*/
|
||||
public async getFromStorage<Value = unknown>(key: string) {
|
||||
return this.logOperation({ operation: 'getFromStorage', key }, async (setLog) => {
|
||||
const entries = await this.ctx.storage.list<Uint8Array>({
|
||||
prefix: getStoragePropKey(key),
|
||||
noCache: true,
|
||||
});
|
||||
if (entries.size) {
|
||||
const entry = decodeChunks<CacheObjectProp<Value>>(entries);
|
||||
setLog({ chunks: entries.size, chunksSize: entry?.size ?? 0 });
|
||||
if (entry && entry.value.expiresAt > Date.now()) {
|
||||
// Found
|
||||
this.lru.set(key, { match: entry.value });
|
||||
return entry.value.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Not found
|
||||
this.lru.set(key, { match: undefined });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the cache object.
|
||||
*/
|
||||
public async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
|
||||
return this.logOperation({ operation: 'set', key }, async (setLog) => {
|
||||
const prop: CacheObjectProp<Value> = {
|
||||
value,
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
this.lru.set(key, { match: prop });
|
||||
await this.ctx.storage.transaction(async (tx) => {
|
||||
const entries = encodeChunks(key, prop);
|
||||
const chunks = Object.keys(entries).length;
|
||||
setLog({ chunks });
|
||||
|
||||
const clockValue: CacheObjectExp = {
|
||||
k: key,
|
||||
c: chunks,
|
||||
};
|
||||
|
||||
await tx.put(getGCClockKey(key, expiresAt), clockValue);
|
||||
await tx.put(entries);
|
||||
|
||||
const currentAlarm = await tx.getAlarm();
|
||||
if (!currentAlarm) {
|
||||
// Set an alarm to garbage collect all entries that have expired in 12h.
|
||||
await tx.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge all keys in the cache object.
|
||||
*/
|
||||
public async purge() {
|
||||
return this.logOperation({ operation: 'purge' }, async (setLog) => {
|
||||
const result = new Set<string>();
|
||||
|
||||
try {
|
||||
// List all the keys in the cache object.
|
||||
const entries = await this.ctx.storage.list<CacheObjectExp>({
|
||||
prefix: 'exp.',
|
||||
noCache: true,
|
||||
});
|
||||
setLog({ entries: entries.size });
|
||||
entries.forEach((exp) => {
|
||||
result.add(exp.k);
|
||||
});
|
||||
} catch (_error) {}
|
||||
|
||||
await this.reset();
|
||||
return Array.from(result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Alarm to garbage collect all entries that have expired.
|
||||
*/
|
||||
async alarm() {
|
||||
return this.logOperation({ operation: 'alarm' }, async (setLog) => {
|
||||
try {
|
||||
const entries = await this.ctx.storage.list<CacheObjectExp>({
|
||||
prefix: 'exp.',
|
||||
noCache: true,
|
||||
});
|
||||
setLog({ entries: entries.size });
|
||||
const toDeleteSet = new Set<string>();
|
||||
|
||||
for (const [key, exp] of entries) {
|
||||
const timestamp = Number.parseInt(key.split('.')[1]);
|
||||
if (timestamp < Date.now()) {
|
||||
toDeleteSet.add(key);
|
||||
for (let i = 0; i < exp.c; i++) {
|
||||
toDeleteSet.add(getStoragePropChunkKey(exp.k, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the keys by batch of 128.
|
||||
const toDelete = Array.from(toDeleteSet);
|
||||
setLog({ toDelete: toDelete.length });
|
||||
for (let i = 0; i < toDelete.length; i += 128) {
|
||||
await this.ctx.storage.delete(toDelete.slice(i, i + 128));
|
||||
}
|
||||
|
||||
// If there are still keys to delete, set an alarm to continue the deletion in 12h.
|
||||
if (toDelete.length) {
|
||||
await this.ctx.storage.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
|
||||
}
|
||||
} catch (_error) {
|
||||
await this.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the cache object.
|
||||
*/
|
||||
async reset() {
|
||||
return this.logOperation({ operation: 'reset' }, async () => {
|
||||
this.lru.clear();
|
||||
await this.ctx.storage.deleteAll();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Time and log an operation.
|
||||
*/
|
||||
async logOperation<T>(
|
||||
log: Record<string, unknown>,
|
||||
fn: (update: (log: Record<string, unknown>) => void) => Promise<T>
|
||||
): Promise<T> {
|
||||
const objectId = this.ctx.id.name ?? this.ctx.id.toString();
|
||||
const update: Record<string, unknown> = {};
|
||||
const start = performance.now();
|
||||
try {
|
||||
return await fn((arg) => {
|
||||
Object.assign(update, arg);
|
||||
});
|
||||
} finally {
|
||||
const duration = performance.now() - start;
|
||||
console.log({ ...log, ...update, objectId, duration });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getStoragePropKey(key: string): string {
|
||||
return `prop.${key}.`;
|
||||
}
|
||||
|
||||
function getStoragePropChunkKey(key: string, index: number): string {
|
||||
return `${getStoragePropKey(key)}${index}`;
|
||||
}
|
||||
|
||||
function getGCClockRootKey(timestamp: number): string {
|
||||
return `exp.${timestamp}.`;
|
||||
}
|
||||
|
||||
function getGCClockKey(key: string, expiresAt: number): string {
|
||||
return `${getGCClockRootKey(expiresAt)}${key}`;
|
||||
}
|
||||
|
||||
function encodeChunks<T>(key: string, value: T): Record<string, Uint8Array> {
|
||||
const buf = encode(value);
|
||||
const entries: Record<string, Uint8Array> = {};
|
||||
const chunks = chunkUint8Array(buf, 128 * 1024);
|
||||
|
||||
for (let index = 0; index < chunks.length; index++) {
|
||||
entries[getStoragePropChunkKey(key, index)] = chunks[index];
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function decodeChunks<T>(entries: Map<string, Uint8Array>): { value: T; size: number } | undefined {
|
||||
const chunks = Array.from(entries.entries())
|
||||
.map(([key, value]) => {
|
||||
const index = Number.parseInt(key.split('.').pop()!);
|
||||
return [index, value] as const;
|
||||
})
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([, value]) => value);
|
||||
|
||||
if (chunks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buf = mergeUint8Array(chunks);
|
||||
return { value: decode(buf) as T, size: buf.length };
|
||||
}
|
||||
|
||||
function chunkUint8Array(input: Uint8Array, chunkSize: number): Uint8Array[] {
|
||||
const chunks: Uint8Array[] = [];
|
||||
for (let i = 0; i < input.length; i += chunkSize) {
|
||||
chunks.push(input.slice(i, i + chunkSize));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function mergeUint8Array(chunks: Uint8Array[]): Uint8Array {
|
||||
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { CacheObject } from './CacheObject';
|
||||
|
||||
export type CacheLocationId = ContinentCode;
|
||||
const allLocations: CacheLocationId[] = ['AF', 'AS', 'NA', 'SA', 'AN', 'EU', 'OC'];
|
||||
|
||||
/**
|
||||
* Location hint for the CacheObject durable object.
|
||||
*/
|
||||
const doLocationHints: {
|
||||
[key in CacheLocationId]: DurableObjectLocationHint;
|
||||
} = {
|
||||
AF: 'afr',
|
||||
AS: 'apac',
|
||||
NA: 'wnam',
|
||||
SA: 'sam',
|
||||
AN: 'oc',
|
||||
EU: 'weur',
|
||||
OC: 'oc',
|
||||
};
|
||||
|
||||
/**
|
||||
* Client to access a cache tag.
|
||||
*/
|
||||
export class CacheObjectStub {
|
||||
private stub: DurableObjectStub<CacheObject>;
|
||||
|
||||
constructor(
|
||||
/** Binding to the CacheObject durable object */
|
||||
private doNamespace: DurableObjectNamespace<CacheObject>,
|
||||
/** ID of the location to target */
|
||||
private locationId: CacheLocationId,
|
||||
/** Name of the tag */
|
||||
private tag: string
|
||||
) {
|
||||
const groupId = getCacheObjectIdName(this.locationId, this.tag);
|
||||
this.stub = this.doNamespace.get(this.doNamespace.idFromName(groupId), {
|
||||
// Initialize the object with a locaiton hint,
|
||||
// as we might want to purge all locations before the object is created.
|
||||
// https://developers.cloudflare.com/durable-objects/reference/data-location/
|
||||
locationHint: doLocationHints[this.locationId],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a descriptor to the cache object.
|
||||
* It can be used to perform multiple operations in a single RPC session.
|
||||
* Ex:
|
||||
* ```ts
|
||||
* using desc = cache.open();
|
||||
* await desc.set('key', 'value', Date.now() + 1000);
|
||||
* await desc.get('key');
|
||||
* ```
|
||||
*/
|
||||
async open() {
|
||||
return await this.stub.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from the cache.
|
||||
*/
|
||||
async get<Value = unknown>(key: string) {
|
||||
return (await this.stub.get(key)) as Value | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the cache.
|
||||
*/
|
||||
async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
|
||||
return await this.stub.set(key, value, expiresAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge all keys in the cache tag.
|
||||
*/
|
||||
async purge() {
|
||||
const keys = new Set<string>();
|
||||
await Promise.all(
|
||||
allLocations.map(async (locationId) => {
|
||||
const groupId = getCacheObjectIdName(locationId, this.tag);
|
||||
const cacheGroup = this.doNamespace.get(this.doNamespace.idFromName(groupId), {
|
||||
// Initialize the object with a locaiton hint,
|
||||
// as we might want to purge all locations before the object is created.
|
||||
// https://developers.cloudflare.com/durable-objects/reference/data-location/
|
||||
locationHint: doLocationHints[this.locationId],
|
||||
});
|
||||
const locationkeys = await cacheGroup.purge();
|
||||
locationkeys.forEach((key) => keys.add(key));
|
||||
})
|
||||
);
|
||||
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
function getCacheObjectIdName(locationId: CacheLocationId, tag: string): string {
|
||||
return `${locationId}:${tag}`;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './CacheObjectStub';
|
||||
@@ -0,0 +1,9 @@
|
||||
import { WorkerEntrypoint } from 'cloudflare:workers';
|
||||
|
||||
export * from './CacheObject';
|
||||
|
||||
export default class Worker extends WorkerEntrypoint {
|
||||
fetch() {
|
||||
return new Response('Hello, world!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"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,
|
||||
"incremental": true,
|
||||
"types": ["./worker-configuration.d.ts"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"generate": {
|
||||
"outputs": ["worker-configuration.d.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
main = "./src/index.ts"
|
||||
name = "gitbook-open-cache"
|
||||
compatibility_date = "2024-09-02"
|
||||
|
||||
durable_objects.bindings = [
|
||||
{name = "CACHE", class_name = "CacheObject"}
|
||||
]
|
||||
|
||||
migrations = [
|
||||
{tag = "v1", new_classes = ["CacheObject"]}
|
||||
]
|
||||
|
||||
[observability]
|
||||
enabled = false
|
||||
|
||||
[env.preview]
|
||||
name = "gitbook-open-cache-preview"
|
||||
durable_objects.bindings = [
|
||||
{name = "CACHE", class_name = "CacheObject"}
|
||||
]
|
||||
migrations = [
|
||||
{tag = "v1", new_classes = ["CacheObject"]}
|
||||
]
|
||||
|
||||
[env.preview.observability]
|
||||
enabled = true
|
||||
head_sampling_rate = 0.1
|
||||
@@ -0,0 +1 @@
|
||||
dist/
|
||||
@@ -0,0 +1,25 @@
|
||||
# @gitbook/cache-tags
|
||||
|
||||
## 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,25 @@
|
||||
{
|
||||
"name": "@gitbook/cache-tags",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w"
|
||||
},
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import type { ComputedContentSource } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
translationSettings: 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.translationSettings}`;
|
||||
default:
|
||||
assertNever(spec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache tag for a given URL.
|
||||
*/
|
||||
export function getCacheTagForURL(url: string | URL) {
|
||||
const parsedURL = url instanceof URL ? url : new URL(url);
|
||||
return getCacheTag({
|
||||
tag: 'url',
|
||||
hostname: parsedURL.hostname,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tags for a computed content source.
|
||||
*/
|
||||
export function getComputedContentSourceCacheTags(
|
||||
inContext: {
|
||||
spaceId: string;
|
||||
organizationId: string;
|
||||
},
|
||||
source: ComputedContentSource
|
||||
) {
|
||||
const tags: string[] = [];
|
||||
|
||||
// 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-language':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'translation',
|
||||
organization: inContext.organizationId,
|
||||
translationSettings: dependency.ref.translationSettings,
|
||||
})
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Do not throw for unknown dependency types
|
||||
// as it might mean we are lacking 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,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 @@
|
||||
dist/
|
||||
@@ -0,0 +1,33 @@
|
||||
# @gitbook/colors
|
||||
|
||||
## 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,21 @@
|
||||
{
|
||||
"name": "@gitbook/colors",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.3.3",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w"
|
||||
},
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
@@ -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,440 @@
|
||||
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]; // Reference white (D65)
|
||||
|
||||
export enum ColorCategory {
|
||||
backgrounds = 'backgrounds',
|
||||
components = 'components',
|
||||
borders = 'borders',
|
||||
accents = 'accents',
|
||||
text = 'text',
|
||||
}
|
||||
|
||||
type ColorSubScale = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
[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],
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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,
|
||||
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;
|
||||
|
||||
if (mixColor && mix?.ratio && mix.ratio > 0) {
|
||||
// If defined, we mix in a (tiny) bit of the mix color with the base color.
|
||||
baseColor.L = mixColor.L * mix.ratio + baseColor.L * (1 - mix.ratio);
|
||||
baseColor.C = mixColor.C * mix.ratio + baseColor.C * (1 - mix.ratio);
|
||||
baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H;
|
||||
}
|
||||
|
||||
if (
|
||||
(darkMode && baseColor.L < backgroundColor.L) ||
|
||||
(!darkMode && baseColor.L > backgroundColor.L)
|
||||
) {
|
||||
// If the supplied color is outside of our lightness bounds, use the supplied color's lightness.
|
||||
// This is mostly used to allow darker-than-dark backgrounds for brands that specifically want that look.
|
||||
const difference = (backgroundColor.L - baseColor.L) / backgroundColor.L;
|
||||
backgroundColor.L = baseColor.L;
|
||||
// At the edges of the scale, the subtle lightness changes stop being perceptible. We need to amp up our mapping to still stand out.
|
||||
const amplifier = 1;
|
||||
mapping = mapping.map((step, index) =>
|
||||
index < 9 ? step + step * amplifier * difference : step
|
||||
);
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
for (let index = 0; index < mapping.length; index++) {
|
||||
const targetL =
|
||||
foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
|
||||
|
||||
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:
|
||||
return 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]) {
|
||||
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];
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
index.ts
|
||||
dist/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import emojisRaws from 'emoji-assets/emoji.json';
|
||||
|
||||
interface EmojiData {
|
||||
@@ -16,11 +17,11 @@ Object.entries(emojis).forEach(([key, value]) => {
|
||||
if (emoji && key !== emoji) {
|
||||
output[key] = emoji;
|
||||
} else if (!emoji) {
|
||||
console.log('No emoji for', key);
|
||||
}
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.resolve(__dirname, 'dist'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
'index.ts',
|
||||
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`,
|
||||
path.resolve(__dirname, 'dist/index.ts'),
|
||||
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user