mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 14:00:28 +00:00
Compare commits
976 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 885264f132 | |||
| f1a6dec951 | |||
| 44f41510fa | |||
| f220229e5e | |||
| 61b4cb8d7c | |||
| 185cdb4883 | |||
| 2cdba53451 | |||
| 950f6c36cf | |||
| ea37977b1e | |||
| 81da82aed7 | |||
| 39a3518651 | |||
| d903273759 | |||
| d130532f69 | |||
| 94fcd21f34 | |||
| 1d2db4ae2b | |||
| d270b4af9b | |||
| ccfbd3c1a8 | |||
| 5c4923557f | |||
| b1f608b865 | |||
| c7993756dd | |||
| d02465823f | |||
| 6b9f6b5b79 | |||
| df7de8fb0c | |||
| 72f5ce3985 | |||
| 04c80262e2 | |||
| 56d1b8fe2d | |||
| 054a1634ab | |||
| 6ff63f2db6 | |||
| b5ad0ce1e5 | |||
| 2ba7e54c81 | |||
| 0a858e74ad | |||
| b0c534fc2c | |||
| 250c194a02 | |||
| 5b10cde936 | |||
| 4aeb81bfa5 | |||
| 955cebf944 | |||
| 7c951ef437 | |||
| 68f0dbcc35 | |||
| 39c4f76aea | |||
| 2fe58e8cbb | |||
| 765141a4d8 | |||
| eb1bd3aa01 | |||
| b7284b0223 | |||
| 5bbd254742 | |||
| 611e28626a | |||
| 2a3bb0eb22 | |||
| 6816f0fa5a | |||
| 17386776ec | |||
| 9ee9082614 | |||
| 334cfdd522 | |||
| 8c961a5dbd | |||
| 4708956e77 | |||
| 813dd03dbb | |||
| 0522dbc61e | |||
| acb9f53156 | |||
| 6016846156 | |||
| cf1fae5837 | |||
| 7212345466 | |||
| e38caf78ff | |||
| 0a8dd67541 | |||
| ed684c19a9 | |||
| 52ab3681e3 | |||
| 9cc5a787e9 | |||
| 0003030bf5 | |||
| 9ad8f45d38 | |||
| a3855257c8 | |||
| 9169c2f053 | |||
| abe028bd38 | |||
| 12c2451f4f | |||
| 8daede5427 | |||
| 4c89aa60eb | |||
| d0a3f64ae4 | |||
| 433d66482d | |||
| bd553bcf9e | |||
| f5894bcf74 | |||
| 1b59e7ce36 | |||
| 32d6f5845d | |||
| 4f5cbfe732 | |||
| afba8fcb34 | |||
| 377a4899e4 | |||
| 938bdeb34b | |||
| d216eaa3f6 | |||
| 5ca5da0f21 | |||
| ebe6eb3b39 | |||
| 3bfe347096 | |||
| 7b38f89078 | |||
| 09b689b0ea | |||
| 2350baa75f | |||
| bc1eca815e | |||
| 78c10340e7 | |||
| c16890a3a6 | |||
| 3b4fe2827a | |||
| 7e807aabb5 | |||
| 973c74ee69 | |||
| efed0b0617 | |||
| ca3b9aca5d | |||
| e2afc07ab2 | |||
| e8fb84d362 | |||
| 57f951a7d9 | |||
| 28008667ed | |||
| f3affc3034 | |||
| 59da30f3c6 | |||
| 2db721112a | |||
| b60039b7d5 | |||
| 8fe9c9afea | |||
| 216ba7a556 | |||
| a2ff57081b | |||
| 40dbd9a14b | |||
| e5bac69c5c | |||
| 8d6598393f | |||
| 59b86eb9dc | |||
| 52c9f6da2f | |||
| dd65987c94 | |||
| ace61901a4 | |||
| e6c3c7635d | |||
| 5134d9e7f8 | |||
| 6821fb2937 | |||
| cfe4812045 | |||
| 14843ac343 | |||
| 8fb6d7465f | |||
| 0ef647586f | |||
| b4039627d9 | |||
| 4a295e6db4 | |||
| 5726999a5a | |||
| 58d7f3c140 | |||
| 07eab986d5 | |||
| 711cf38f9b | |||
| 4f7c0eea9f | |||
| f3448de2da | |||
| ec92075ef8 | |||
| 1e013cd87c | |||
| f033734308 | |||
| 50c05164f7 | |||
| 7fb9004b43 | |||
| 4721403986 | |||
| df848ef64e | |||
| 0b40ccd286 | |||
| caaa692f0b | |||
| 42d43e09c4 | |||
| 7ccbf55ba7 | |||
| 81f5bfc2f9 | |||
| a7a713bca2 | |||
| d6613c787f | |||
| 427f748e1c | |||
| 4f5fec7213 | |||
| 8c0a53ab40 | |||
| 2dfd5aab20 | |||
| 8d3c6562a2 | |||
| dae019c115 | |||
| 73e0cbb2d6 | |||
| 392f59450c | |||
| b4918f60ce | |||
| 8f7c304d58 | |||
| a3a944d7dd | |||
| 88a35ed057 | |||
| 67998b6f15 | |||
| ff6d1150a5 | |||
| 500c8cb649 | |||
| 6859f7d239 | |||
| dfa8a37be1 | |||
| 11a6511b7a | |||
| af98402655 | |||
| b7a0db3339 | |||
| 382a19885b | |||
| d99da6a3ae | |||
| 72cd0e59e6 | |||
| 42d88da73c | |||
| 2863fe0dc1 | |||
| 87218062be | |||
| 8a3910e208 | |||
| 6294bbb53c | |||
| a28a997851 | |||
| 28a9ee701e | |||
| 50e8b2ec52 | |||
| 315717fa2d | |||
| da67711c90 | |||
| fecdbe8dde | |||
| 9316ccd1f4 | |||
| c3b620e975 | |||
| 7a00880e5b | |||
| b6b597564d | |||
| d410381957 | |||
| 902c3c6c1a | |||
| 4fb2a4a6d7 | |||
| 2d64c78787 | |||
| fbfcca5dae | |||
| 664ae8bff6 | |||
| fa3d2aae08 | |||
| e7a591d9f6 | |||
| 2860beacdc | |||
| 231167d3e1 | |||
| c0ee60ec13 | |||
| f58b904ba6 | |||
| d5992f1a5c | |||
| fa12f9eeb6 | |||
| 33726c88a0 | |||
| 6aa3ff9c5e | |||
| 7d3fe23195 | |||
| 521052d84f | |||
| c730845e17 | |||
| 7b5a3616c4 | |||
| c9373efdb5 | |||
| 015615de8a | |||
| 0f867e533a | |||
| 7d5a6d2255 | |||
| a1da4f023d | |||
| 666e8426e1 | |||
| df2fa42bda | |||
| b3a7ad6d2c | |||
| 2932077bf9 | |||
| 957afd967f | |||
| 4c9a9d0416 | |||
| 40df91a363 | |||
| af66ff71ed | |||
| 6f8ecd6111 | |||
| fa3eb07617 | |||
| dc4268db64 | |||
| a0c06a72cc | |||
| 19c20c0be3 | |||
| fae34e56ac | |||
| ba0094a862 | |||
| b259009c9c | |||
| d88dd5ed8a | |||
| 57bb146075 | |||
| 4b67fe5317 | |||
| 938125e772 | |||
| 1c8d9febb3 | |||
| 223edae17d | |||
| 12a455dab5 | |||
| d00dc8ca72 | |||
| 2c85a302fc | |||
| 3460b78b45 | |||
| 95a1f652f9 | |||
| cc37e2ae15 | |||
| 08382ce62f | |||
| 80cb52a237 | |||
| c6637b09e2 | |||
| 778624af00 | |||
| e15757d01f | |||
| aed79fd6c3 | |||
| 0c973a3292 | |||
| 56c923c0ed | |||
| 7d7806df30 | |||
| cb5598dc19 | |||
| 47f01edcb8 | |||
| e6ddc0f07f | |||
| 373f18f451 | |||
| 3f292065cd | |||
| 5e975ab95b | |||
| a3ec264764 | |||
| 5d504ffa4c | |||
| 04999662db | |||
| f7a34706c7 | |||
| f328a41982 | |||
| 20ebecb114 | |||
| 5a69692f54 | |||
| 42ca7e1580 | |||
| c4ebb3fecd | |||
| 2a805cc082 | |||
| 89a5816ee4 | |||
| c3f6b8c003 | |||
| 0e201d5899 | |||
| ae5f1abc58 | |||
| 580101d04e | |||
| 81b4a4db53 | |||
| 8339e91e2b | |||
| 3119066728 | |||
| 326e28e9b0 | |||
| 90f0127ada | |||
| 0353a8186c | |||
| dd043df00f | |||
| 97b7c79bc6 | |||
| 634e0b4302 | |||
| 2a23f1f003 | |||
| 88ffdcc303 | |||
| b6b09d42af | |||
| ebc39e9db9 | |||
| 0df3849ea2 | |||
| 8ed1bda2f2 | |||
| d67699a1f1 | |||
| 3363a18856 | |||
| ad1dc0b914 | |||
| e4c7307c99 | |||
| f9c942c8ff | |||
| eeb977fc03 | |||
| 4b8a62122a | |||
| e44e947fc1 | |||
| 7588cfe1c5 | |||
| a9a643f222 | |||
| 77d6cb75be | |||
| 1942d221df | |||
| 041601ed65 | |||
| 5567281a85 | |||
| 4c3753046b | |||
| 89f8e16184 | |||
| a3e1125ced | |||
| 4dae8acfed | |||
| bc081c4344 | |||
| e4275b235d | |||
| 77397cac82 | |||
| b6a08bc679 | |||
| 116575c811 | |||
| 4ba7bd5ce1 | |||
| 223ab5377f | |||
| ae43ca146c | |||
| 42342893d6 | |||
| eedefdd5c9 | |||
| f5e152dd74 | |||
| 95ea22d9c5 | |||
| f23023ddfa | |||
| da74077954 | |||
| 29aaba5949 | |||
| daf41fc728 | |||
| b08eb1af59 | |||
| 8322cd3e35 | |||
| 444039c142 | |||
| 3b0eff7f71 | |||
| c765463e38 | |||
| e8ca25b67e | |||
| 528eee3fc6 | |||
| 168a4fa998 | |||
| 3ea75adba4 | |||
| cbd768a095 | |||
| 2d01653a78 | |||
| aa3357a57d | |||
| de53946896 | |||
| d51204148e | |||
| 319761a39e | |||
| 7fbf01a348 | |||
| c22490bd80 | |||
| 860fd6d02e | |||
| c43c524804 | |||
| 66bb938d85 | |||
| 0e6d928496 | |||
| f07982d9f8 | |||
| b92ecfad58 | |||
| 416bde70bc | |||
| 580f7ade15 | |||
| cdffd7c47b | |||
| 5a959cf041 | |||
| 31466546a2 | |||
| 90ead98eaa | |||
| 65e9cb9d5c | |||
| e59076a074 | |||
| 653920dec9 | |||
| 23cedd2d8b | |||
| 70c4182fd8 | |||
| b62b101065 | |||
| b9c929beeb | |||
| 2b6c593289 | |||
| c0b339e406 | |||
| da485f5144 | |||
| 139a8050a5 | |||
| da7b369ffd | |||
| 432da64919 | |||
| 7d0b422200 | |||
| fb90eb0acc | |||
| b647538e41 | |||
| e50797a5c1 | |||
| 5b1e01c786 | |||
| 813b2af06b | |||
| 6f71da88d8 | |||
| fa91eb7eaf | |||
| c756761591 | |||
| 434af90366 | |||
| 0f41e194cb | |||
| bd353489e9 | |||
| 77fd393f15 | |||
| 8701b5e078 | |||
| 61db166f92 | |||
| 0d9b9cea0f | |||
| 7bb37c7f0a | |||
| 5b2bf828e3 | |||
| c85d3f9752 | |||
| 4d7088bbf0 | |||
| dd6ab6044c | |||
| d236bf029c | |||
| d70d566abe | |||
| a25fdedea9 | |||
| 373183a2cd | |||
| e9fa50dfb5 | |||
| 57ca4e0e88 | |||
| bc90adb73a | |||
| 1505ddbd5a | |||
| cd99ed57fb | |||
| 27de1cd01d | |||
| ae78fc5394 | |||
| 40e8e69c6d | |||
| e84a46a558 | |||
| 18a72d4466 | |||
| d7aa5fa003 | |||
| 27532a5f41 | |||
| 48c18c03b9 | |||
| bba2e52e24 | |||
| 54ee0149e1 | |||
| 8fda9d2fab | |||
| 77a72d79ce | |||
| 73e2b472ef | |||
| 2f28cc1323 | |||
| f5516d4e21 | |||
| d2facb2db1 | |||
| 6eae764b7c | |||
| 970ef8f886 | |||
| ed0720699e | |||
| ad94d00d5f | |||
| c7843f7f52 | |||
| 1fe3286bdb | |||
| ce030fdf76 | |||
| 9ba8783952 | |||
| 2f3966cc53 | |||
| 8f65a22e13 | |||
| 2787d884cd | |||
| fc00b51683 | |||
| f80c3a7b37 | |||
| 4e5d6c7487 | |||
| 96f84b5917 | |||
| 6b8a5fdd1b | |||
| 89083ac790 | |||
| 70be2c6b2b | |||
| bcf877841b | |||
| a84b06bb19 | |||
| 24b7808d9a | |||
| 8364ee988e | |||
| 07f6e2d233 | |||
| 7212973f34 | |||
| af44e0436b | |||
| 3f9c35d505 | |||
| 2c13f8b95c | |||
| 4dab1c56e5 | |||
| 4723f03ef1 | |||
| 278689e0a0 | |||
| 886e204e68 | |||
| c59947ab39 | |||
| 4f0a772a21 | |||
| 903f962673 | |||
| 31d800e48b | |||
| c1c58eb919 | |||
| eec3eed100 | |||
| 99da8df3d1 | |||
| c9ea23919e | |||
| 16292def79 | |||
| 9108c56ca1 | |||
| b011ea0af8 | |||
| d33171b779 | |||
| 6aaeae269e | |||
| e5f0cc8663 | |||
| 9b5f971aa1 | |||
| ff3b708ad3 | |||
| e996904013 | |||
| f32bf1f15d | |||
| 6cda2dd57b | |||
| c60e9ba27f | |||
| 9bc3d504c8 | |||
| b5e4fd75e9 | |||
| a92cb49445 | |||
| 88f64b0d63 | |||
| 844059f9a5 | |||
| eaf2d68308 | |||
| 12bdbf988d | |||
| 29bab79005 | |||
| 8d5de97ddf | |||
| d9b5a898ab | |||
| f127d28675 | |||
| 65c1bdc455 | |||
| 0711a171d4 | |||
| c58c551f9e | |||
| f574858b9b | |||
| 3e1592e16b | |||
| 90604c4c3b | |||
| 9ffc3b606d | |||
| e00c87d3e6 | |||
| bb3ca9c165 | |||
| 8ee9757132 | |||
| 946a463979 | |||
| 7aad9f7ff8 | |||
| 3fe8f7c219 | |||
| 0278a14e8e | |||
| e5366d3a49 | |||
| 38e84ffe1a | |||
| 052e07a936 | |||
| 7e67ebc11d | |||
| c9dea94717 | |||
| 1344754ca9 | |||
| 0f4759fb89 | |||
| 6d77b647a4 | |||
| 5907bd9af4 | |||
| b66d04c9c5 | |||
| 3173d8ec2d | |||
| 31e381035e | |||
| 259074b81f | |||
| 9a2f5b9295 | |||
| 58cd70b0c1 | |||
| 821899a767 | |||
| bfcc119ff3 | |||
| 76c797498e | |||
| 13514bf6fa | |||
| 05ffd0e321 | |||
| 701eaad92a | |||
| c66ef5dd10 | |||
| f774f9b3a0 | |||
| 29dc7ccb1a | |||
| 80237c34f9 | |||
| 979233c5c0 | |||
| 2f03e6acee | |||
| c492f09a19 | |||
| 2c3af5e73c | |||
| 6597f8508d | |||
| 8beb5d6bcf | |||
| 53f5dbea26 | |||
| 725952a106 | |||
| 3319375e9a | |||
| c5a4619020 | |||
| 989fc69abe | |||
| 0924259217 | |||
| 05affac34f | |||
| 453a459566 | |||
| 722f02ea09 | |||
| 5bcea2fe3b | |||
| 73afd1f5d0 | |||
| 9b8a0d3e54 | |||
| 75bd98d66c | |||
| 9b914d10bd | |||
| 3e11678d8d | |||
| 6e03638130 | |||
| 23f884efb7 | |||
| 2ae76f999b | |||
| 685325cdc7 | |||
| ab132eadb7 | |||
| 14504e43b5 | |||
| 4ed79574e8 | |||
| 31bcd1a386 | |||
| 270e5ef7ad | |||
| 1f4733355e | |||
| 027a859ef4 | |||
| 59f50eaee7 | |||
| 80d2733c25 | |||
| feb6735a4d | |||
| f1d1d2fd81 | |||
| a749c67803 | |||
| c808bb1483 | |||
| e24206ebf0 | |||
| 3410785a3a | |||
| b4a12d606e | |||
| a0545545d7 | |||
| 1f11650ca1 | |||
| da55facf2c | |||
| dc2dbc5710 | |||
| bdd6303bcc | |||
| 66d0fc0683 | |||
| 05e1d8cd96 | |||
| 9f0de74caa | |||
| a820739bd2 | |||
| 304042017c | |||
| 82cd9f2979 | |||
| a3f1fea27b | |||
| bb5c6a42e7 | |||
| 445baaaa61 | |||
| 7419ee7dba | |||
| aa6be381a4 | |||
| 61575838f3 | |||
| 359bb979f8 | |||
| b29a885bae | |||
| 919da7f2c9 | |||
| d222c116e0 | |||
| df840dac18 | |||
| f8d4c7697c | |||
| dddb4ecbb8 | |||
| 470d10cb47 | |||
| dae11e9607 | |||
| 3d1eb53708 | |||
| a76c9a56a7 | |||
| eaad0d4e09 | |||
| 46edde9da6 | |||
| dda0cc635e | |||
| 727bde2d16 | |||
| 9f30af1b33 | |||
| ff05e20537 | |||
| 53de5b13c1 | |||
| a025118f69 | |||
| 71688a817b | |||
| 00c476ea7f | |||
| 5576906914 | |||
| d5aaccd2c4 | |||
| 7059c2ba25 | |||
| aaab157d49 | |||
| 9943276c0c | |||
| c31cf5243a | |||
| 0c03676efb | |||
| 883d10acf8 | |||
| 3e5e458118 | |||
| 0510b6f29e | |||
| 92b7668bf3 | |||
| 3973b1cbf3 | |||
| 160fca1ea1 | |||
| e90c96f7e9 | |||
| d935fb1b2e | |||
| a6529584a5 | |||
| 18953b2d6b | |||
| 21cbd9e3b0 | |||
| 38061bdf65 | |||
| d9029c7e1e | |||
| fe8acc986e | |||
| e721f17f7e | |||
| 2f73db785e | |||
| 9e18ae6587 | |||
| 9eca010f94 | |||
| 6e54a06d9c | |||
| 1f8e416e41 | |||
| cfccc44406 | |||
| 45b30c9c27 | |||
| 67f78368fb | |||
| d9c8d57e8e | |||
| 0d615e3888 | |||
| 68287d3df4 | |||
| dff08ae3e3 | |||
| 8cfa67c1dd | |||
| b41d425993 | |||
| 12c7862250 | |||
| 6f54826296 | |||
| ccf2cffc4b | |||
| f7b801b4c8 | |||
| 95f2aa45eb | |||
| 9524ff5f5a | |||
| 1823101b03 | |||
| 1a8cfd2a8b | |||
| 5b4e710770 | |||
| 1762f85ef9 | |||
| d370a3f0e3 | |||
| d3e573c2e9 | |||
| 94876e3ad1 | |||
| ad190602c7 | |||
| 162b4b78b6 | |||
| e5dc05e994 | |||
| b46cff70c0 | |||
| b6c3870eda | |||
| 1c1f504601 | |||
| 1b8a45678c | |||
| 38de9eb9d7 | |||
| eac1314a7d | |||
| 29d59793dc | |||
| 128ad20280 | |||
| 994ebff8c1 | |||
| 142938401c | |||
| 65cc4afb23 | |||
| e000f23c44 | |||
| 1de9d1aa33 | |||
| 0e601e277b | |||
| e582a47f37 | |||
| 1b251a78b0 | |||
| 24f5249ae9 | |||
| 17c1631013 | |||
| cb100d5e00 | |||
| 1fcc807513 | |||
| 5664e5a30f | |||
| 26e64019d8 | |||
| 56331d2e74 | |||
| 55b095e503 | |||
| 6a073e1dd5 | |||
| 960e25a3f6 | |||
| 1de338c312 | |||
| dbba50cea4 | |||
| 8f046a9cd6 | |||
| 2906e6038a | |||
| 87b8ea8c05 | |||
| 47971dce31 | |||
| 528a05398c | |||
| f584148ab9 | |||
| 0de22f7a96 | |||
| 09c7c30779 | |||
| fde32e26bf | |||
| a6f65916f1 | |||
| 6059efe094 | |||
| 16194c50ba | |||
| af3c6a971c | |||
| ecfdb976a3 | |||
| 08acea651a | |||
| 6b503608cd | |||
| d8763997a5 | |||
| 32aa1f99c1 | |||
| 718a8a54b1 | |||
| 741dd490d4 | |||
| 02d876e6d8 | |||
| 56c52e0bb0 | |||
| 6036becf93 | |||
| 3a7210d514 | |||
| 6088fa582d | |||
| 6691492ef7 | |||
| 7ee9158cd3 | |||
| 5ae1b883ab | |||
| 88397bf419 | |||
| 1138d5935b | |||
| 8276ba080e | |||
| 37d13d80f5 | |||
| 1c97536ab1 | |||
| ca8f028cc0 | |||
| 300f7bfe4b | |||
| 82dc9c40f7 | |||
| 44a20fe5ee | |||
| b950a64406 | |||
| c5f374f66f | |||
| ae99f87db0 | |||
| 5112e3e79b | |||
| c1e27ccacc | |||
| b0bd871997 | |||
| c30bc24fd6 | |||
| 648f0e9e84 | |||
| d2bc5672e0 | |||
| 5c87ec7c2f | |||
| d66c184ed2 | |||
| 12f25d8bfc | |||
| cbe61397a3 | |||
| deb8c54da3 | |||
| 5dab70fab4 | |||
| 665b6bed66 | |||
| f4a90defc3 | |||
| 46f63cbd55 | |||
| c77142a16f | |||
| eb7c22f52a | |||
| e86e51f06f | |||
| db74ea3001 | |||
| 99579ac29c | |||
| 2f6540e008 | |||
| 72c3b80529 | |||
| 8126a83c50 | |||
| 210c432b37 | |||
| f92e90603f | |||
| 14172791e9 | |||
| e4e2f524d4 | |||
| fc7b16f6a7 | |||
| 6ba3ae74d5 | |||
| c71d1598d8 | |||
| 5950657b0e | |||
| 0e1a48cd06 | |||
| 98245e5f20 | |||
| 0b6ddca981 | |||
| ea1468c892 | |||
| e8e64bf510 | |||
| aaf8daf0d7 | |||
| 8af1abc4e1 | |||
| 48ab59feb3 | |||
| f64d5073d4 | |||
| 53b9f10ac8 | |||
| 75606e4df7 | |||
| 0643b7f351 | |||
| 5b3b4e0614 | |||
| 8d039d1348 | |||
| 87eea73081 | |||
| 67a6fb4c87 | |||
| 23584c9862 | |||
| 2f767125c4 | |||
| c73e07d42d | |||
| 6b5d7e6057 | |||
| 076dc48e89 | |||
| bb208ab36a | |||
| 60e35001b6 | |||
| 29a0d7ea02 | |||
| 1f2b7f7acf | |||
| 39e5cb63ef | |||
| 5b5928fb8d | |||
| 08068743a5 | |||
| 35eae1adcf | |||
| e85d357abd | |||
| 31396d8109 | |||
| 664debc0bc | |||
| 56c31c76b3 | |||
| 2d1a71f3d1 | |||
| c67cd73058 | |||
| 2572cb4352 | |||
| c9f6c7ccac | |||
| b6520b0dbc | |||
| 9fe8142117 | |||
| 57cdd25b5f | |||
| 1005ee5d52 | |||
| d9bb9f9a07 | |||
| 75a6fabe61 | |||
| 75e29b69cc | |||
| ffd3937538 | |||
| 575587e4c5 | |||
| 2ce59d7c92 | |||
| 3b3d6e2a73 | |||
| f2a467fdbf | |||
| 867481c66d | |||
| fc8065b7a8 | |||
| 28f1ed9205 | |||
| 568266bc9a | |||
| 7739f341b8 | |||
| 2b501f191c | |||
| aa2ed0fda1 | |||
| 51cba074d7 | |||
| 2c945c663c | |||
| d9a18ecabb | |||
| eab7931afb | |||
| 99b6c92377 | |||
| 4d56f1163b | |||
| 07cf835551 | |||
| 061c0c1a90 | |||
| 1ed18c05ef | |||
| ca134c8a60 | |||
| 7ba67fd852 | |||
| 7675c2c411 | |||
| 7c71363af8 | |||
| d48926eb2a | |||
| d843e5e06f | |||
| a2e564738e | |||
| 54a797a0b1 | |||
| a78c1ecb4d | |||
| 4771c78001 | |||
| b7a5106f36 | |||
| 4bcbdc50ff | |||
| 5d72b35c7f | |||
| ff50ac215c | |||
| 3fe3809416 | |||
| 363d337a72 | |||
| f89b31c940 | |||
| 630c3e84dd | |||
| d43202f137 | |||
| 0964043489 | |||
| e9b31a545d | |||
| 7f28a88374 | |||
| 07ea45bf84 | |||
| a86d35f721 | |||
| d6c28a0ff6 | |||
| a7af3ca7bc | |||
| cda08a9843 | |||
| 7588774237 | |||
| 683373b263 | |||
| 673ec8fb5d | |||
| 63bacaa671 | |||
| e3a3d6a505 | |||
| 175171dda1 | |||
| a4b63b88c8 | |||
| c3675fdfe7 | |||
| 2fa08519b6 | |||
| 065627060b | |||
| 2982b6a808 | |||
| c3f56ecd40 | |||
| fe9e6c1afd | |||
| 73394ae929 | |||
| c754fc9ff4 | |||
| 1ca6208057 | |||
| cdfae4d4df | |||
| c3da5469ca | |||
| 79f2d8ba76 | |||
| 7b1bdb92a6 | |||
| f9a075eed1 | |||
| 32e4193557 | |||
| 133c3e7c0a | |||
| 6ce3cea682 | |||
| 250e77d18b | |||
| e3bfe006e9 | |||
| 76aeecdc68 | |||
| 15d2ee3e76 | |||
| b066db479f | |||
| a69ff2789a | |||
| 4df235d64a | |||
| 11524457e3 | |||
| 8f7364293e | |||
| 56f5fa1fbe | |||
| b075f0f7e9 | |||
| 0ca32afe8d | |||
| 32c2f257a9 | |||
| a7066ccbe1 | |||
| 09248e0efe | |||
| e2ef22fc5f | |||
| 75f825e4f8 | |||
| 9b8d5198e4 | |||
| e09f747727 | |||
| 16e6171750 | |||
| f885e887ea | |||
| 5096f7fbc5 | |||
| f16560c1ee | |||
| d0f4860759 | |||
| 34d36c6de1 | |||
| 042b850b5d | |||
| 73892f4fc2 | |||
| 636b868bf1 | |||
| b144368e3f | |||
| 683b4193d1 | |||
| f4c953665a | |||
| 0f990c772a | |||
| 0bf985af61 | |||
| 42473611d3 | |||
| 2c437f7d94 | |||
| aa8c49eb0c | |||
| b32e40c72a | |||
| 2ff7ed1b52 | |||
| bfbed1a69b | |||
| ef9d012be9 | |||
| f12a2155c0 | |||
| 43f696c4a8 | |||
| 4c19014dcd | |||
| 0e7f9d6778 | |||
| 00a0bf3c1b | |||
| e9149036f6 | |||
| b6a9967959 | |||
| 8bbc3caf3e | |||
| 237b703a6c | |||
| cf3045a718 | |||
| a679e7283f | |||
| 13c75341e2 | |||
| 4cbcc5bb20 | |||
| 51955dae3b | |||
| 51fa3ab2c8 | |||
| af9e66e87e | |||
| 8878dc7f80 | |||
| 4336bc20e0 | |||
| 4a7613a521 | |||
| 689f55340f | |||
| 160ae3d49d | |||
| c079c3c6d9 | |||
| cb782a7557 | |||
| 502d8fc087 | |||
| 6295881138 | |||
| 17f71ba680 | |||
| 5fe7adb87b | |||
| 41684d091b | |||
| 9f6c3347cb | |||
| aa321989c4 | |||
| 0f1565cb4b | |||
| 3996110ebd | |||
| 3422ad4c84 | |||
| 3c07e65b9a | |||
| 287df91661 | |||
| 776bc3135a | |||
| 4638958c9f | |||
| 1f24fe46c4 | |||
| ccbcab8822 | |||
| 094e9cdac2 | |||
| 5c35f36b3a | |||
| d7ec2b57a4 | |||
| 373a1e0312 | |||
| e9fe3975bb | |||
| 32a73b55d4 | |||
| 0426312d6c | |||
| ede2335fb3 | |||
| 709f1a1ccb | |||
| 24b785cbfb | |||
| acc3f2f418 | |||
| d4a653d6a9 | |||
| 7e74d25447 | |||
| 3445db41ce | |||
| d1bd02a6f5 | |||
| 24cd72e088 | |||
| 47f7558c38 | |||
| de747b7b33 | |||
| 8e8b73fbd9 | |||
| e966e33e8f | |||
| bd0ca5babd | |||
| f6bacea1ae | |||
| ba2a523722 | |||
| 57adb3e54a | |||
| 7377928c8a | |||
| 5f8a8fe9b1 | |||
| 29a0bf25c4 | |||
| 92bb016576 | |||
| c8810ea002 | |||
| aa8bbaddc4 | |||
| b8c136e9c0 | |||
| 46d54172b0 | |||
| 68dc2de190 | |||
| 81b372f372 | |||
| 966e520640 | |||
| 12702930ec | |||
| 6ce57bcda1 | |||
| d99b453bc4 | |||
| aaf5793cb7 | |||
| 9b1762d2ab | |||
| 8d38d000fd | |||
| 4cf9cdd56d | |||
| 39406bbf54 | |||
| 9c5c1e5b68 | |||
| 6929b0b98b | |||
| 7c26d491b1 | |||
| 97035a054a | |||
| db45c6d773 | |||
| e3f5f81e62 |
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
# Configurations
|
||||
|
||||
### API ###
|
||||
|
||||
## API token to use when looking up the published content
|
||||
# GITBOOK_API_URL=https://api.gitbook.com
|
||||
# GITBOOK_TOKEN=xxx
|
||||
|
||||
## User agent to use when making requests to the API
|
||||
# GITBOOK_USER_AGENT=GitBook/1.0.0
|
||||
# GITBOOK_USER_AGENT_COMMENT=something
|
||||
|
||||
### URL of the application ###
|
||||
# NEXT_PUBLIC_GITBOOK_APP_URL=https://app.gitbook.com
|
||||
|
||||
### Image resizing ###
|
||||
# GITBOOK_IMAGE_RESIZE_SIGNING_KEY=1234567890
|
||||
# GITBOOK_IMAGE_RESIZE_URL=https://mycompany.com/cdn-cgi/image/
|
||||
|
||||
### SEO ###
|
||||
# GITBOOK_BLOCK_SEARCH_INDEXATION=true
|
||||
|
||||
## Caching
|
||||
# GITBOOK_OUTPUT_CACHE=true
|
||||
|
||||
### Sentry ###
|
||||
# SENTRY_DSN=xxx
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"plugins": ["import"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"groups": ["builtin", "external", ["internal", "parent", "sibling", "index"]],
|
||||
"newlines-between": "always",
|
||||
"distinctGroup": true,
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "@gitbook/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}
|
||||
],
|
||||
"alphabetize": {
|
||||
"order": "asc",
|
||||
"caseInsensitive": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"@next/next/no-img-element": ["off"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Changes to the API data cache functions can invalidate all existing data cache
|
||||
# causing a massive amount of revalidation, impacting our API.
|
||||
packages/gitbook/src/lib/data/api.ts @SamyPesse
|
||||
+44
-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,48 @@ 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. Start the development server:
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
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/url`.
|
||||
|
||||
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/url/gitbook.com/docs` after running the development server.
|
||||
|
||||
You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
|
||||
|
||||
### Commit your update
|
||||
|
||||
@@ -71,4 +108,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/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/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||
- id: deploy_middleware
|
||||
name: Deploy middleware to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Deploy server to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
- name: 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: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/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/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
- name: Extract server version worker ID
|
||||
shell: bash
|
||||
id: extract_server_version_id
|
||||
run: |
|
||||
version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run updateWrangler scripts
|
||||
shell: bash
|
||||
run: |
|
||||
bun run ./packages/gitbook/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||
|
||||
- id: upload_middleware
|
||||
name: Upload middleware to Cloudflare
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Extract middleware version worker ID
|
||||
shell: bash
|
||||
id: extract_middleware_version_id
|
||||
run: |
|
||||
version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Deploy server and middleware to Cloudflare
|
||||
if: ${{ inputs.deploy == 'true' }}
|
||||
uses: ./.github/actions/gradual-deploy-cloudflare
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
opServiceAccount: ${{ inputs.opServiceAccount }}
|
||||
opItem: ${{ inputs.opItem }}
|
||||
environment: ${{ inputs.environment }}
|
||||
serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||
middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
|
||||
deploy: ${{ inputs.deploy }}
|
||||
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
|
||||
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
|
||||
@@ -0,0 +1,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'
|
||||
+31
-137
@@ -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.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Cache Next.js build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.next/cache
|
||||
# Generate a new cache whenever packages or source files change.
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-
|
||||
- name: Sets env vars for production
|
||||
run: |
|
||||
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
|
||||
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
|
||||
if: startsWith(github.ref, 'refs/heads/main')
|
||||
- name: Sets env vars for preview
|
||||
run: |
|
||||
echo "SENTRY_ENVIRONMENT=preview" >> $GITHUB_ENV
|
||||
if: 1 && !startsWith(github.ref, 'refs/heads/main')
|
||||
- name: Build Next.js with next-on-pages
|
||||
run: bun run build:cloudflare
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
||||
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
- id: cloudflare
|
||||
name: Publish to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: ./.vercel/output/static
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
workingDirectory: ./
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "ID: ${{ steps.cloudflare.outputs.id }}"
|
||||
echo "URL: ${{ steps.cloudflare.outputs.url }}"
|
||||
echo "Environment: ${{ steps.cloudflare.outputs.environment }}"
|
||||
echo "Alias: ${{ steps.cloudflare.outputs.alias }}"
|
||||
- name: Archive build output
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-output
|
||||
path: .vercel/
|
||||
visual-testing:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing
|
||||
needs: deploy
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Run Playwright tests
|
||||
run: bun e2e
|
||||
env:
|
||||
BASE_URL: ${{needs.deploy.outputs.deployment_url}}
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
pagespeed-testing:
|
||||
runs-on: ubuntu-latest
|
||||
name: PageSpeed Testing
|
||||
needs: deploy
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Run pagespeed tests
|
||||
run: bun ./tests/pagespeed-testing.ts $DEPLOYMENT_URL
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{needs.deploy.outputs.deployment_url}}
|
||||
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
|
||||
format:
|
||||
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.0.33
|
||||
- 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.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun lint --no-cache
|
||||
- run: bun format:check
|
||||
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.0.33
|
||||
- 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.0.33
|
||||
- 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,202 @@
|
||||
name: Preview
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
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-v2-vercel
|
||||
- deploy-v2-cloudflare
|
||||
steps:
|
||||
- name: Find GitHub Comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: 'Summary of the deployments'
|
||||
|
||||
- name: Create or update GitHub comment
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
Summary of the deployments:
|
||||
|
||||
| Version | URL | Status |
|
||||
| --- | --- | --- |
|
||||
| Vercel | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}](${{ needs.deploy-v2-vercel.outputs.deployment-url }}) | ${{ needs.deploy-v2-vercel.result == 'success' && '✅' || '❌' }} |
|
||||
| Cloudflare | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v2-cloudflare.result == 'success' && '✅' || '❌' }} |
|
||||
|
||||
### Test content
|
||||
|
||||
| Site | `2v` | `2c` |
|
||||
| --- | --- | --- |
|
||||
| GitBook | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs) |
|
||||
| E2E | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
|
||||
edit-mode: replace
|
||||
visual-testing-v2-vercel:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2
|
||||
needs: deploy-v2-vercel
|
||||
timeout-minutes: 15
|
||||
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: 15
|
||||
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-v2:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2
|
||||
needs: deploy-v2-vercel
|
||||
timeout-minutes: 15
|
||||
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: 15
|
||||
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-v2:
|
||||
runs-on: ubuntu-latest
|
||||
name: PageSpeed Testing v1
|
||||
needs: deploy-v2-vercel
|
||||
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-v2-vercel.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 }}"
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
||||
fetch-depth: 0
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
publish: npm run release
|
||||
version: npm run changeset-version
|
||||
env:
|
||||
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
|
||||
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
+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 +0,0 @@
|
||||
.next
|
||||
@@ -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> - <a href="https://github.com/orgs/GitbookIO/discussions/categories/feature-requests">Feature requests</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](https://bun.sh/) (Version: >=1.2.15)
|
||||
- We use a text-based lockfile which isn't supported below 1.2.15
|
||||
|
||||
### Set up
|
||||
|
||||
@@ -46,32 +48,56 @@ 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. Start your local development server.
|
||||
|
||||
```
|
||||
bun dev
|
||||
```
|
||||
|
||||
Then open the space in your web browser, using http://localhost:3000/<host>/<path> (example: http://localhost:3000/docs.gitbook.com).
|
||||
6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/url`.
|
||||
|
||||
### Other development commands
|
||||
examples:
|
||||
|
||||
- `bun format`: format the code
|
||||
- `bun lint`: lint the code
|
||||
- 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 +105,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 +116,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 +132,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/gitbook/worker-configuration.d.ts",
|
||||
"**/*.css"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"useEditorconfig": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 100,
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"performance": {
|
||||
"noDelete": "warn"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"noUselessFragments": "warn",
|
||||
"noBannedTypes": "warn"
|
||||
},
|
||||
"correctness": {
|
||||
"noUndeclaredVariables": "error",
|
||||
"noUnusedVariables": "error",
|
||||
"useArrayLiterals": "error",
|
||||
"useHookAtTopLevel": "error",
|
||||
"noUnusedImports": "error",
|
||||
"noVoidElementsWithChildren": "warn",
|
||||
"useJsxKeyInIterable": "warn",
|
||||
"useExhaustiveDependencies": "warn",
|
||||
"noUnknownFunction": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "warn",
|
||||
"noParameterAssign": "off",
|
||||
"useThrowOnlyError": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noConsole": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"allow": ["assert", "error", "warn"]
|
||||
}
|
||||
},
|
||||
"noExplicitAny": "warn",
|
||||
"noImplicitAnyLet": "warn",
|
||||
"noConfusingVoidType": "warn",
|
||||
"noControlCharactersInRegex": "warn",
|
||||
"noPrototypeBuiltins": "warn",
|
||||
"noAssignInExpressions": "warn",
|
||||
"noArrayIndexKey": "warn"
|
||||
},
|
||||
"a11y": {
|
||||
"useSemanticElements": "warn",
|
||||
"useKeyWithClickEvents": "warn",
|
||||
"noSvgWithoutTitle": "warn",
|
||||
"useButtonType": "warn",
|
||||
"useIframeTitle": "warn",
|
||||
"useAltText": "warn",
|
||||
"noPositiveTabindex": "warn",
|
||||
"useFocusableInteractive": "warn",
|
||||
"useAriaPropsForRole": "warn",
|
||||
"useValidAnchor": "warn",
|
||||
"noLabelWithoutControl": "warn",
|
||||
"noNoninteractiveTabindex": "warn"
|
||||
},
|
||||
"nursery": {
|
||||
"useSortedClasses": {
|
||||
"level": "error",
|
||||
"fix": "safe",
|
||||
"options": {
|
||||
"attributes": ["class", "className", "style"],
|
||||
"functions": ["clsx", "tw"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "es5",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": [
|
||||
"packages/gitbook/**/*",
|
||||
"packages/react-openapi/**/*",
|
||||
"packages/react-math/**/*",
|
||||
"packages/react-contentkit/**/*",
|
||||
"packages/icons/**/*"
|
||||
],
|
||||
"javascript": {
|
||||
"globals": ["React"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["packages/gitbook/**/*"],
|
||||
"javascript": {
|
||||
"globals": ["React", "GitBookIntegrationEvent"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.css"],
|
||||
"javascript": {
|
||||
"globals": ["theme"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.test.ts", "packages/gitbook/tests/**/*"],
|
||||
"javascript": {
|
||||
"globals": ["Bun"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"packages/cache-do/**/*",
|
||||
"packages/gitbook/cf-env.d.ts",
|
||||
"packages/gitbook/src/cloudflare-entrypoint.ts"
|
||||
],
|
||||
"javascript": {
|
||||
"globals": [
|
||||
"DurableObjectLocationHint",
|
||||
"DurableObjectNamespace",
|
||||
"DurableObjectStub",
|
||||
"ContinentCode",
|
||||
"Fetcher",
|
||||
"ExportedHandler"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,576 +0,0 @@
|
||||
import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import {
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationLocale,
|
||||
CustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import rison from 'rison';
|
||||
|
||||
import { getContentTestURL } from '../tests/utils';
|
||||
|
||||
interface Test {
|
||||
name: string;
|
||||
url: string;
|
||||
run?: (page: Page) => Promise<unknown>;
|
||||
fullPage?: boolean;
|
||||
screenshot?: false;
|
||||
}
|
||||
|
||||
interface TestsCase {
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
tests: Array<Test>;
|
||||
}
|
||||
|
||||
const allLocales: CustomizationLocale[] = [
|
||||
CustomizationLocale.Fr,
|
||||
CustomizationLocale.Es,
|
||||
CustomizationLocale.Ja,
|
||||
CustomizationLocale.Zh,
|
||||
];
|
||||
|
||||
async function waitForCookiesDialog(page: Page) {
|
||||
const dialog = page.getByRole('dialog', { name: 'Cookies' });
|
||||
const accept = dialog.getByRole('button', { name: 'Accept' });
|
||||
const reject = dialog.getByRole('button', { name: 'Reject' });
|
||||
await expect(accept).toBeVisible();
|
||||
await expect(reject).toBeVisible();
|
||||
}
|
||||
|
||||
const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'GitBook Site (Single Variant)',
|
||||
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Home',
|
||||
url: '',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'No variants dropdown',
|
||||
url: '',
|
||||
run: async (page) => {
|
||||
await expect(page.locator('[data-testid="space-dropdown-button"]')).toHaveCount(
|
||||
0,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
url: '?q=',
|
||||
},
|
||||
{
|
||||
name: 'Search Results',
|
||||
url: '?q=gitbook',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-test="search-results"]');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AI Search',
|
||||
url: '?q=What+is+GitBook%3F&ask=true',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-test="search-ask-answer"]');
|
||||
},
|
||||
screenshot: false,
|
||||
},
|
||||
{
|
||||
name: 'Not found',
|
||||
url: 'content-not-found',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'GitBook Site (Multi Variants)',
|
||||
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/multi-variants/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Variants dropdown',
|
||||
url: '',
|
||||
run: async (page) => {
|
||||
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
|
||||
await spaceDrowpdown.waitFor();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Default variant',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
name: 'RFC variant',
|
||||
url: 'v/rfcs',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'GitBook',
|
||||
baseUrl: 'https://docs.gitbook.com',
|
||||
tests: [
|
||||
{
|
||||
name: 'Home',
|
||||
url: '',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
url: '?q=',
|
||||
},
|
||||
{
|
||||
name: 'Search Results',
|
||||
url: '?q=gitbook',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-test="search-results"]');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AI Search',
|
||||
url: '?q=What+is+GitBook%3F&ask=true',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-test="search-ask-answer"]');
|
||||
},
|
||||
screenshot: false,
|
||||
},
|
||||
{
|
||||
name: 'Not found',
|
||||
url: 'content-not-found',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Versioning',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Revision',
|
||||
url: '~/revisions/S55pwsEr5UVoroaOiWnP/blocks/headings',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'PDF',
|
||||
url: '~gitbook/pdf?limit=10',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Content tests',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Text',
|
||||
url: 'text-page',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Long text',
|
||||
url: 'text-page/long-text',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Images',
|
||||
url: 'blocks/block-images',
|
||||
run: waitForCookiesDialog,
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Inline Images',
|
||||
url: 'blocks/inline-images',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Tabs',
|
||||
url: 'blocks/tabs',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Hints',
|
||||
url: 'blocks/hints',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Integration Blocks',
|
||||
url: 'blocks/integrations',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Tables',
|
||||
url: 'blocks/tables',
|
||||
run: waitForCookiesDialog,
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Expandables',
|
||||
url: 'blocks/expandables',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'API Blocks',
|
||||
url: 'blocks/api-blocks',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Headings',
|
||||
url: 'blocks/headings',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Marks',
|
||||
url: 'blocks/marks',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Emojis',
|
||||
url: 'blocks/emojis',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Links',
|
||||
url: 'blocks/links',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Lists',
|
||||
url: 'blocks/lists',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Code',
|
||||
url: 'blocks/code',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Cards',
|
||||
url: 'blocks/cards',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Math',
|
||||
url: 'blocks/math',
|
||||
},
|
||||
{
|
||||
name: 'Embeds',
|
||||
url: 'blocks/embeds',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Annotations',
|
||||
url: 'blocks/annotations',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-testid="annotation-button"]');
|
||||
await page.click('[data-testid="annotation-button"]');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Page options',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'With cover',
|
||||
url: 'page-options/page-with-cover',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'With hero cover',
|
||||
url: 'page-options/page-with-hero-cover',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'With cover and no TOC',
|
||||
url: 'page-options/page-with-cover-and-no-toc',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Customization',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Without header',
|
||||
url: getCustomizationURL({
|
||||
header: {
|
||||
preset: CustomizationHeaderPreset.None,
|
||||
links: [],
|
||||
},
|
||||
}),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Share links',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-share-links/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Valid link',
|
||||
url: 'Fc6mMII9FKgnwm7qqynx/',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Invalid link',
|
||||
url: 'invalid/',
|
||||
run: async (page) => {
|
||||
await expect(
|
||||
page.getByText('Authentication missing to access this content'),
|
||||
).toBeVisible();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Visitor Auth - Space',
|
||||
baseUrl: `https://gitbook.gitbook.io/gbo-va-space/`,
|
||||
tests: [
|
||||
{
|
||||
name: 'First',
|
||||
url: (() => {
|
||||
const privateKey = '70b844d0-c519-4532-8586-5970ce48c537';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `first?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Second',
|
||||
url: (() => {
|
||||
const privateKey = '70b844d0-c519-4532-8586-5970ce48c537';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `second?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Visitor Auth - Collection',
|
||||
baseUrl: `https://gitbook.gitbook.io/gbo-va-collection/`,
|
||||
tests: [
|
||||
{
|
||||
name: 'Root',
|
||||
url: (() => {
|
||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Primary (Space A)',
|
||||
url: (() => {
|
||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `v/spacea?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Space B',
|
||||
url: (() => {
|
||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `v/spaceb?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Space C',
|
||||
url: (() => {
|
||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `v/spacec?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Visitor Auth - Space (custom domain)',
|
||||
baseUrl: `https://test.gitbook.community/`,
|
||||
tests: [
|
||||
{
|
||||
name: 'Root',
|
||||
url: (() => {
|
||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'First',
|
||||
url: (() => {
|
||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `first?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Custom page',
|
||||
url: (() => {
|
||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `custom-page?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Inner page',
|
||||
url: (() => {
|
||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `custom-page/inner-page?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Languages',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: allLocales.map((locale) => ({
|
||||
name: locale,
|
||||
url: getCustomizationURL({
|
||||
internationalization: {
|
||||
locale,
|
||||
inherit: false,
|
||||
},
|
||||
}),
|
||||
run: async (page) => {
|
||||
const dialog = page.getByTestId('cookies-dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
test.describe(testCase.name, () => {
|
||||
for (const testEntry of testCase.tests) {
|
||||
test(testEntry.name, async ({ page, baseURL }) => {
|
||||
const contentUrl = new URL(testEntry.url, testCase.baseUrl);
|
||||
const url = getContentTestURL(contentUrl.toString(), baseURL);
|
||||
await page.goto(url);
|
||||
if (testEntry.run) {
|
||||
await testEntry.run(page);
|
||||
}
|
||||
if (testEntry.screenshot !== false) {
|
||||
await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, {
|
||||
viewports: ['macbook-16', 'macbook-13', 'iphone-x', 'ipad-2'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
fullPage: testEntry.fullPage ?? false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a URL with customization settings.
|
||||
*/
|
||||
function getCustomizationURL(partial: Partial<CustomizationSettings>): string {
|
||||
const encoded = rison.encode_object(partial);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('customization', encoded);
|
||||
|
||||
return `?${searchParams.toString()}`;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
const { withSentryConfig } = require('@sentry/nextjs');
|
||||
|
||||
module.exports = withSentryConfig(
|
||||
{
|
||||
env: {
|
||||
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
|
||||
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
|
||||
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
|
||||
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
},
|
||||
|
||||
webpack(config) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
|
||||
// Required for `swagger2openapi` to work:
|
||||
fs: false,
|
||||
path: false,
|
||||
http: false,
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
async headers() {
|
||||
return [
|
||||
// Cache all static assets for 1 year
|
||||
{
|
||||
source: '/~gitbook/static/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'public, max-age=31536000, immutable',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
poweredByHeader: false,
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.gitbook.io',
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
silent: true,
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
},
|
||||
{
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
transpileClientSDK: false,
|
||||
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
|
||||
tunnelRoute: '/~gitbook/monitoring',
|
||||
// Don't hide source maps from generated client bundles
|
||||
hideSourceMaps: false,
|
||||
disableLogger: true,
|
||||
automaticVercelMonitors: false,
|
||||
},
|
||||
);
|
||||
+35
-83
@@ -1,92 +1,44 @@
|
||||
{
|
||||
"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",
|
||||
"esbuild": "0.24.2"
|
||||
},
|
||||
"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",
|
||||
"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"
|
||||
"dev": "turbo run dev",
|
||||
"build": "turbo run build",
|
||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"format": "biome check --write ./",
|
||||
"format:check": "biome check --diagnostic-level=error ./",
|
||||
"unit": "turbo run unit",
|
||||
"e2e": "turbo run e2e",
|
||||
"e2e-customers": "turbo run e2e-customers",
|
||||
"changeset": "changeset",
|
||||
"changeset-version": "changeset version && bun run format",
|
||||
"release": "turbo run release && changeset publish",
|
||||
"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.51.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.94.1",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@upstash/redis": "^1.27.1",
|
||||
"ajv": "^8.12.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.0.7",
|
||||
"classnames": "^2.5.1",
|
||||
"content-security-policy-merger": "^1.0.0",
|
||||
"framer-motion": "^10.16.14",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsontoxml": "^1.0.1",
|
||||
"katex": "^0.16.9",
|
||||
"mathjax": "^3.2.2",
|
||||
"memoizee": "^0.4.15",
|
||||
"next": "^14.2.4",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^1.15.4",
|
||||
"object-hash": "^3.0.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-map": "^7.0.0",
|
||||
"parse-cache-control": "^1.0.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "^1.2.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.4",
|
||||
"url-join": "^5.0.0"
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.130.0"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^2.0.0",
|
||||
"@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 @@
|
||||
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,213 @@
|
||||
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;
|
||||
translation: string;
|
||||
}
|
||||
): string {
|
||||
switch (spec.tag) {
|
||||
case 'user':
|
||||
return `user:${spec.user}`;
|
||||
case 'url':
|
||||
return `url:${spec.hostname}`;
|
||||
case 'space':
|
||||
return `space:${spec.space}`;
|
||||
case 'change-request':
|
||||
return `space:${spec.space}:change-request:${spec.changeRequest}`;
|
||||
case 'revision':
|
||||
return `space:${spec.space}:revision:${spec.revision}`;
|
||||
case 'document':
|
||||
return `space:${spec.space}:document:${spec.document}`;
|
||||
case 'computed-document':
|
||||
return `space:${spec.space}:computed-document:${spec.sourceType}`;
|
||||
case 'site':
|
||||
return `site:${spec.site}`;
|
||||
case 'integration':
|
||||
return `integration:${spec.integration}`;
|
||||
case 'openapi':
|
||||
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
|
||||
case 'translation':
|
||||
return `organization:${spec.organization}:translation:${spec.translation}`;
|
||||
default:
|
||||
assertNever(spec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 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[] = [];
|
||||
|
||||
if (!('dependencies' in source)) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
// We add the dependencies as tags, to ensure that the computed content is invalidated
|
||||
// when the dependencies are updated.
|
||||
const dependencies = Object.values(source.dependencies ?? {});
|
||||
if (dependencies.length > 0) {
|
||||
dependencies.forEach((dependency) => {
|
||||
switch (dependency.ref.kind) {
|
||||
case 'space':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
space: dependency.ref.space,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'openapi':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'openapi',
|
||||
organization: inContext.organizationId,
|
||||
openAPISpec: dependency.ref.spec,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'translation':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'translation',
|
||||
organization: inContext.organizationId,
|
||||
translation: dependency.ref.translation,
|
||||
})
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Do not throw for unknown dependency types
|
||||
// as it might mean we are 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/
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# @gitbook/emoji-codepoints
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 57adb3e: Second release to fix publishing with changeset
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5f8a8fe: Initial release
|
||||
@@ -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)};`
|
||||
);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"name": "@gitbook/emoji-codepoints",
|
||||
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"exports": "./index.ts",
|
||||
"exports": "./dist/index.ts",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"emoji-assets": "^8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "bun ./build.ts"
|
||||
"generate": "bun ./build.ts",
|
||||
"clean": "rm -rf ./dist"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
src/data/*.json
|
||||
@@ -0,0 +1,7 @@
|
||||
# @gitbook/fonts
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- fbfcca5: Initial version of the package
|
||||
@@ -0,0 +1,3 @@
|
||||
# `@gitbook/fonts`
|
||||
|
||||
Utilities to lookup default fonts supported by GitBook.
|
||||
@@ -0,0 +1,91 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { APIv2 } from 'google-font-metadata';
|
||||
|
||||
import { CustomizationDefaultFont } from '@gitbook/api';
|
||||
|
||||
import type { FontDefinitions } from '../src/types';
|
||||
|
||||
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
|
||||
[CustomizationDefaultFont.Inter]: 'inter',
|
||||
[CustomizationDefaultFont.FiraSans]: 'fira-sans-extra-condensed',
|
||||
[CustomizationDefaultFont.IBMPlexSerif]: 'ibm-plex-serif',
|
||||
[CustomizationDefaultFont.Lato]: 'lato',
|
||||
[CustomizationDefaultFont.Merriweather]: 'merriweather',
|
||||
[CustomizationDefaultFont.NotoSans]: 'noto-sans',
|
||||
[CustomizationDefaultFont.OpenSans]: 'open-sans',
|
||||
[CustomizationDefaultFont.Overpass]: 'overpass',
|
||||
[CustomizationDefaultFont.Poppins]: 'poppins',
|
||||
[CustomizationDefaultFont.Raleway]: 'raleway',
|
||||
[CustomizationDefaultFont.Roboto]: 'roboto',
|
||||
[CustomizationDefaultFont.RobotoSlab]: 'roboto-slab',
|
||||
[CustomizationDefaultFont.SourceSansPro]: 'source-sans-3',
|
||||
[CustomizationDefaultFont.Ubuntu]: 'ubuntu',
|
||||
[CustomizationDefaultFont.ABCFavorit]: 'inter',
|
||||
};
|
||||
|
||||
/**
|
||||
* Scripts to generate the list of all icons.
|
||||
*/
|
||||
async function main() {
|
||||
// @ts-expect-error - we build the object
|
||||
const output: FontDefinitions = {};
|
||||
|
||||
for (const font of Object.values(CustomizationDefaultFont)) {
|
||||
const googleFontName = googleFontsMap[font];
|
||||
const fontMetadata = APIv2[googleFontName.toLowerCase()];
|
||||
if (!fontMetadata) {
|
||||
throw new Error(`Font ${googleFontName} not found`);
|
||||
}
|
||||
|
||||
output[font] = {
|
||||
font: googleFontName,
|
||||
unicodeRange: fontMetadata.unicodeRange,
|
||||
variants: {
|
||||
'400': {},
|
||||
'700': {},
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(output[font].variants).forEach((weight) => {
|
||||
const variants = fontMetadata.variants[weight];
|
||||
const normalVariant = variants.normal;
|
||||
if (!normalVariant) {
|
||||
throw new Error(`Font ${googleFontName} has no normal variant`);
|
||||
}
|
||||
|
||||
output[font].variants[weight] = {};
|
||||
Object.entries(normalVariant).forEach(([script, url]) => {
|
||||
output[font].variants[weight][script] = url.url.woff;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await writeDataFile('fonts', JSON.stringify(output, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* We write both in dist and src as the build process might have happen already
|
||||
* and tsc doesn't copy the files.
|
||||
*/
|
||||
async function writeDataFile(name, content) {
|
||||
const srcData = path.resolve(__dirname, '../src/data');
|
||||
const distData = path.resolve(__dirname, '../dist/data');
|
||||
|
||||
// Ensure the directories exists
|
||||
await Promise.all([
|
||||
fs.mkdir(srcData, { recursive: true }),
|
||||
fs.mkdir(distData, { recursive: true }),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
fs.writeFile(path.resolve(srcData, `${name}.json`), content),
|
||||
fs.writeFile(path.resolve(distData, `${name}.json`), content),
|
||||
]);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`Error generating icons list: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@gitbook/fonts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"google-font-metadata": "^6.0.3",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "bun ./bin/generate.js",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w",
|
||||
"clean": "rm -rf ./dist && rm -rf ./src/data",
|
||||
"unit": "bun test"
|
||||
},
|
||||
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getDefaultFont should return correct object for Latin text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should return correct object for Cyrillic text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should return correct object for Greek text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle mixed script text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different font weights: regular 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different font weights: bold 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different fonts: inter 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different fonts: roboto 1`] = `
|
||||
{
|
||||
"font": "Roboto",
|
||||
"url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { FontDefinitions } from './types';
|
||||
|
||||
import rawFonts from './data/fonts.json' with { type: 'json' };
|
||||
|
||||
export const fonts: FontDefinitions = rawFonts;
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { CustomizationDefaultFont } from '@gitbook/api';
|
||||
import { getDefaultFont } from './getDefaultFont';
|
||||
|
||||
describe('getDefaultFont', () => {
|
||||
it('should return null for invalid font', () => {
|
||||
const result = getDefaultFont({
|
||||
font: 'invalid-font' as CustomizationDefaultFont,
|
||||
text: 'Hello',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for invalid weight', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello',
|
||||
weight: 999 as any,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for text not supported by any script', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: '😀', // Emoji not supported by Inter
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return correct object for Latin text', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello World',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should return correct object for Cyrillic text', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Привет мир',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should return correct object for Greek text', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Γεια σας',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle mixed script text', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello Привет',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle different font weights', () => {
|
||||
const regular = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello',
|
||||
weight: 400,
|
||||
});
|
||||
const bold = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello',
|
||||
weight: 700,
|
||||
});
|
||||
expect(regular).not.toBeNull();
|
||||
expect(bold).not.toBeNull();
|
||||
expect(regular).toMatchSnapshot('regular');
|
||||
expect(bold).toMatchSnapshot('bold');
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: '',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle different fonts', () => {
|
||||
const inter = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello',
|
||||
weight: 400,
|
||||
});
|
||||
const roboto = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Roboto,
|
||||
text: 'Hello',
|
||||
weight: 400,
|
||||
});
|
||||
expect(inter).not.toBeNull();
|
||||
expect(roboto).not.toBeNull();
|
||||
expect(inter).toMatchSnapshot('inter');
|
||||
expect(roboto).toMatchSnapshot('roboto');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { CustomizationDefaultFont } from '@gitbook/api';
|
||||
import { fonts } from './fonts';
|
||||
import type { FontWeight } from './types';
|
||||
|
||||
/**
|
||||
* Get the URL to load a font for a text.
|
||||
*/
|
||||
export function getDefaultFont(input: {
|
||||
/**
|
||||
* GitBook font to use.
|
||||
*/
|
||||
font: CustomizationDefaultFont;
|
||||
|
||||
/**
|
||||
* Text to display with the font.
|
||||
*/
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* Font weight to use.
|
||||
*/
|
||||
weight: FontWeight;
|
||||
}): { font: string; url: string } | null {
|
||||
if (!input.text.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fontDefinition = fonts[input.font];
|
||||
if (!fontDefinition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const variant = fontDefinition.variants[`${input.weight}`];
|
||||
if (!variant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const script = getBestUnicodeRange(input.text, fontDefinition.unicodeRange);
|
||||
if (!script) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return variant[script]
|
||||
? {
|
||||
font: input.font,
|
||||
url: variant[script],
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which named @font-face unicode-range covers
|
||||
* the greatest share of the characters in `text`.
|
||||
*
|
||||
* @param text The text you want to inspect.
|
||||
* @param ranges An object whose keys are range names and whose
|
||||
* values are CSS-style comma-separated unicode-range
|
||||
* declarations (e.g. "U+0370-03FF,U+1F00-1FFF").
|
||||
* @returns The key of the best-matching range, or `null`
|
||||
* when nothing matches at all.
|
||||
*/
|
||||
function getBestUnicodeRange(text: string, ranges: Record<string, string>): string | null {
|
||||
// ---------- helper: parse "U+XXXX" or "U+XXXX-YYYY" ----------
|
||||
const parseOne = (token: string): [number, number] | null => {
|
||||
token = token.trim().toUpperCase();
|
||||
if (!token.startsWith('U+')) return null;
|
||||
|
||||
const body = token.slice(2); // drop "U+"
|
||||
const [startHex, endHex] = body.split('-');
|
||||
const start = Number.parseInt(startHex, 16);
|
||||
const end = endHex ? Number.parseInt(endHex, 16) : start;
|
||||
|
||||
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
|
||||
return [start, end];
|
||||
};
|
||||
|
||||
// ---------- helper: build lookup table ----------
|
||||
const parsed: Record<string, [number, number][]> = {};
|
||||
for (const [label, list] of Object.entries(ranges)) {
|
||||
parsed[label] = list
|
||||
.split(',')
|
||||
.map(parseOne)
|
||||
.filter((x): x is [number, number] => x !== null);
|
||||
}
|
||||
|
||||
// ---------- tally code-point hits ----------
|
||||
const hits: Record<string, number> = Object.fromEntries(Object.keys(parsed).map((k) => [k, 0]));
|
||||
|
||||
for (let i = 0; i < text.length; ) {
|
||||
const cp = text.codePointAt(i)!;
|
||||
i += cp > 0xffff ? 2 : 1; // advance by 1 UTF-16 code-unit (or 2 for surrogates)
|
||||
|
||||
for (const [label, rangesArr] of Object.entries(parsed)) {
|
||||
if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
|
||||
hits[label]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- choose the "best" ----------
|
||||
let winner: string | null = null;
|
||||
let maxCount = 0;
|
||||
|
||||
for (const [label, count] of Object.entries(hits)) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
winner = label;
|
||||
}
|
||||
}
|
||||
|
||||
return maxCount > 0 ? winner : null;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './getDefaultFont';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { CustomizationDefaultFont } from '@gitbook/api';
|
||||
|
||||
export type FontWeight = 400 | 700;
|
||||
|
||||
export type FontDefinition = {
|
||||
font: string;
|
||||
unicodeRange: {
|
||||
[script: string]: string;
|
||||
};
|
||||
variants: {
|
||||
[weight in string]: {
|
||||
[script: string]: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type FontDefinitions = { [fontName in CustomizationDefaultFont]: FontDefinition };
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2023"],
|
||||
"module": "ESNext",
|
||||
"target": "es2022",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"generate": {
|
||||
"inputs": ["bin/**/*", "package.json"],
|
||||
"outputs": ["src/data/*.json", "dist/data/*.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# visual tests
|
||||
screenshots/
|
||||
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# Generated public files
|
||||
/public/~gitbook/static/*
|
||||
!/public/~gitbook/static/images
|
||||
|
||||
# cloudflare
|
||||
.open-next
|
||||
.wrangler
|
||||
worker-configuration.d.ts
|
||||
@@ -0,0 +1,876 @@
|
||||
# gitbook
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d130532: Suggest questions in the current space context
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 44f4151: Fix close buttons tooltips
|
||||
- d903273: Strip visitor params from URL
|
||||
- df7de8f: Fix an issue where links were not rendering properly in Ask AI answers
|
||||
- d270b4a: All tracking is now disabled for dynamic routes such as the site preview.
|
||||
- b1f608b: Implement basic URL scheme for assistant with ask
|
||||
- d024658: Prevent page breaks inside blocks when printing
|
||||
- 185cdb4: Fix scrolling to anchor positionning
|
||||
- 5c49235: Remove highlighting in Safari for PowerShell and C++ to avoid page crash until next version with bug fix is released
|
||||
- f1a6dec: Update OpenAPI parser
|
||||
- 2cdba53: Upgrade to Tailwind v4
|
||||
- Updated dependencies [2cdba53]
|
||||
- Updated dependencies [f1a6dec]
|
||||
- @gitbook/react-math@0.6.1
|
||||
- @gitbook/icons@0.2.2
|
||||
- @gitbook/openapi-parser@3.0.0
|
||||
- @gitbook/react-contentkit@0.7.3
|
||||
- @gitbook/react-openapi@1.3.5
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7c951ef: 'Support new colors in text formatting
|
||||
- 39c4f76: Add fullwidth page option
|
||||
- eb1bd3a: AI response feedback buttons
|
||||
- 2ba7e54: Fix headings styles in hint block
|
||||
- 250c194: Add nosnippet to announcement banner.
|
||||
- 4aeb81b: Fix search input styling & text cutoff
|
||||
- 68f0dbc: Add confirmation modal to contentkit button
|
||||
- 611e286: Remove padding on first Heading in Columns
|
||||
- b0c534f: Tweaks to AI Chat and AI Actions Dropdown
|
||||
- Updated dependencies [b5ad0ce]
|
||||
- @gitbook/openapi-parser@2.2.2
|
||||
- @gitbook/react-openapi@1.3.4
|
||||
|
||||
## 0.14.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 0003030: Implement AI actions dropdown
|
||||
- acb9f53: New search layout
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 334cfdd: Fix responsive class for SearchInput button
|
||||
- 6816f0f: add browserlists and fix old browser css-masks
|
||||
- 9ee9082: Refactor icon loading state in AIAction components
|
||||
- 52ab368: Reposition AI Actions dropdown
|
||||
- 7212345: Fix AI Actions dropdown and LLM integration
|
||||
- 8daede5: Ensure all content links are resolved relatively to preview.
|
||||
- ed684c1: Move AI Actions markdown fetching to client-side
|
||||
- 9cc5a78: Generalise keyboard shortcuts, add Cmd+J to AI Chat
|
||||
- Updated dependencies [6816f0f]
|
||||
- Updated dependencies [1738677]
|
||||
- @gitbook/icons@0.2.1
|
||||
- @gitbook/openapi-parser@2.2.1
|
||||
- @gitbook/react-contentkit@0.7.2
|
||||
- @gitbook/react-openapi@1.3.3
|
||||
|
||||
## 0.13.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f5894bc: Fix incorrect revision ID in Markdown fetch logic
|
||||
- 1b59e7c: Styling improvements to AI Chat Followup questions
|
||||
- Updated dependencies [bd553bc]
|
||||
- @gitbook/openapi-parser@2.2.0
|
||||
- @gitbook/react-openapi@1.3.2
|
||||
|
||||
## 0.13.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- af98402: Add support for inline icons.
|
||||
- 7d3fe23: Add circular corners and depth styling
|
||||
- f3affc3: Display MCP tool calls in AI chat.
|
||||
- fa12f9e: Support dark-mode specific page cover image
|
||||
- df848ef: Add support for icons in buttons.
|
||||
- b7a0db3: Fix rendering of ogimage with SVG logos.
|
||||
- c3b620e: Best effort at preserving current variant when navigating between sections by matching the pathname against site spaces in the new section.
|
||||
- 4fb2a4a: Rework full-width layout, add support for full-width page option
|
||||
- df848ef: Add support for text alignment for headings and paragraphs.
|
||||
- f033734: Add support for site customization option to change how external links open.
|
||||
- 7b38f89: Enable AI chat when AI mode is configured to assistant
|
||||
- 8d65983: Add AI chat
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 42d88da: Fix UX issue about highlighting the search term in search result sections
|
||||
- 6aa3ff9: 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
|
||||
|
||||
- 015615d: Respect fullWidth and defaultWidth for images
|
||||
- e8fb84d: Fix hash with align in columns
|
||||
- 4721403: Hide scrollbar on sections
|
||||
- d410381: Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list
|
||||
- 7d5a6d2: fix nested a tag causing hydration error
|
||||
- bc1eca8: Error handling for AI Chat
|
||||
- b3a7ad6: fix href being empty in TOC
|
||||
- dc4268d: Fix navigation between sections/variants when previewing a site in v2
|
||||
- 58d7f3c: Fix revision id for computed content
|
||||
- 11a6511: Fix crash when integration script fails to render block.
|
||||
- 7a00880: Improve support for OAuth2 security type
|
||||
- c0ee60e: Adds Columns layout block to GBO
|
||||
- 9316ccd: Update Models page styling
|
||||
- 42d43e0: Show scrollbars
|
||||
- 72cd0e5: Optimize performances by using a smarter per-request cache arround data cached functions
|
||||
- af66ff7: add a force-revalidate api route to force bust the cache in case of errors
|
||||
- e2afc07: Fix resolution of page by resolving site redirects before space redirects
|
||||
- 88a35ed: Fix crash when integration is triggering invalid requests.
|
||||
- 711cf38: Optimize the fetch of revision files by using only the getRevision cache.
|
||||
- f58b904: encode customization header
|
||||
- 4f5fec7: Fix CodeBlock layout
|
||||
- 3bfe347: Show tabs when there is a single section group present
|
||||
- a7a713b: Scroll to active TOC when clicking a link
|
||||
- ba0094a: fix ISR on preview env
|
||||
- 521052d: Fix concurrent execution in Vercel causing pages to not be attached to the proper tags.
|
||||
- 33726c8: Generate a llms-full.txt version of the docs site
|
||||
- 500c8cb: Don't crash ogimage generation on RTL text, as a workaround until we can support it.
|
||||
- 6859f7d: Fix rendering of ogimage when logo or icon are AVIF images.
|
||||
- a28a997: Add margin to adjacent buttons
|
||||
- 67998b6: Fix ogimage generation failing with some JPEG images.
|
||||
- 8c0a53a: Fix page group not expanded by default
|
||||
- dfa8a37: Don't cache unexpected API errors for more than a few minutes.
|
||||
- 73e0cbb: Fix an issue where PDF export URLs were not keeping their query params.
|
||||
- e5bac69: Fix markdown page generation for groups
|
||||
- 6294bbb: add a global error boundary
|
||||
- b60039b: Fix links to other spaces within a section.
|
||||
- 59da30f: Add support for cover repositioning
|
||||
- b403962: Handle nullish OpenAPI mediaTypeObject
|
||||
- c730845: Fix missing title on button to close the announcement banner.
|
||||
- 231167d: Make icons for page groups more contrasting
|
||||
- d99da6a: Ignore case while highlighting search results.
|
||||
- dae019c: Consistently show variant selector in section bar if site has sections
|
||||
- dd65987: Include page group children under the .md route
|
||||
- 57bb146: Make TOC height dynamic based on visible header and footer elements
|
||||
- 4f7c0ee: Clicking an active TOC item toggles its descendants
|
||||
- ca3b9ac: Improve AI Chat context popup
|
||||
- 5726999: Fix viewing a page from a revision
|
||||
- 392f594: Fix InlineLinkTooltip having a negative impact on performance, especially on larger pages.
|
||||
- c9373ef: Fix bold header links hover color
|
||||
- fa3eb07: cache fonts and static image used in OGImage in memory
|
||||
- e7a591d: Fix border being added to cards
|
||||
- 427f748: Add metadata for adding site to Apple devices home
|
||||
- a3a944d: Fix crash during rendering of ogimage for VA sites with default icon.
|
||||
- 4b67fe5: Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search
|
||||
- caaa692: Allow to zoom images on mobile if relevant
|
||||
- 902c3c6: apply customization for dynamic context
|
||||
- b6b5975: Reverse order of feedback smileys
|
||||
- fbfcca5: Fix ogimage using incorrect Google Font depending on language.
|
||||
- 2932077: remove trailing slash from linker
|
||||
- 2350baa: Support for OpenAPI Array request body
|
||||
- Updated dependencies [957afd9]
|
||||
- Updated dependencies [7a00880]
|
||||
- Updated dependencies [11a6511]
|
||||
- Updated dependencies [fbfcca5]
|
||||
- Updated dependencies [a0c06a7]
|
||||
- Updated dependencies [b403962]
|
||||
- Updated dependencies [1e013cd]
|
||||
- Updated dependencies [4f5cbfe]
|
||||
- Updated dependencies [4c9a9d0]
|
||||
- Updated dependencies [40df91a]
|
||||
- Updated dependencies [2350baa]
|
||||
- @gitbook/react-openapi@1.3.1
|
||||
- @gitbook/react-contentkit@0.7.1
|
||||
- @gitbook/fonts@0.1.0
|
||||
- @gitbook/openapi-parser@2.1.5
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8339e91: Fix images in reusable content across spaces.
|
||||
- 326e28e: Design tweaks to code blocks and OpenAPI pages
|
||||
- 3119066: Add support for reusable content across spaces.
|
||||
- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c4ebb3f: Fix openapi-select hover in responses
|
||||
- aed79fd: Decrease rounding of header logo
|
||||
- 42ca7e1: Fix openapi CR preview
|
||||
- e6ddc0f: Fix URL in sitemap
|
||||
- 5e975ab: Fix code highlighting for HTTP
|
||||
- 5d504ff: Fix resolution of links in reusable contents
|
||||
- 95a1f65: Better print layouts: wrap code blocks & force table column auto-sizing
|
||||
- 0499966: Fix invalid sitemap.xml generated with relative URLs instead of absolute ones
|
||||
- 2a805cc: Change OpenAPI schema-optional from `info` to `tint` color
|
||||
- 580101d: Fix schemas disclosure label causing client error
|
||||
- 12a455d: Fix OpenAPI layout issues
|
||||
- 97b7c79: Increase logging around caching behaviour causing page crashes.
|
||||
- 373f18f: Prevent section group popovers from opening on click
|
||||
- 3f29206: Update the regex for validating site redirect
|
||||
- 0c973a3: Always link main logo to the root of the site
|
||||
- ae5f1ab: Change `Dropdown`s to use Radix's `DropdownMenu`
|
||||
- 0e201d5: Add border to filled sidebar on gradient theme
|
||||
- dd043df: Revert investigation work around URL caches.
|
||||
- 89a5816: Fix OpenAPI disclosure label ("Show properties") misalignment on mobile
|
||||
- Updated dependencies [c3f6b8c]
|
||||
- Updated dependencies [d00dc8c]
|
||||
- Updated dependencies [42ca7e1]
|
||||
- Updated dependencies [326e28e]
|
||||
- Updated dependencies [5e975ab]
|
||||
- Updated dependencies [f7a3470]
|
||||
- Updated dependencies [580101d]
|
||||
- Updated dependencies [20ebecb]
|
||||
- Updated dependencies [80cb52a]
|
||||
- Updated dependencies [cb5598d]
|
||||
- Updated dependencies [c6637b0]
|
||||
- Updated dependencies [a3ec264]
|
||||
- @gitbook/colors@0.3.3
|
||||
- @gitbook/openapi-parser@2.1.4
|
||||
- @gitbook/react-openapi@1.3.0
|
||||
|
||||
## 0.11.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ebc39e9]
|
||||
- Updated dependencies [b6b09d4]
|
||||
- @gitbook/react-openapi@1.2.1
|
||||
|
||||
## 0.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d67699a: Add OpenAPI Webhook block
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4b8a621: Show sections tabs only if there is at least two sections
|
||||
- 8ed1bda: Translate OpenAPI blocks
|
||||
- 7588cfe: Improve OpenAPIResponses examples and schemas
|
||||
- Updated dependencies [eeb977f]
|
||||
- Updated dependencies [3363a18]
|
||||
- Updated dependencies [d67699a]
|
||||
- Updated dependencies [8ed1bda]
|
||||
- Updated dependencies [7588cfe]
|
||||
- Updated dependencies [ad1dc0b]
|
||||
- @gitbook/react-openapi@1.2.0
|
||||
|
||||
## 0.10.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [77397ca]
|
||||
- @gitbook/cache-tags@0.3.1
|
||||
|
||||
## 0.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- b62b101: Do not set cookie to identify visitor for insights when disabled.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 95ea22d: Cache AI Page Link summary
|
||||
- daf41fc: Tweak footer design (and refactor)
|
||||
- de53946: Fix security issue with injection of "javacript:` url in the back button of PDFs
|
||||
- b92ecfa: Implement retry logic for the DO cache to prevent when revalidating content.
|
||||
- 528eee3: Add superscript and subscript text rendering
|
||||
- aa3357a: Fix OpenAPISchemas description padding
|
||||
- 168a4fa: Add support for buttons to GitBook.
|
||||
- 70c4182: Improve OpenAPI schema style
|
||||
- 2b6c593: Remove stable from x-stability
|
||||
- 580f7ad: Improve the error message returned by the revalidate endpoint.
|
||||
- cbd768a: Improve OpenAPI codesample (add OpenAPISelect component)
|
||||
- c765463: Fix ogimage generation crashing when site is using a custom WOFF2 font
|
||||
- e59076a: Improve OpenAPI schemas block ungrouped style. Classnames have changed, please refer to this PR to update GBX.
|
||||
- 29aaba5: Override Scalar's overscroll-behavior
|
||||
- 90ead98: Better error handling in cache revalidation.
|
||||
- Updated dependencies [116575c]
|
||||
- Updated dependencies [cdffd7c]
|
||||
- Updated dependencies [70c4182]
|
||||
- Updated dependencies [2b6c593]
|
||||
- Updated dependencies [cbd768a]
|
||||
- Updated dependencies [e59076a]
|
||||
- Updated dependencies [eedefdd]
|
||||
- Updated dependencies [23cedd2]
|
||||
- @gitbook/cache-tags@0.3.0
|
||||
- @gitbook/colors@0.3.2
|
||||
- @gitbook/react-openapi@1.1.10
|
||||
- @gitbook/openapi-parser@2.1.3
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- da7b369: Fix missing headers in OpenAPIResponses
|
||||
- 139a805: Fix OpenAPI enum display
|
||||
- Updated dependencies [da7b369]
|
||||
- Updated dependencies [da485f5]
|
||||
- Updated dependencies [139a805]
|
||||
- @gitbook/react-openapi@1.1.9
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fb90eb0: Limit tinted background on bold theme to sites with filled sidebar
|
||||
- 7d0b422: Handle grouped OpenAPISchemas
|
||||
- Updated dependencies [7d0b422]
|
||||
- Updated dependencies [fb90eb0]
|
||||
- @gitbook/react-openapi@1.1.8
|
||||
- @gitbook/colors@0.3.1
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 77fd393: Track event when visitor is opening a search result.
|
||||
- d70d566: Support site announcement banner
|
||||
- 77fd393: Track event when clicking announcement banner link.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e84a46a: Fix OpenAPI tabs indicator overflow
|
||||
- bc90adb: Fix favicon not being displayed in Google because `robots.txt` was preventing the indexation of the image route
|
||||
- 434af90: Fix image resizing when using the proxy feature in a site.
|
||||
- c756761: Add breadcrumbs to search results
|
||||
- 40e8e69: Disallow crawling by web-robots of search/ask URLs
|
||||
- 77fd393: Fix clicking search results when the site is embedded in an iframe.
|
||||
- 1505ddb: Fix multiple request examples selector not showing
|
||||
- 61db166: Add OpenAPI write-only indicator
|
||||
- 6f71da8: Fix padding in schemas
|
||||
- fa91eb7: Fix PDF generation when user has dark mode configured.
|
||||
- 5b1e01c: Support for x-stability property
|
||||
- 57ca4e0: Fix a crash when a page contains a block of an integration that is no longer installed
|
||||
- d236bf0: Fix flash when loading sites with dark mode as default theme
|
||||
- cd99ed5: Fix spec properties rendering and missing keys
|
||||
- 813b2af: Support for x-enumDescriptions and x-gitbook-enum
|
||||
- e9fa50d: Trim the search query to avoid showing a loading state when typing
|
||||
- Updated dependencies [bd35348]
|
||||
- Updated dependencies [ae78fc5]
|
||||
- Updated dependencies [7bb37c7]
|
||||
- Updated dependencies [373183a]
|
||||
- Updated dependencies [1505ddb]
|
||||
- Updated dependencies [61db166]
|
||||
- Updated dependencies [5b1e01c]
|
||||
- Updated dependencies [cd99ed5]
|
||||
- Updated dependencies [813b2af]
|
||||
- Updated dependencies [a25fded]
|
||||
- @gitbook/react-openapi@1.1.7
|
||||
- @gitbook/openapi-parser@2.1.2
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ed07206: Fix OpenAPI path overflow on mobile
|
||||
- 4dab1c5: Fix alignment of `prominent` search bar on full-width pages
|
||||
- 6eae764: Support body examples
|
||||
- 54ee014: Add initial support for loading custom fonts
|
||||
- d2facb2: Mark properties as optional if not required
|
||||
- bba2e52: Fix site redirects when it includes a section/variant path
|
||||
- 4723f03: Restyle section group dropdown
|
||||
- 24b7808: Fix `prominent` search bar width on `md` screens
|
||||
- 1fe3286: Fix OpenAPI block overflow issue
|
||||
- Updated dependencies [48c18c0]
|
||||
- Updated dependencies [6eae764]
|
||||
- Updated dependencies [7212973]
|
||||
- Updated dependencies [d2facb2]
|
||||
- Updated dependencies [73e2b47]
|
||||
- Updated dependencies [70be2c6]
|
||||
- Updated dependencies [fc00b51]
|
||||
- Updated dependencies [a84b06b]
|
||||
- @gitbook/openapi-parser@2.1.1
|
||||
- @gitbook/react-openapi@1.1.6
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 886e204: Update OpenAPI operation path design
|
||||
- Updated dependencies [886e204]
|
||||
- Updated dependencies [4f0a772]
|
||||
- @gitbook/react-openapi@1.1.5
|
||||
- @gitbook/colors@0.3.0
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- eec3eed: Add styling for prominent search bar option
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 16292de: Display sidebar on no-TOC pages
|
||||
- 9b5f971: Transparent background for OpenAPI path block
|
||||
- 99da8df: Optimize favicons and og:image using the image resizer
|
||||
- b011ea0: Fix rendering of code blocks in Ask AI when being streamed
|
||||
- 9bc3d50: Info hint background and link color fixes
|
||||
- 31d800e: Render OpenAPISchemas block
|
||||
- Updated dependencies [6aaeae2]
|
||||
- Updated dependencies [c60e9ba]
|
||||
- Updated dependencies [9108c56]
|
||||
- Updated dependencies [31d800e]
|
||||
- Updated dependencies [ff3b708]
|
||||
- Updated dependencies [f32bf1f]
|
||||
- Updated dependencies [c9ea239]
|
||||
- @gitbook/react-contentkit@0.7.0
|
||||
- @gitbook/react-openapi@1.1.4
|
||||
- @gitbook/cache-tags@0.2.0
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- eaf2d68: OpenAPI operation title fallback in sections
|
||||
- Updated dependencies [844059f]
|
||||
- Updated dependencies [88f64b0]
|
||||
- @gitbook/react-openapi@1.1.3
|
||||
- @gitbook/icons@0.2.0
|
||||
- @gitbook/react-contentkit@0.6.2
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f127d28: Rename OpenAPIModels to OpenAPISchemas
|
||||
- Updated dependencies [f127d28]
|
||||
- @gitbook/react-openapi@1.1.2
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [f574858]
|
||||
- @gitbook/react-openapi@1.1.1
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- bb3ca9c: Implement OpenAPI models blocks
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5907bd9: Adjust hint block spacing
|
||||
- 9ffc3b6: Fix content overflowing out of its container in tabs
|
||||
- Updated dependencies [0278a14]
|
||||
- Updated dependencies [bb3ca9c]
|
||||
- Updated dependencies [3173d8e]
|
||||
- Updated dependencies [052e07a]
|
||||
- @gitbook/openapi-parser@2.1.0
|
||||
- @gitbook/react-openapi@1.1.0
|
||||
|
||||
## 0.6.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 05ffd0e: Improving data cache management for computed content
|
||||
- 8beb5d6: Add input elements to ContentKit
|
||||
- Updated dependencies [53f5dbe]
|
||||
- Updated dependencies [05ffd0e]
|
||||
- Updated dependencies [8beb5d6]
|
||||
- @gitbook/openapi-parser@2.0.2
|
||||
- @gitbook/cache-tags@0.1.0
|
||||
- @gitbook/react-contentkit@0.6.1
|
||||
- @gitbook/react-openapi@1.0.5
|
||||
|
||||
## 0.6.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9b914d1: Fix getProxyModeBasePath that was computing incorrect base path in some scenarios
|
||||
- 2ae76f9: Change how a site in proxy mode is resolved
|
||||
- 027a859: Add support for links style customization option
|
||||
- 3e11678: fix: lost section groups
|
||||
- 3319375: Support OpenAPI operation block
|
||||
- Updated dependencies [722f02e]
|
||||
- Updated dependencies [0924259]
|
||||
- @gitbook/react-openapi@1.0.4
|
||||
- @gitbook/openapi-parser@2.0.1
|
||||
|
||||
## 0.6.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a820739: Remove unused search api method from gitbook/api/lib
|
||||
- a054554: Implement a trusted mode to speed up OpenAPI spec validation
|
||||
- 66d0fc0: Update design for hint block: use semantic colors (info, warning, danger, success) and add alternative styling for hints with headings
|
||||
- 9f0de74: Add support for new OpenAPI ref
|
||||
- da55fac: Render GitBook blocks in OpenAPI operation description
|
||||
- Updated dependencies [c808bb1]
|
||||
- Updated dependencies [dc2dbc5]
|
||||
- Updated dependencies [f1d1d2f]
|
||||
- Updated dependencies [e24206e]
|
||||
- Updated dependencies [a054554]
|
||||
- Updated dependencies [05e1d8c]
|
||||
- Updated dependencies [b4a12d6]
|
||||
- Updated dependencies [9f0de74]
|
||||
- Updated dependencies [da55fac]
|
||||
- @gitbook/openapi-parser@2.0.0
|
||||
- @gitbook/react-openapi@1.0.3
|
||||
|
||||
## 0.6.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 359bb97: Fix opening external links when the GitBook page is embedded in an iframe
|
||||
- 6157583: Improve Markdown parsing
|
||||
- 82cd9f2: Add support for anchor links in OpenAPI blocks
|
||||
- Updated dependencies [445baaa]
|
||||
- Updated dependencies [bb5c6a4]
|
||||
- Updated dependencies [a3f1fea]
|
||||
- Updated dependencies [6157583]
|
||||
- Updated dependencies [7419ee7]
|
||||
- Updated dependencies [82cd9f2]
|
||||
- @gitbook/colors@0.2.0
|
||||
- @gitbook/react-openapi@1.0.2
|
||||
- @gitbook/openapi-parser@1.0.1
|
||||
|
||||
## 0.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- dddb4ec: Fix long tab group description
|
||||
- Updated dependencies [f8d4c76]
|
||||
- Updated dependencies [dddb4ec]
|
||||
- Updated dependencies [f8d4c76]
|
||||
- @gitbook/react-openapi@1.0.1
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 98245e5: Adapt code to pull token from customer backend generated custom cookies
|
||||
- af3c6a9: Reintroduce a safety check around search whilst we continue investigating caching.
|
||||
- 95f2aa4: Track new events for site insights when ads are being clicked
|
||||
- 08acea6: Investigate an issue causing caches to return empty objects instead of null/undefined.
|
||||
- 1138d59: Add support for sidebar background styles
|
||||
- 9e18ae6: Overhaul colour scale & Tailwind colour classes
|
||||
- e86e51f: Fix an issue where the redirects of potentially malicious images were not going through.
|
||||
- 7059c2b: Add support for computed content by fetching computed documents for pages.
|
||||
- c71d159: Track events for site insights using the new dedicated API.
|
||||
- eb7c22f: Revert scalar to 1.0.87 to mitigate an issue with ApiClientModalProvider
|
||||
- ea1468c: Send redirectOnError param to getPublishedContent when token is pulled from cookie
|
||||
- 7ee9158: Restyle PageAside to use sidebar list styles
|
||||
- dbba50c: Fix an issue where search and Ask AI triggers unnecessary renders when in a Visitor Authenticated site.
|
||||
- 1417279: Track clicks on links (header, footer, content) for site insights.
|
||||
- 9eca010: Improve the display of recommended questions by streaming them.
|
||||
- 160fca1: new OpenAPI blocks design
|
||||
- 71688a8: Introduction of new themes: Clean, Muted, Bold, and Gradient
|
||||
- 1823101: Fix internal properties appearing in OpenAPI docs.
|
||||
- 6a073e1: Add antialiasing for text rendering
|
||||
- 8126a83: Improve readability of tables with hover style and vertical dividers
|
||||
- db74ea3: Image optimization endpoint redirects to underlying image URL if the signature is not the latest.
|
||||
- 99579ac: Fix a vulnerability issue for images using an older version of the image signing parameter.
|
||||
- e4e2f52: Track an event into site insights when visitor is opening the Scalar API client.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d876399: Fix UI search without ask AI enabled and fix error with questions not returned from API
|
||||
- c30bc24: Fix empty sitemap
|
||||
- e90c96f: page outline on the right remains visible when scrolling, move mode toggler to PageAside
|
||||
- 5b4e710: Support llms.txt
|
||||
- b6c3870: Add support for keyboard marks
|
||||
- 6059efe: Fix search no results error showing while there are results
|
||||
- c77142a: Log component stack in Sentry
|
||||
- 1de9d1a: Apply antialiasing on any text that are not code inline/blocks to avoid contrast issues
|
||||
- 32aa1f9: Handle security issue with cookies on Safari
|
||||
- d935fb1: Don't add extra page scroll when footer is not present
|
||||
- 53de5b1: Fix site section URL resolution in Ask AI sources
|
||||
- 24f5249: Fix vertical section overflow color
|
||||
- 1762f85: Reduce gap between subsequent header buttons
|
||||
- c1e27cc: Fix pass Sentry release properly
|
||||
- 5ae1b88: Fix shrinking page icons
|
||||
- 8f046a9: Start using tint in more places, TOC and PageAside
|
||||
- 665b6be: Ignore invalid API calls to `getSiteRedirectFromSource` API
|
||||
|
||||
To reduce the load on the API and also avoid errors.
|
||||
|
||||
- 26e6401: Remove KV cache backend and only rely on DO as an external cache backend
|
||||
- 8cfa67c: Fix default outline list styling
|
||||
- d66c184: Ignore errors from event flushing
|
||||
- 6088fa5: Simplify search results logic to investigate a bug
|
||||
- 68287d3: Cache API spec for 24 hours, revalidated every 2 hours
|
||||
- 09c7c30: Try to fix error on og image generation
|
||||
- ae99f87: Improve emoji setup, align with GitBook app
|
||||
- 2906e60: Downgrade to Next.js v14 to fix incompatibilities with next-on-pages causing multiple bugs.
|
||||
- 3a7210d: Fix zoom image view transition on Safari
|
||||
- 718a8a5: Position the variant picker in the ToC
|
||||
- e5dc05e: Update footer styling and allow for more than 4 footer groups
|
||||
- 8276ba0: Make cookies access safer
|
||||
- 1b8a456: Fix Image blocks zoomable behaviour
|
||||
- 56c52e0: Handle Firefox security error on localStorage
|
||||
- 0510b6f: Add section description to SectionGroupTile
|
||||
- 1fcc807: Fix errors from customization not found
|
||||
- 46edde9: Improve the OpenAPI package API
|
||||
- 8af1abc: Improve contrast of search box placeholder
|
||||
- 92b7668: Improve header offset
|
||||
- d9c8d57: Do not dereference before caching OpenAPI spec.
|
||||
- 94876e3: Fix regression issue with page icons for multi-line titles
|
||||
- 47971dc: Fix OG image generation for non-latin characters
|
||||
- 82dc9c4: Simplify the `useHash` algo used.
|
||||
- 128ad20: Ignore cache invalidation error from local backend
|
||||
- ff05e20: Improvements to inline images in headings
|
||||
- cb100d5: Allow only good values for theme query parameter. Avoid having a 500 error when we pass an invalid value.
|
||||
- d5aaccd: Remove use of deprecatd API createSitesPageFeedback
|
||||
- 48ab59f: Improve colour contrast of list item decoration
|
||||
- d2bc567: Set Sentry release
|
||||
- 37d13d8: Avoid error on fetch by passing a string URL
|
||||
- d3e573c: Generate sitemap for all sections and spaces
|
||||
- f7b801b: Add feedback form to page rating control
|
||||
- d370a3f: Update the routes for changes/revisions in multi-id mode to match the normal mode
|
||||
- 46f63cb: Fix code format overriding inline link styles
|
||||
- 5950657: Fix emojis display
|
||||
- 528a053: Fix server actions stability leading to no results found sometimes on search
|
||||
- eac1314: Lazy load iframely script to make page more responsive
|
||||
- ad19060: Cards stand out slighly more on tinted and dark mode sites, and have better support for headings inside them
|
||||
- 6f54826: New highlight colors
|
||||
- 5c87ec7: Implement a safer way to interact with localStorage.
|
||||
|
||||
If it's disabled on the browser it should not throw error.
|
||||
|
||||
- 02d876e: Fix search UI behaviour
|
||||
- f4a90de: Fix two issues where pages would crash due Recoil not behaving correctly in RSC.
|
||||
- 5576906: Fix table of content displaying arrow next to page with only hidden pages
|
||||
- aaab157: Visual fix for section group in Safari
|
||||
- cbe6139: Fix dynamic tabs infinite loop
|
||||
- 65cc4af: Fix error when accessing a change request not found
|
||||
- 727bde2: Improve and split OpenAPI parser into its own package
|
||||
- 0b6ddca: Fix variant selector contrast for non-default themes
|
||||
- 87b8ea8: Fix issue leading to increase the storage write and the stability of the platform
|
||||
- fde32e2: Force route handler to be dynamic to avoid errors
|
||||
- a025118: Change card layout depending on cover aspect ratio
|
||||
- 300f7bf: Fix search loading state
|
||||
- 29d5979: Disable C/C++ highlight temporarily
|
||||
- 18953b2: Subtler tint color when based on the primary color, by mixing in some gray
|
||||
- 1c97536: Fix Sentry instrumentation
|
||||
- b0bd871: Even safer localStorage
|
||||
- b950a64: Avoid errors on legacy browsers
|
||||
- 38061bd: Add section groups to section tabs
|
||||
- 160fca1: Support deprecated and x-deprecated-sunset in OpenAPI spec
|
||||
- 0e601e2: Improve styling of header buttons with shadows and high-contrast styles
|
||||
- 6691492: Fix viewing PDF from space
|
||||
- e8e64bf: Fix bullet list display on full size blocks
|
||||
- 16194c5: Vertical orientation for sections list on sites without header
|
||||
- b41d425: Improve OpenAPI rendering performances by caching markdown parsing
|
||||
- 1f8e416: Improve performances by highlighting code client-side if the code block is offscreen
|
||||
- 1429384: Fix error when accessing some not found pages.
|
||||
- 21cbd9e: Change link color to primary-subtle
|
||||
- 5dab70f: Fix "Parser" language syntax highlighting
|
||||
- deb8c54: Upgrade Next.js to v15, upgrade Shiki and use JS RegExp engine
|
||||
- 56331d2: Fix breadcrumbs emoji display + add contrast styles
|
||||
- a6f6591: Fix server actions cache compromised. Leading to some bugs on frontend.
|
||||
- 44a20fe: Improve smoothness of scroll listener
|
||||
- 5664e5a: Fix variant dropdown styling in header
|
||||
- 6b50360: Fix view transition error on Safari
|
||||
- 741dd49: Bump `heading-3` font size to offset it from paragraphs
|
||||
- 5112e3e: Fix Sentry instrumentation server-side
|
||||
- 1de338c: Remove animation on section tabs. Page is reloaded (for technical reasons), so the animation is not accurate here.
|
||||
- Updated dependencies [d9029c7]
|
||||
- Updated dependencies [6e54a06]
|
||||
- Updated dependencies [162b4b7]
|
||||
- Updated dependencies [e4e2f52]
|
||||
- Updated dependencies [0c03676]
|
||||
- Updated dependencies [3e5e458]
|
||||
- Updated dependencies [46edde9]
|
||||
- Updated dependencies [d9c8d57]
|
||||
- Updated dependencies [ccf2cff]
|
||||
- Updated dependencies [dda0cc6]
|
||||
- Updated dependencies [eb7c22f]
|
||||
- Updated dependencies [ea1468c]
|
||||
- Updated dependencies [648f0e9]
|
||||
- Updated dependencies [160fca1]
|
||||
- Updated dependencies [f92e906]
|
||||
- Updated dependencies [e721f17]
|
||||
- Updated dependencies [727bde2]
|
||||
- Updated dependencies [dff08ae]
|
||||
- Updated dependencies [fc7b16f]
|
||||
- Updated dependencies [fe8acc9]
|
||||
- Updated dependencies [1823101]
|
||||
- Updated dependencies [a652958]
|
||||
- Updated dependencies [2f73db7]
|
||||
- Updated dependencies [160fca1]
|
||||
- Updated dependencies [12c7862]
|
||||
- Updated dependencies [b41d425]
|
||||
- @gitbook/react-openapi@1.0.0
|
||||
- @gitbook/openapi-parser@1.0.0
|
||||
- @gitbook/react-contentkit@0.6.0
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 57cdd25: GitBook Open now supports Ask AI in sites. When asking a question to Ask AI, GitBook will use context from across your site sections and variants to provide the best answer.
|
||||
- ca134c8: Fix an issue where the active site section indicator appeared above any dropdowns.
|
||||
- d48926e: Fix an issue where the space dropdown was shown under the site sections in Safari.
|
||||
- 9fe8142: Fix an issue where Ask AI was erroring due to an object being passed as a param.
|
||||
- d843e5e: Fix an issue where the space dropdown could appear behind the header.
|
||||
- a2e5647: Fix the styling of site section tabs on smaller screens.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 076dc48: Fix expandable block anchore resolution
|
||||
- d9bb9f9: Fix an issue with the cookie banner buttons being non responsive
|
||||
- 23584c9: Update the site header with new styling, a new search button, and refactored layout
|
||||
- 664debc: Add support for tint color
|
||||
- 4d56f11: Update styling of search+ask modal
|
||||
- 061c0c1: Fix a regression in variant drop-down caused by missing z-index.
|
||||
- 2f76712: Add breadcrumbs above page title
|
||||
- 07cf835: Add scroll margin to the top when there are sections
|
||||
- 5d72b35: Smoother tab transition for sections
|
||||
- 7c71363: Don't adjust fallback font for mono font.
|
||||
- 7675c2c: Optimize performances by using new API endpoint for fetching site data.
|
||||
- 87eea73: Fix margin and image resolution of header logo
|
||||
- aa2ed0f: Restyle hint blocks
|
||||
- ffd3937: Fix security issue with image resizing that could be used for phishing
|
||||
- 2ce59d7: Fix - whitespace added to site section tabs with icons.
|
||||
- c73e07d: Increase token max length to fix code not highlighted
|
||||
- 3b3d6e2: Add icons to sections
|
||||
- 1ed18c0: style: adds missing scalar css variables
|
||||
- Updated dependencies [b7a5106]
|
||||
- Updated dependencies [4771c78]
|
||||
- Updated dependencies [ff50ac2]
|
||||
- Updated dependencies [867481c]
|
||||
- Updated dependencies [7ba67fd]
|
||||
- Updated dependencies [a78c1ec]
|
||||
- @gitbook/cache-do@0.1.1
|
||||
- @gitbook/react-openapi@0.7.1
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- e09f747: Revalidate change request cached content when pressing refresh button
|
||||
- 2fa0851: Add navigation tabs for sections
|
||||
- a4b63b8: Support resolution of new site URLs with sections
|
||||
- 5c35f36: Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
|
||||
- e9b31a5: Unify section tab styles with page item styles
|
||||
- f12a215: Add support for Norwegian language
|
||||
- f4c9536: Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks)
|
||||
- 1f24fe4: Add support for page icons
|
||||
- cda08a9: Add support for searching results in a sections site
|
||||
- b32e40c: Persist state of tabs and dynamically sync them based on title
|
||||
- 15d2ee3: Show the caption for file blocks
|
||||
- f885e88: Improve the toolbar for change-requests and revisions to show more actions
|
||||
- 07ea45b: Remove deprecated synced block from GitBook Open
|
||||
- c3675fd: Added support for new Reusable Content block.
|
||||
- 1f24fe4: Add support for icons style customization for sites
|
||||
- 4c19014: Prevent search indexation for pages where it's configured as disabled
|
||||
- 3422ad4: Update rendering of community ads to match new API response, and make it possible to preview ads.
|
||||
- 1152445: Changed the alternative URL resolution criteria in order to support site URLs without /v/ prefix
|
||||
- 2c437f7: Fix linking to a tab itself
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- aa32198: Avoid multiple <h1> in the page by using a <div> for the title in the header
|
||||
- 51fa3ab: Adds content-visibility css property to OpenAPI Operation for better render performance
|
||||
- a7066cc: Fix scroll position when navigating pages on mobile
|
||||
- c754fc9: Add automatic color contrast in site header, restyle search button
|
||||
- 5fe7adb: RND-3532: drop down menu for hidden links at small screen size
|
||||
- 6295881: Change dark mode shadow for multi-space search toolbar
|
||||
- f89b31c: Upgrade the scalar api client package
|
||||
- 13c7534: Use ellipsis and fix icon color for more links in the header on small screen
|
||||
- f885e88: Improve consistency of change request preview by removing cache-control on response
|
||||
- 16e6171: Improve performances of loading pages with embeds by caching them
|
||||
- 34d36c6: Fix GitBook specific static assets not being served correctly when deployed on Cloudflare
|
||||
- af9e66e: Only display spaces dropdown in compact header when site is multi-variants
|
||||
- e3a3d6a: Improve perception of fast loading by not rendering skeletons for individual blocks in the top part of the viewport
|
||||
- 042b850: Automatically scroll to active item in TOC
|
||||
- d43202f: Optimize bundle size of the server output by reducing bundle size of shiki (skipping themes)
|
||||
- bfbed1a: Ensure "Sponsored via GitBook" can be translated in all languages
|
||||
- fe9e6c1: Update ogimage with new design
|
||||
- 17f71ba: Use url hash to open Expandable and scroll to anchor
|
||||
- 3c07e65: Fix margin for paragraphs in quote blocks
|
||||
- 636b868: Use new cache backend, powered by Durable Objects, alongside the existing ones (KV, etc).
|
||||
- f16560c: Include offset in calculations of whether scrollable element is in view
|
||||
- 689f553: Fix inconsistent click area in table because of scroll indicator
|
||||
- 6ce3cea: Stop using KV cache backend for now, but also improves it for higher performances
|
||||
- e914903: Synchronize response and response example tabs
|
||||
- 0f990c7: Show definition title when visible in cards
|
||||
- e3a3d6a: Fix flickering when displaying an "Ask" answer with code blocks
|
||||
- 4cbcc5b: Rollback of scalar modal while fixing perf issue
|
||||
- 3996110: Optimize images rendered in community ads
|
||||
- 133c3e7: Update design of Checkbox to be more consistent and readable
|
||||
- 5096f7f: Disable KV cache for gitbook.com/docs as a test, also disable it for change-request to improve consistency
|
||||
- 0f1565c: Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
|
||||
- 2ff7ed1: Fix table of contents being visible on mobile when disabled at the page level
|
||||
- b075f0f: Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
|
||||
- cb782a7: Fix "ip" being passed to BSA for community ads
|
||||
- a7af3ca: Improving the look and feel of new section tabs
|
||||
- 0bf985a: Don't show hidden pages in the empty state of a page
|
||||
- d6c28a0: Update header styling of sections, variant selector, and button links
|
||||
|
||||
- Change position of variant selector depending on context (next to logo or in table of contents)
|
||||
- Update section tab styling and animation
|
||||
- Make header buttons smaller with a new `medium` button size
|
||||
|
||||
- Updated dependencies [51fa3ab]
|
||||
- Updated dependencies [9b8d519]
|
||||
- Updated dependencies [cf3045a]
|
||||
- Updated dependencies [f89b31c]
|
||||
- Updated dependencies [d0f4860]
|
||||
- Updated dependencies [ef9d012]
|
||||
- Updated dependencies [094e9cd]
|
||||
- Updated dependencies [636b868]
|
||||
- Updated dependencies [56f5fa1]
|
||||
- Updated dependencies [5c35f36]
|
||||
- Updated dependencies [4247361]
|
||||
- Updated dependencies [aa8c49e]
|
||||
- Updated dependencies [e914903]
|
||||
- Updated dependencies [4cbcc5b]
|
||||
- Updated dependencies [0f1565c]
|
||||
- Updated dependencies [237b703]
|
||||
- Updated dependencies [51955da]
|
||||
- Updated dependencies [a679e72]
|
||||
- Updated dependencies [c079c3c]
|
||||
- Updated dependencies [5c35f36]
|
||||
- Updated dependencies [776bc31]
|
||||
- @gitbook/react-openapi@0.7.0
|
||||
- @gitbook/cache-do@0.1.0
|
||||
- @gitbook/icons@0.1.0
|
||||
- @gitbook/react-contentkit@0.5.1
|
||||
- @gitbook/react-math@0.6.0
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 24b785c: Update shiki for code block syntax highlighting, with support for more languages and fixes for diffs. It also patches the deployment on Cloudflare to support edge functions larger than 4MB.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- acc3f2f: Fix error with the "Try it" of OpenAPI block because of the Scalar proxy failing on Cloudflare with the `cache` option
|
||||
- Updated dependencies [709f1a1]
|
||||
- Updated dependencies [ede2335]
|
||||
- Updated dependencies [0426312]
|
||||
- @gitbook/react-openapi@0.6.0
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3445db4]
|
||||
- @gitbook/react-contentkit@0.5.0
|
||||
- @gitbook/react-openapi@0.5.0
|
||||
- @gitbook/react-math@0.5.0
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [24cd72e]
|
||||
- @gitbook/react-contentkit@0.4.0
|
||||
- @gitbook/react-math@0.4.0
|
||||
- @gitbook/react-openapi@0.4.0
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- de747b7: Refactor the repository to be a proper monorepo and publish JS files on NPM instead of TypeScript files.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [de747b7]
|
||||
- Updated dependencies [de747b7]
|
||||
- @gitbook/react-contentkit@0.3.0
|
||||
- @gitbook/react-openapi@0.3.0
|
||||
- @gitbook/react-math@0.3.0
|
||||
@@ -0,0 +1,253 @@
|
||||
import { type TestsCase, runTestCases, waitForCookiesDialog } from './util';
|
||||
|
||||
/** A list of test cases to run on the customers' docs sites. */
|
||||
const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'Snyk',
|
||||
contentBaseURL: 'https://docs.snyk.io',
|
||||
tests: [
|
||||
{ name: 'Home', url: '/', run: waitForCookiesDialog },
|
||||
{ name: 'OpenAPI', url: '/snyk-api/reference/apps', run: waitForCookiesDialog },
|
||||
],
|
||||
},
|
||||
// {
|
||||
// name: 'Nexthink',
|
||||
// contentBaseURL: 'https://docs.nexthink.com',
|
||||
// tests: [
|
||||
// {
|
||||
// name: 'Home',
|
||||
// url: '/',
|
||||
// screenshot: { waitForTOCScrolling: false },
|
||||
// run: waitForCookiesDialog,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
name: 'asiksupport-stg.dto.kemkes.go.id',
|
||||
contentBaseURL: 'https://asiksupport-stg.dto.kemkes.go.id',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'jasons-tutorials.gitbook.io',
|
||||
contentBaseURL: 'https://jasons-tutorials.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'faq.deltaemulator.com',
|
||||
contentBaseURL: 'https://faq.deltaemulator.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.dify.ai',
|
||||
contentBaseURL: 'https://docs.dify.ai',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'seeddao.gitbook.io',
|
||||
contentBaseURL: 'https://seeddao.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'faq.altstore.io',
|
||||
contentBaseURL: 'https://faq.altstore.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'support.audacityteam.org',
|
||||
contentBaseURL: 'https://support.audacityteam.org',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.gmgn.ai',
|
||||
contentBaseURL: 'https://docs.gmgn.ai',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.spicychat.ai',
|
||||
contentBaseURL: 'https://docs.spicychat.ai',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.portainer.io',
|
||||
contentBaseURL: 'https://docs.portainer.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.chirptoken.io',
|
||||
contentBaseURL: 'https://docs.chirptoken.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.dexscreener.com',
|
||||
contentBaseURL: 'https://docs.dexscreener.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.pancakeswap.finance',
|
||||
contentBaseURL: 'https://docs.pancakeswap.finance',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'book.character.ai',
|
||||
contentBaseURL: 'https://book.character.ai',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'azcoiner.gitbook.io',
|
||||
contentBaseURL: 'https://azcoiner.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.midas.app',
|
||||
contentBaseURL: 'https://docs.midas.app',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.keeper.io',
|
||||
contentBaseURL: 'https://docs.keeper.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'adiblar.gitbook.io',
|
||||
contentBaseURL: 'https://adiblar.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.gradient.network',
|
||||
contentBaseURL: 'https://docs.gradient.network',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
// {
|
||||
// name: 'mygate-network.gitbook.io',
|
||||
// contentBaseURL: 'https://mygate-network.gitbook.io',
|
||||
// tests: [{ name: 'Home', url: '/' }],
|
||||
// },
|
||||
{
|
||||
name: 'treasurenft.gitbook.io',
|
||||
contentBaseURL: 'https://treasurenft.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'browndust2.gitbook.io',
|
||||
contentBaseURL: 'https://browndust2.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/', screenshot: { waitForTOCScrolling: false } }],
|
||||
},
|
||||
{
|
||||
name: 'junookyo.gitbook.io',
|
||||
contentBaseURL: 'https://junookyo.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'meshnet.nordvpn.com',
|
||||
contentBaseURL: 'https://meshnet.nordvpn.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'manual.bubble.io',
|
||||
contentBaseURL: 'https://manual.bubble.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.tickettool.xyz',
|
||||
contentBaseURL: 'https://docs.tickettool.xyz',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'wiki.redmodding.org',
|
||||
contentBaseURL: 'https://wiki.redmodding.org',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
// {
|
||||
// name: 'docs.cherry-ai.com',
|
||||
// contentBaseURL: 'https://docs.cherry-ai.com',
|
||||
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
// },
|
||||
{
|
||||
name: 'docs.snyk.io',
|
||||
contentBaseURL: 'https://docs.snyk.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.realapp.link',
|
||||
contentBaseURL: 'https://docs.realapp.link',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.plaza.finance',
|
||||
contentBaseURL: 'https://docs.plaza.finance',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.publicai.io',
|
||||
contentBaseURL: 'https://docs.publicai.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'hyperliquid.gitbook.io',
|
||||
contentBaseURL: 'https://hyperliquid.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.umbraco.com',
|
||||
contentBaseURL: 'https://docs.umbraco.com',
|
||||
tests: [
|
||||
{
|
||||
name: 'Home',
|
||||
url: '/welcome',
|
||||
run: waitForCookiesDialog,
|
||||
screenshot: { waitForTOCScrolling: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'sosovalue-white-paper.gitbook.io',
|
||||
contentBaseURL: 'https://sosovalue-white-paper.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
// {
|
||||
// name: 'docs.revrobotics.com',
|
||||
// contentBaseURL: 'https://docs.revrobotics.com',
|
||||
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
// },
|
||||
{
|
||||
name: 'chartschool.stockcharts.com',
|
||||
contentBaseURL: 'https://chartschool.stockcharts.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.soniclabs.com',
|
||||
contentBaseURL: 'https://docs.soniclabs.com',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.meshchain.ai',
|
||||
contentBaseURL: 'https://docs.meshchain.ai',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.thousandeyes.com',
|
||||
contentBaseURL: 'https://docs.thousandeyes.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'docs.raydium.io',
|
||||
contentBaseURL: 'https://docs.raydium.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.fluentbit.io',
|
||||
contentBaseURL: 'https://docs.fluentbit.io',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
{
|
||||
name: 'run-ai-docs.nvidia.com',
|
||||
contentBaseURL: 'https://run-ai-docs.nvidia.com',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'customers-v2',
|
||||
tests: [
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'OG Image', url: '/~gitbook/ogimage/h17zQIFwy3MaafVNmItO', mode: 'image' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
runTestCases(testCases);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,170 @@
|
||||
import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { getContentTestURL } from '../tests/utils';
|
||||
import { waitForIcons } from './util';
|
||||
|
||||
test.describe('PDF export', () => {
|
||||
test('export all pages as PDF (e2e)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
getContentTestURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?limit=10'
|
||||
)
|
||||
);
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('export all pages as PDF (GitBook docs)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(getContentTestURL('https://gitbook.com/docs/~gitbook/pdf?limit=10'));
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('export a single page as PDF (e2e)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
getContentTestURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?page=Bw7LjWwgTjV8nIV4s7rs&only=yes&limit=2'
|
||||
)
|
||||
);
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('export a single page as PDF (GitBook docs)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
getContentTestURL(
|
||||
'https://gitbook.com/docs/~gitbook/pdf?page=DfnNkU49mvLe2ythHAyx&only=yes&limit=2'
|
||||
)
|
||||
);
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,459 @@
|
||||
import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import {
|
||||
CustomizationAIMode,
|
||||
CustomizationBackground,
|
||||
CustomizationCorners,
|
||||
CustomizationDefaultFont,
|
||||
CustomizationDefaultMonospaceFont,
|
||||
CustomizationDepth,
|
||||
type CustomizationHeaderItem,
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationIconsStyle,
|
||||
CustomizationLinksStyle,
|
||||
CustomizationLocale,
|
||||
CustomizationSearchStyle,
|
||||
CustomizationSidebarBackgroundStyle,
|
||||
CustomizationSidebarListStyle,
|
||||
CustomizationTheme,
|
||||
CustomizationThemeMode,
|
||||
type CustomizationThemedColor,
|
||||
type SiteCustomizationSettings,
|
||||
SiteExternalLinksTarget,
|
||||
} from '@gitbook/api';
|
||||
import { type BrowserContext, type Page, type Response, expect, test } from '@playwright/test';
|
||||
import deepMerge from 'deepmerge';
|
||||
import rison from 'rison';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
|
||||
import { getContentTestURL, getTestURL } from '../tests/utils';
|
||||
|
||||
export interface Test {
|
||||
name: string;
|
||||
/**
|
||||
* URL to visit for testing.
|
||||
*/
|
||||
url: string | (() => string | Promise<string>);
|
||||
cookies?: Parameters<BrowserContext['addCookies']>[0];
|
||||
/**
|
||||
* Test to run
|
||||
*/
|
||||
run?: (page: Page, response: Response | null) => Promise<unknown>;
|
||||
/**
|
||||
* Mode for the test.
|
||||
*/
|
||||
mode?: 'page' | 'image';
|
||||
/**
|
||||
* Whether the test should be fullscreened during testing.
|
||||
*/
|
||||
fullPage?: boolean;
|
||||
/**
|
||||
* Whether to take a screenshot of the test or set a threshold for the screenshot.
|
||||
*/
|
||||
screenshot?:
|
||||
| false
|
||||
| {
|
||||
/**
|
||||
* Screenshot threshold.
|
||||
* From 0 to 1, where 0 is the most strict and 1 is the most permissive.
|
||||
* @default 0.5
|
||||
*/
|
||||
threshold?: number;
|
||||
/**
|
||||
* Whether to wait for the table of contents to finish scrolling before taking the screenshot.
|
||||
*/
|
||||
waitForTOCScrolling?: boolean;
|
||||
};
|
||||
/**
|
||||
* Whether to only run this test.
|
||||
*/
|
||||
only?: boolean;
|
||||
}
|
||||
|
||||
export type TestsCase = {
|
||||
name: string;
|
||||
skip?: boolean;
|
||||
tests: Array<Test>;
|
||||
contentBaseURL?: string;
|
||||
};
|
||||
|
||||
export const allLocales: CustomizationLocale[] = [
|
||||
CustomizationLocale.Fr,
|
||||
CustomizationLocale.Es,
|
||||
CustomizationLocale.Ja,
|
||||
CustomizationLocale.Zh,
|
||||
];
|
||||
|
||||
export const allThemeModes: CustomizationThemeMode[] = [
|
||||
CustomizationThemeMode.Light,
|
||||
CustomizationThemeMode.Dark,
|
||||
];
|
||||
|
||||
export const allTintColors: Array<{
|
||||
label: string;
|
||||
value: CustomizationThemedColor | undefined;
|
||||
}> = [
|
||||
{
|
||||
label: 'Off',
|
||||
value: undefined,
|
||||
},
|
||||
{ label: 'Primary', value: { light: '#346DDB', dark: '#346DDB' } },
|
||||
{ label: 'Custom', value: { light: '#C62C68', dark: '#EF96B8' } },
|
||||
];
|
||||
|
||||
export const allThemes: CustomizationTheme[] = [
|
||||
CustomizationTheme.Clean,
|
||||
CustomizationTheme.Muted,
|
||||
CustomizationTheme.Bold,
|
||||
CustomizationTheme.Gradient,
|
||||
];
|
||||
|
||||
export const allDeprecatedThemePresets: CustomizationHeaderPreset[] = [
|
||||
CustomizationHeaderPreset.Default,
|
||||
CustomizationHeaderPreset.Bold,
|
||||
CustomizationHeaderPreset.Contrast,
|
||||
CustomizationHeaderPreset.Custom,
|
||||
];
|
||||
|
||||
export const allSidebarBackgroundStyles: CustomizationSidebarBackgroundStyle[] = [
|
||||
CustomizationSidebarBackgroundStyle.Default,
|
||||
CustomizationSidebarBackgroundStyle.Filled,
|
||||
];
|
||||
|
||||
export const allSearchStyles: CustomizationSearchStyle[] = [
|
||||
CustomizationSearchStyle.Prominent,
|
||||
CustomizationSearchStyle.Subtle,
|
||||
];
|
||||
|
||||
// Common customization settings
|
||||
|
||||
export const headerLinks: CustomizationHeaderItem[] = [
|
||||
{
|
||||
title: 'Secondary button',
|
||||
to: { kind: 'url', url: 'https://www.gitbook.com' },
|
||||
style: 'button-secondary',
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
title: 'Primary button',
|
||||
to: { kind: 'url', url: 'https://www.gitbook.com' },
|
||||
style: 'button-primary',
|
||||
links: [],
|
||||
},
|
||||
];
|
||||
|
||||
export async function waitForCookiesDialog(page: Page) {
|
||||
const dialog = page.getByTestId('cookies-dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
}
|
||||
|
||||
export async function waitForNotFound(_page: Page, response: Response | null) {
|
||||
expect(response).not.toBeNull();
|
||||
expect(response?.status()).toBe(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform test cases into Playwright tests and run it.
|
||||
*/
|
||||
export function runTestCases(testCases: TestsCase[]) {
|
||||
for (const testCase of testCases) {
|
||||
if (testCase.skip) {
|
||||
continue;
|
||||
}
|
||||
|
||||
test.describe(testCase.name, () => {
|
||||
for (const testEntry of testCase.tests) {
|
||||
const { mode = 'page' } = testEntry;
|
||||
const testFn = testEntry.only ? test.only : test;
|
||||
testFn(testEntry.name, async ({ page, context }) => {
|
||||
const testEntryPathname =
|
||||
typeof testEntry.url === 'function' ? await testEntry.url() : testEntry.url;
|
||||
const url = testCase.contentBaseURL
|
||||
? getContentTestURL(
|
||||
new URL(testEntryPathname, testCase.contentBaseURL).toString()
|
||||
)
|
||||
: getTestURL(testEntryPathname);
|
||||
|
||||
if (testEntry.cookies) {
|
||||
await context.addCookies(
|
||||
testEntry.cookies.map((cookie) => ({
|
||||
...cookie,
|
||||
domain: new URL(url).host,
|
||||
path: '/',
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const response = await page.goto(url);
|
||||
if (testEntry.run) {
|
||||
await testEntry.run(page, response);
|
||||
}
|
||||
const screenshotOptions = testEntry.screenshot;
|
||||
if (screenshotOptions !== false) {
|
||||
const screenshotName = `${testCase.name} - ${testEntry.name}`;
|
||||
if (mode === 'image') {
|
||||
await argosScreenshot(page, screenshotName, {
|
||||
viewports: ['macbook-13'],
|
||||
threshold: screenshotOptions?.threshold ?? undefined,
|
||||
fullPage: true,
|
||||
});
|
||||
} else {
|
||||
await argosScreenshot(page, screenshotName, {
|
||||
viewports: ['macbook-16', 'macbook-13', 'ipad-2', 'iphone-x'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: screenshotOptions?.threshold ?? undefined,
|
||||
fullPage: testEntry.fullPage ?? false,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
if (screenshotOptions?.waitForTOCScrolling !== false) {
|
||||
await waitForTOCScrolling(page);
|
||||
}
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a URL with customization settings.
|
||||
*/
|
||||
export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettings>): string {
|
||||
// We replicate the theme migration logic from the API to the tests, because the don't get these settings from the API.
|
||||
// We can remove this once the migration to the new themes have been completed and the new theme styles are verified
|
||||
// Map the theme preset (+ tint) to one of the new themes
|
||||
const newTheme = (() => {
|
||||
if (partial.styling?.theme) {
|
||||
return partial.styling.theme;
|
||||
}
|
||||
|
||||
switch (partial.header?.preset) {
|
||||
case CustomizationHeaderPreset.Bold:
|
||||
case CustomizationHeaderPreset.Contrast:
|
||||
case CustomizationHeaderPreset.Custom:
|
||||
return CustomizationTheme.Bold;
|
||||
|
||||
case CustomizationHeaderPreset.None:
|
||||
case CustomizationHeaderPreset.Default:
|
||||
if (partial.styling?.tint) {
|
||||
return CustomizationTheme.Muted;
|
||||
}
|
||||
|
||||
return CustomizationTheme.Clean;
|
||||
default:
|
||||
return CustomizationTheme.Clean;
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Default customization settings.
|
||||
*
|
||||
* The customization object passed to the URL should be a valid API settings object. Hence we extend the test with necessary defaults.
|
||||
*/
|
||||
const DEFAULT_CUSTOMIZATION: SiteCustomizationSettings = {
|
||||
styling: {
|
||||
theme: newTheme,
|
||||
primaryColor: { light: '#346DDB', dark: '#346DDB' },
|
||||
infoColor: { light: '#787878', dark: '#787878' },
|
||||
warningColor: { light: '#FE9A00', dark: '#FE9A00' },
|
||||
dangerColor: { light: '#FB2C36', dark: '#FB2C36' },
|
||||
successColor: { light: '#00C950', dark: '#00C950' },
|
||||
corners: CustomizationCorners.Rounded,
|
||||
depth: CustomizationDepth.Subtle,
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
monospaceFont: CustomizationDefaultMonospaceFont.IBMPlexMono,
|
||||
background: CustomizationBackground.Plain,
|
||||
icons: CustomizationIconsStyle.Regular,
|
||||
links: CustomizationLinksStyle.Default,
|
||||
sidebar: {
|
||||
background: CustomizationSidebarBackgroundStyle.Default,
|
||||
list: CustomizationSidebarListStyle.Default,
|
||||
},
|
||||
search: CustomizationSearchStyle.Subtle,
|
||||
},
|
||||
internationalization: {
|
||||
locale: CustomizationLocale.En,
|
||||
},
|
||||
insights: {
|
||||
trackingCookie: true,
|
||||
},
|
||||
favicon: {},
|
||||
header: {
|
||||
preset: CustomizationHeaderPreset.Default,
|
||||
links: [],
|
||||
},
|
||||
footer: {
|
||||
groups: [],
|
||||
},
|
||||
themes: {
|
||||
default: CustomizationThemeMode.Light,
|
||||
toggeable: true,
|
||||
},
|
||||
pdf: {
|
||||
enabled: true,
|
||||
},
|
||||
feedback: {
|
||||
enabled: false,
|
||||
},
|
||||
ai: {
|
||||
mode: CustomizationAIMode.None,
|
||||
},
|
||||
externalLinks: {
|
||||
target: SiteExternalLinksTarget.Self,
|
||||
},
|
||||
advancedCustomization: {
|
||||
enabled: true,
|
||||
},
|
||||
git: {
|
||||
showEditLink: false,
|
||||
},
|
||||
pagination: {
|
||||
enabled: true,
|
||||
},
|
||||
pageActions: {
|
||||
externalAI: true,
|
||||
markdown: true,
|
||||
},
|
||||
trademark: {
|
||||
enabled: true,
|
||||
},
|
||||
privacyPolicy: {
|
||||
url: 'https://www.gitbook.com/privacy',
|
||||
},
|
||||
socialPreview: {},
|
||||
};
|
||||
|
||||
const encoded = rison.encode_object(deepMerge(DEFAULT_CUSTOMIZATION, partial));
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('customization', encoded);
|
||||
|
||||
return `?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all icons present on the page to be loaded.
|
||||
*/
|
||||
export async function waitForIcons(page: Page) {
|
||||
await page.waitForFunction(() => {
|
||||
const urlStates: Record<
|
||||
string,
|
||||
{ state: 'pending'; uri: null } | { state: 'loaded'; uri: string }
|
||||
> = (window as any).__ICONS_STATES__ || {};
|
||||
(window as any).__ICONS_STATES__ = urlStates;
|
||||
|
||||
const fetchSvgAsDataUri = async (url: string): Promise<string> => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch SVG: ${response.status}`);
|
||||
}
|
||||
|
||||
const svgText = await response.text();
|
||||
const encoded = encodeURIComponent(svgText).replace(/'/g, '%27').replace(/"/g, '%22');
|
||||
|
||||
return `data:image/svg+xml;charset=utf-8,${encoded}`;
|
||||
};
|
||||
|
||||
const loadUrl = (url: string) => {
|
||||
// Mark the URL as pending.
|
||||
urlStates[url] = { state: 'pending', uri: null };
|
||||
fetchSvgAsDataUri(url).then((uri) => {
|
||||
urlStates[url] = { state: 'loaded', uri };
|
||||
});
|
||||
};
|
||||
|
||||
const icons = Array.from(document.querySelectorAll('svg.gb-icon'));
|
||||
const results = icons.map((icon) => {
|
||||
if (!(icon instanceof SVGElement)) {
|
||||
throw new Error('Icon is not an SVGElement');
|
||||
}
|
||||
|
||||
// Ignore icons that are not visible.
|
||||
if (!icon.checkVisibility()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const state = icon.getAttribute('data-argos-state');
|
||||
|
||||
if (state === 'pending') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state === 'loaded') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/light/moon.svg?v=2&token=a463935e93")
|
||||
const maskImage = window.getComputedStyle(icon).getPropertyValue('mask-image');
|
||||
const urlMatch = maskImage.match(/url\("([^"]+)"\)/);
|
||||
const url = urlMatch?.[1];
|
||||
|
||||
// If URL is invalid we throw an error.
|
||||
if (!url) {
|
||||
throw new Error('No mask-image');
|
||||
}
|
||||
|
||||
// If the URL is already queued for loading, we return the state.
|
||||
if (urlStates[url]) {
|
||||
if (urlStates[url].state === 'loaded') {
|
||||
icon.setAttribute('data-argos-state', 'pending');
|
||||
icon.style.maskImage = `url("${urlStates[url].uri}")`;
|
||||
requestAnimationFrame(() => {
|
||||
icon.setAttribute('data-argos-state', 'loaded');
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
loadUrl(url);
|
||||
return false;
|
||||
});
|
||||
|
||||
return results.every((x) => x);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for TOC to be correctly scrolled into view.
|
||||
*/
|
||||
async function waitForTOCScrolling(page: Page) {
|
||||
const viewport = await page.viewportSize();
|
||||
if (viewport && viewport.width >= 1024) {
|
||||
const toc = page.getByTestId('table-of-contents');
|
||||
await expect(toc).toBeVisible();
|
||||
await page.evaluate(() => {
|
||||
const tocScrollContainer = document.querySelector(
|
||||
'[data-testid="table-of-contents"] [data-testid="toc-scroll-container"]'
|
||||
);
|
||||
if (!tocScrollContainer) {
|
||||
throw new Error('TOC scroll container not found');
|
||||
}
|
||||
tocScrollContainer.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
*/
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
// This is needed to throw "forbidden" when the api token expired during revalidation
|
||||
authInterrupts: true,
|
||||
useCache: true,
|
||||
|
||||
// Content is fully static, we can cache it in the session memory cache for a long time
|
||||
staleTimes: {
|
||||
dynamic: 3600, // 1 hour
|
||||
static: 3600, // 1 hour
|
||||
},
|
||||
|
||||
// Since content is fully static, we don't want to fetch on hover again
|
||||
optimisticClientCache: false,
|
||||
},
|
||||
|
||||
env: {
|
||||
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
|
||||
|
||||
// GitBook envs
|
||||
GITBOOK_API_URL: process.env.GITBOOK_API_URL,
|
||||
GITBOOK_APP_URL: process.env.GITBOOK_APP_URL,
|
||||
GITBOOK_INTEGRATIONS_HOST: process.env.GITBOOK_INTEGRATIONS_HOST,
|
||||
GITBOOK_IMAGE_RESIZE_URL: process.env.GITBOOK_IMAGE_RESIZE_URL,
|
||||
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
|
||||
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
|
||||
GITBOOK_URL: process.env.GITBOOK_URL,
|
||||
GITBOOK_API_TOKEN: process.env.GITBOOK_API_TOKEN,
|
||||
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
GITBOOK_SECRET: process.env.GITBOOK_SECRET,
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
|
||||
GITBOOK_IMAGE_RESIZE_MODE: process.env.GITBOOK_IMAGE_RESIZE_MODE,
|
||||
GITBOOK_FONTS_URL: process.env.GITBOOK_FONTS_URL,
|
||||
GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME,
|
||||
|
||||
// Next.js envs
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
|
||||
|
||||
// Used to detect if the app is running in V2 mode
|
||||
GITBOOK_V2: 'true',
|
||||
},
|
||||
|
||||
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
poweredByHeader: false,
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.gitbook.io',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/~gitbook/static/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'public, max-age=31536000, immutable',
|
||||
},
|
||||
{
|
||||
key: 'Access-Control-Allow-Origin',
|
||||
value: '*',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { OpenNextConfig } from '@opennextjs/cloudflare';
|
||||
|
||||
export default {
|
||||
default: {
|
||||
override: {
|
||||
wrapper: 'cloudflare-node',
|
||||
converter: 'edge',
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
},
|
||||
},
|
||||
middleware: {
|
||||
external: true,
|
||||
override: {
|
||||
wrapper: 'cloudflare-edge',
|
||||
converter: 'edge',
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
},
|
||||
},
|
||||
dangerous: {
|
||||
enableCacheInterception: true,
|
||||
},
|
||||
edgeExternals: ['node:crypto'],
|
||||
} satisfies OpenNextConfig;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
|
||||
//Only needed to run locally, in prod we'll use the one from do.js
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
// Only needed to run locally, in prod we'll use the one from do.js
|
||||
export class R2WriteBuffer extends DurableObject {
|
||||
writePromise;
|
||||
|
||||
async write(cacheKey, value) {
|
||||
// We are already writing to this key
|
||||
if (this.writePromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
|
||||
this.ctx.waitUntil(
|
||||
this.writePromise.finally(() => {
|
||||
this.writePromise = undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
// We can't move the handler import to the top level, otherwise the runtime will not be properly initialized
|
||||
const { handler } = await import(
|
||||
'../../.open-next/server-functions/default/handler.mjs'
|
||||
);
|
||||
|
||||
// - `Request`s are handled by the Next server
|
||||
return handler(request, env, ctx);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"main": "default.js",
|
||||
"name": "gitbook-open-v2-server",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"vars": {
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
},
|
||||
"env": {
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "dev",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-dev"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["R2WriteBuffer", "DOShardedTagCache"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
// Just as a test for the preview environment to check that everything works
|
||||
"NEXT_PRIVATE_DEBUG_CACHE": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-preview"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-staging"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-staging-tail"
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
// This is a bit misleading, but it means that we can have 500 concurrent revalidations
|
||||
// This means that we'll have up to 100 durable objects instance running at the same time
|
||||
"MAX_REVALIDATE_CONCURRENCY": "100",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-production"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-production"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-prod-tail"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// This worker only purposes it to host the different DO that we will need in the other workers.
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
|
||||
// `use cache` could cause multiple writes to the same key to happen concurrently, there is a limit of 1 write per key/second
|
||||
// so we need to buffer writes to the R2 bucket to avoid hitting this limit.
|
||||
export class R2WriteBuffer extends DurableObject {
|
||||
writePromise;
|
||||
|
||||
async write(cacheKey, value) {
|
||||
// We are already writing to this key
|
||||
if (this.writePromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
|
||||
this.ctx.waitUntil(
|
||||
this.writePromise.finally(() => {
|
||||
this.writePromise = undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
export default {
|
||||
async fetch() {
|
||||
// This worker does not handle any requests, it only provides Durable Objects
|
||||
return new Response('This worker is not meant to handle requests directly', {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"main": "do.js",
|
||||
"name": "gitbook-open-v2-do",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"env": {
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-preview"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
"STAGE": "staging",
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||
}
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-staging-tail"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-staging"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
// R2 is strongly consistent, so we can disable SQLite
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
// We don't want to pollute the memory with broken cache entries
|
||||
// Most of the time, those are fake requests.
|
||||
"NEXT_CACHE_DO_QUEUE_MAX_RETRIES": "1",
|
||||
"STAGE": "production"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-production"
|
||||
}
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-prod-tail"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-production"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { WorkerEntrypoint } from 'cloudflare:workers';
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
|
||||
import { handler as middlewareHandler } from '../../.open-next/middleware/handler.mjs';
|
||||
|
||||
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
export default class extends WorkerEntrypoint {
|
||||
async fetch(request) {
|
||||
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
|
||||
if (this.env.STAGE !== 'preview') {
|
||||
// https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#version-affinity
|
||||
reqOrResp.headers.set(
|
||||
'Cloudflare-Workers-Version-Overrides',
|
||||
`gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"`
|
||||
);
|
||||
return this.env.DEFAULT_WORKER?.fetch(reqOrResp, {
|
||||
cf: {
|
||||
cacheEverything: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
// If we are in preview mode, we need to send the request to the preview URL
|
||||
const modifiedUrl = new URL(reqOrResp.url);
|
||||
modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME;
|
||||
const nextRequest = new Request(modifiedUrl, reqOrResp);
|
||||
return fetch(nextRequest, {
|
||||
cf: {
|
||||
cacheEverything: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"main": "middleware.js",
|
||||
"name": "gitbook-open-v2",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
"assets": {
|
||||
"directory": "../../.open-next/assets",
|
||||
"binding": "ASSETS"
|
||||
},
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"vars": {
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
},
|
||||
"env": {
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "dev",
|
||||
"NEXT_PRIVATE_DEBUG_CACHE": "true",
|
||||
// When deployed locally, we don't have access to the tag cache here,
|
||||
// we should just bypass the cache to go to the server directly
|
||||
"SHOULD_BYPASS_CACHE": "true",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-dev"
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-dev"
|
||||
}
|
||||
]
|
||||
},
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
"PREVIEW_HOSTNAME": "TO_REPLACE",
|
||||
"WORKER_VERSION_ID": "TO_REPLACE"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-preview"
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-preview"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
"STAGE": "staging",
|
||||
"WORKER_VERSION_ID": "TO_REPLACE",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "open-2c.gitbook-staging.com/*",
|
||||
"zone_name": "gitbook-staging.com"
|
||||
},
|
||||
{
|
||||
"pattern": "static-2c.gitbook-staging.com/*",
|
||||
"zone_name": "gitbook-staging.com"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-staging"
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-staging"
|
||||
}
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-staging-tail"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
// This is a bit misleading, but it means that we can have 500 concurrent revalidations
|
||||
// This means that we'll have up to 100 durable objects instance running at the same time
|
||||
"MAX_REVALIDATE_CONCURRENCY": "100",
|
||||
// Temporary variable to find the issue once deployed
|
||||
// TODO: remove this once the issue is fixed
|
||||
"DEBUG_CLOUDFLARE": "true",
|
||||
"WORKER_VERSION_ID": "TO_REPLACE",
|
||||
"STAGE": "production",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "open-2c.gitbook.com/*",
|
||||
"zone_name": "gitbook.com"
|
||||
},
|
||||
{
|
||||
"pattern": "static-2c.gitbook.com/*",
|
||||
"zone_name": "gitbook.com"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-production"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-production"
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-production"
|
||||
}
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-prod-tail"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// In this script, we use the args from the cli to update the PREVIEW_URL vars in the wrangler config file for the middleware
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const wranglerConfigPath = path.join(__dirname, '../middlewareWrangler.jsonc');
|
||||
|
||||
const file = fs.readFileSync(wranglerConfigPath, 'utf-8');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
// The versionId is in the format xxx-xxx-xxx-xxx, we need the first part to reconstruct the preview URL
|
||||
const versionId = args[0];
|
||||
|
||||
// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev
|
||||
const previewHostname = `${versionId.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
|
||||
|
||||
let updatedFile = file.replace(
|
||||
/"PREVIEW_HOSTNAME": "TO_REPLACE"/,
|
||||
`"PREVIEW_HOSTNAME": "${previewHostname}"`
|
||||
);
|
||||
|
||||
updatedFile = updatedFile.replaceAll(
|
||||
/"WORKER_VERSION_ID": "TO_REPLACE"/g,
|
||||
`"WORKER_VERSION_ID": "${versionId}"`
|
||||
);
|
||||
|
||||
fs.writeFileSync(wranglerConfigPath, updatedFile);
|
||||
@@ -0,0 +1,210 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import type {
|
||||
CacheEntryType,
|
||||
CacheValue,
|
||||
IncrementalCache,
|
||||
WithLastModified,
|
||||
} from '@opennextjs/aws/types/overrides.js';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
|
||||
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
|
||||
|
||||
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
|
||||
export const DEFAULT_PREFIX = 'incremental-cache';
|
||||
|
||||
export type KeyOptions = {
|
||||
cacheType?: CacheEntryType;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace
|
||||
* the cache operations. It also integrates both R2 and Cache API in a single class.
|
||||
* Having our own, will allow us to customize it in the future if needed.
|
||||
*/
|
||||
class GitbookIncrementalCache implements IncrementalCache {
|
||||
name = 'GitbookIncrementalCache';
|
||||
|
||||
protected localCache: Cache | undefined;
|
||||
|
||||
async get<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
cacheType?: CacheType
|
||||
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
|
||||
const cacheKey = this.getR2Key(key, cacheType);
|
||||
|
||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||
const localCache = await this.getCacheInstance();
|
||||
if (!r2) throw new Error('No R2 bucket');
|
||||
if (process.env.SHOULD_BYPASS_CACHE === 'true') {
|
||||
// We are in a local middleware environment, we should bypass the cache
|
||||
// and go directly to the server.
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// Check local cache first if available
|
||||
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
|
||||
if (localCacheEntry) {
|
||||
const result = (await localCacheEntry.json()) as WithLastModified<
|
||||
CacheValue<CacheType>
|
||||
>;
|
||||
return this.returnNullOn404(result);
|
||||
}
|
||||
|
||||
const r2Object = await r2.get(cacheKey);
|
||||
if (!r2Object) return null;
|
||||
|
||||
return this.returnNullOn404({
|
||||
value: await r2Object.json(),
|
||||
lastModified: r2Object.uploaded.getTime(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to get from cache', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: This is a workaround to handle 404 responses in the cache.
|
||||
// It should be handled by OpenNext cache interception directly. This should be removed once OpenNext cache interception is fixed.
|
||||
returnNullOn404<CacheType extends CacheEntryType = 'cache'>(
|
||||
cacheEntry: WithLastModified<CacheValue<CacheType>> | null
|
||||
): WithLastModified<CacheValue<CacheType>> | null {
|
||||
if (!cacheEntry?.value) return null;
|
||||
if ('meta' in cacheEntry.value && cacheEntry.value.meta?.status === 404) {
|
||||
return null;
|
||||
}
|
||||
return cacheEntry;
|
||||
}
|
||||
|
||||
async set<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
value: CacheValue<CacheType>,
|
||||
cacheType?: CacheType
|
||||
): Promise<void> {
|
||||
const cacheKey = this.getR2Key(key, cacheType);
|
||||
|
||||
const localCache = await this.getCacheInstance();
|
||||
|
||||
try {
|
||||
await this.writeToR2(cacheKey, JSON.stringify(value));
|
||||
|
||||
//TODO: Check if there is any places where we don't have tags
|
||||
// Ideally we should always have tags, but in case we don't, we need to decide how to handle it
|
||||
// For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment
|
||||
const tags = this.getTagsFromCacheEntry(value) ?? [
|
||||
`build_id/${process.env.NEXT_BUILD_ID}`,
|
||||
];
|
||||
|
||||
// We consider R2 as the source of truth, so we update the local cache
|
||||
// only after a successful R2 write
|
||||
await localCache.put(
|
||||
this.getCacheUrlKey(cacheKey),
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
value,
|
||||
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
|
||||
// See https://developers.cloudflare.com/workers/reference/security-model/
|
||||
lastModified: Date.now(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
// Cache-Control default to 30 minutes, will be overridden by `revalidate`
|
||||
// In theory we should always get the `revalidate` value
|
||||
'cache-control': `max-age=${value.revalidate ?? 60 * 30}`,
|
||||
'cache-tag': tags.join(','),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to set to cache', e);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
const cacheKey = this.getR2Key(key);
|
||||
|
||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||
const localCache = await this.getCacheInstance();
|
||||
if (!r2) throw new Error('No R2 bucket');
|
||||
|
||||
try {
|
||||
await r2.delete(cacheKey);
|
||||
|
||||
// Here again R2 is the source of truth, so we delete from local cache first
|
||||
await localCache.delete(this.getCacheUrlKey(cacheKey));
|
||||
} catch (e) {
|
||||
console.error('Failed to delete from cache', e);
|
||||
}
|
||||
}
|
||||
|
||||
async writeToR2(key: string, value: string): Promise<void> {
|
||||
try {
|
||||
const env = getCloudflareContext().env as {
|
||||
WRITE_BUFFER: DurableObjectNamespace<
|
||||
Rpc.DurableObjectBranded & {
|
||||
write: (key: string, value: string) => Promise<void>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
const id = env.WRITE_BUFFER.idFromName(key);
|
||||
|
||||
// A stub is a client used to invoke methods on the Durable Object
|
||||
const stub = env.WRITE_BUFFER.get(id);
|
||||
|
||||
await stub.write(key, value);
|
||||
} catch {
|
||||
// We fallback to writing directly to R2
|
||||
// it can fail locally because the limit is 1Mb per args
|
||||
// It is 32Mb in production, so we should be fine
|
||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||
r2?.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
async getCacheInstance(): Promise<Cache> {
|
||||
if (this.localCache) return this.localCache;
|
||||
this.localCache = await caches.open('incremental-cache');
|
||||
return this.localCache;
|
||||
}
|
||||
|
||||
// Utility function to generate keys for R2/Cache API
|
||||
getR2Key(initialKey: string, cacheType: CacheEntryType = 'cache'): string {
|
||||
let key = initialKey;
|
||||
if (cacheType === 'composable') {
|
||||
// For composable cache, we need to discard the build ID from the key
|
||||
const [_buildId, ...restOfTheKey] = JSON.parse(initialKey);
|
||||
key = JSON.stringify([...restOfTheKey]);
|
||||
}
|
||||
|
||||
const hash = createHash('sha256').update(key).digest('hex');
|
||||
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace(
|
||||
/\/+/g,
|
||||
'/'
|
||||
);
|
||||
}
|
||||
|
||||
getCacheUrlKey(cacheKey: string): string {
|
||||
return `http://cache.local/${cacheKey}`;
|
||||
}
|
||||
|
||||
getTagsFromCacheEntry<CacheType extends CacheEntryType>(
|
||||
entry: CacheValue<CacheType>
|
||||
): string[] | undefined {
|
||||
if ('tags' in entry && entry.tags) {
|
||||
return entry.tags;
|
||||
}
|
||||
|
||||
if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) {
|
||||
const rawTags = entry.meta.headers['x-next-cache-tags'];
|
||||
if (typeof rawTags === 'string') {
|
||||
return rawTags.split(',');
|
||||
}
|
||||
}
|
||||
if ('value' in entry) {
|
||||
return entry.tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new GitbookIncrementalCache();
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Queue } from '@opennextjs/aws/types/overrides.js';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
|
||||
|
||||
export default {
|
||||
name: 'GitbookISRQueue',
|
||||
send: async (msg) => {
|
||||
const { ctx } = getCloudflareContext();
|
||||
ctx.waitUntil(doQueue.send(msg));
|
||||
},
|
||||
} satisfies Queue;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { getLogger } from '@/lib/logger';
|
||||
import type { Queue } from '@opennextjs/aws/types/overrides.js';
|
||||
|
||||
export default {
|
||||
name: 'GitbookISRQueue',
|
||||
send: async (msg) => {
|
||||
const logger = getLogger().subLogger('GitbookISRQueue');
|
||||
// We should never reach this point in the server. If that's the case, we should log it.
|
||||
logger.warn('send called on server side, this should not happen.', msg);
|
||||
},
|
||||
} satisfies Queue;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { createLogger, getLogger } from '@/lib/logger';
|
||||
import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js';
|
||||
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
|
||||
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
|
||||
|
||||
const originalTagCache = doShardedTagCache({
|
||||
baseShardSize: 12,
|
||||
regionalCache: true,
|
||||
regionalCacheTtlSec: 60 * 5 /* 5 minutes */,
|
||||
// Because we invalidate the Cache API on update, we can safely set this to true
|
||||
regionalCacheDangerouslyPersistMissingTags: true,
|
||||
shardReplication: {
|
||||
numberOfSoftReplicas: 2,
|
||||
numberOfHardReplicas: 1,
|
||||
regionalReplication: {
|
||||
defaultRegion: 'enam',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'GitbookTagCache',
|
||||
mode: 'nextMode',
|
||||
// We don't really use this one, as of now it is only used for soft tags
|
||||
getLastRevalidated: async (tags: string[]) => {
|
||||
const tagsToCheck = tags.filter(softTagFilter);
|
||||
if (tagsToCheck.length === 0) {
|
||||
return 0; // If no tags to check, return 0
|
||||
}
|
||||
|
||||
return await originalTagCache.getLastRevalidated(tagsToCheck);
|
||||
},
|
||||
hasBeenRevalidated: async (tags: string[], lastModified?: number) => {
|
||||
try {
|
||||
const tagsToCheck = tags.filter(softTagFilter);
|
||||
if (tagsToCheck.length === 0) {
|
||||
return false; // If no tags to check, return false
|
||||
}
|
||||
|
||||
return await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified);
|
||||
} catch (e) {
|
||||
createLogger('gitbookTagCache', {}).error(
|
||||
`hasBeenRevalidated - Error checking tags ${tags.join(', ')}`,
|
||||
e
|
||||
);
|
||||
return false; // In case of error, return false
|
||||
}
|
||||
},
|
||||
writeTags: async (tags: string[]) => {
|
||||
const tagsToWrite = tags.filter(softTagFilter);
|
||||
if (tagsToWrite.length === 0) {
|
||||
const logger = getLogger().subLogger('gitbookTagCache');
|
||||
logger.warn('writeTags - No valid tags to write');
|
||||
return; // If no tags to write, exit early
|
||||
}
|
||||
|
||||
await originalTagCache.writeTags(tagsToWrite);
|
||||
},
|
||||
} satisfies NextModeTagCache;
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.15.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/fonts": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@gitbook/react-contentkit": "workspace:*",
|
||||
"@gitbook/react-math": "workspace:*",
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@opennextjs/cloudflare": "^1.6.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tusbar/cache-control": "^1.0.2",
|
||||
"ai": "^4.2.2",
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.1.20",
|
||||
"classnames": "^2.5.1",
|
||||
"direction": "^2.0.1",
|
||||
"event-iterator": "^2.0.0",
|
||||
"framer-motion": "^10.16.14",
|
||||
"image-size": "^2.0.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsontoxml": "^1.0.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"katex": "^0.16.9",
|
||||
"mathjax": "^3.2.2",
|
||||
"mdast-util-from-markdown": "^2.0.2",
|
||||
"mdast-util-frontmatter": "^2.0.1",
|
||||
"mdast-util-gfm": "^3.1.0",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"memoizee": "^0.4.17",
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"next": "^15.3.2",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
"object-identity": "^0.1.2",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-map": "^7.0.3",
|
||||
"quick-lru": "^7.0.1",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "^3.2.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-remove": "^4.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"url-join": "^5.0.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"warn-once": "^0.1.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^5.0.9",
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsontoxml": "^1.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^20",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/parse-cache-control": "^1.0.4",
|
||||
"@types/psi": "^4.1.6",
|
||||
"@types/react": "18.3.13",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/rison": "^0.0.9",
|
||||
"deepmerge": "^4.3.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"postcss": "^8",
|
||||
"psi": "^4.1.0",
|
||||
"stylelint": "^16.16.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"ts-essentials": "^10.0.1",
|
||||
"typescript": "^5.5.3",
|
||||
"vercel": "^39.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math && wrangler types",
|
||||
"copy:icons": "gitbook-icons ./public/~gitbook/static/icons",
|
||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math",
|
||||
"dev": "env-cmd --silent -f ../../.env.local next",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"build:cloudflare": "opennextjs-cloudflare build",
|
||||
"dev:cloudflare": "wrangler dev --port 8771 --env preview",
|
||||
"dev:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
|
||||
"dev:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
|
||||
"e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts",
|
||||
"e2e-customers": "playwright test e2e/customers.spec.ts",
|
||||
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.3%, chrome >= 64, edge >= 79, firefox >= 67, opera >= 51, safari >= 12 and not dead"
|
||||
]
|
||||
}
|
||||
@@ -16,8 +16,10 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
contextOptions: {
|
||||
reducedMotion: 'reduce',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
# GitBook immutable static assets
|
||||
# Duplicated from next.config.mjs until OpenNext supports generating static headers
|
||||
/~gitbook/static/*
|
||||
cache-control: public,max-age=31536000,immutable
|
||||
Access-Control-Allow-Origin: *
|
||||
/_next/static/*
|
||||
Access-Control-Allow-Origin: *
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import NextError from 'next/error';
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
}) {
|
||||
console.error('Global error:', error);
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<NextError statusCode={undefined as any} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { SitePageSkeleton } from '@/components/SitePage';
|
||||
|
||||
export default function Loading() {
|
||||
return <SitePageSkeleton />;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { SitePageNotFound } from '@/components/SitePage';
|
||||
|
||||
export default async function NotFound() {
|
||||
return <SitePageNotFound />;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { type RouteParams, getDynamicSiteContext, getPagePathFromParams } from '@/app/utils';
|
||||
import {
|
||||
SitePage,
|
||||
generateSitePageMetadata,
|
||||
generateSitePageViewport,
|
||||
} from '@/components/SitePage';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<RouteParams>;
|
||||
searchParams: Promise<{ fallback?: string }>;
|
||||
};
|
||||
|
||||
export default async function Page(props: PageProps) {
|
||||
const params = await props.params;
|
||||
const { context } = await getDynamicSiteContext(params);
|
||||
const pathname = getPagePathFromParams(params);
|
||||
|
||||
return <SitePage context={context} pageParams={{ pathname }} />;
|
||||
}
|
||||
|
||||
export async function generateViewport(props: PageProps): Promise<Viewport> {
|
||||
const { context } = await getDynamicSiteContext(await props.params);
|
||||
return generateSitePageViewport(context);
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: PageProps): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const { context } = await getDynamicSiteContext(params);
|
||||
const pathname = getPagePathFromParams(params);
|
||||
|
||||
return generateSitePageMetadata({
|
||||
context,
|
||||
pageParams: { pathname },
|
||||
});
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
|
||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||
import {
|
||||
SiteLayout,
|
||||
generateSiteLayoutMetadata,
|
||||
generateSiteLayoutViewport,
|
||||
} from '@/components/SiteLayout';
|
||||
import { getThemeFromMiddleware } from '@/lib/middleware';
|
||||
import { shouldTrackEvents } from '@/lib/tracking';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
interface SiteDynamicLayoutProps {
|
||||
params: Promise<RouteLayoutParams>;
|
||||
}
|
||||
|
||||
export default async function SiteDynamicLayout({
|
||||
params,
|
||||
children,
|
||||
}: React.PropsWithChildren<SiteDynamicLayoutProps>) {
|
||||
const { context, visitorAuthClaims } = await getDynamicSiteContext(await params);
|
||||
const forcedTheme = await getThemeFromMiddleware();
|
||||
const withTracking = shouldTrackEvents(await headers());
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout forcedTheme={forcedTheme} customization={context.customization}>
|
||||
<SiteLayout
|
||||
context={context}
|
||||
forcedTheme={forcedTheme}
|
||||
withTracking={withTracking}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
>
|
||||
{children}
|
||||
</SiteLayout>
|
||||
</CustomizationRootLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateViewport({ params }: SiteDynamicLayoutProps) {
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
return generateSiteLayoutViewport(context);
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: SiteDynamicLayoutProps) {
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
return generateSiteLayoutMetadata(context);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
|
||||
import { serveIcon } from '@/routes/icon';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
) {
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
return serveIcon(context, request);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
|
||||
import type { PageIdParams } from '@/components/SitePage';
|
||||
import { serveOGImage } from '@/routes/ogimage';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<RouteLayoutParams & PageIdParams> }
|
||||
) {
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
return serveOGImage(context, await params);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
|
||||
import { PDFRootLayout } from '@/components/PDF';
|
||||
|
||||
export default async function RootLayout(props: {
|
||||
params: Promise<RouteLayoutParams>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { params, children } = props;
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
|
||||
return <PDFRootLayout context={context}>{children}</PDFRootLayout>;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
|
||||
import { PDFPage, generatePDFMetadata } from '@/components/PDF';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<RouteLayoutParams>;
|
||||
}) {
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
return generatePDFMetadata(context);
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<RouteLayoutParams>;
|
||||
searchParams: Promise<{ [key: string]: string }>;
|
||||
}) {
|
||||
const { params, searchParams } = props;
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
return <PDFPage context={context} searchParams={await searchParams} />;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { SitePageNotFound } from '@/components/SitePage';
|
||||
|
||||
export default async function NotFound() {
|
||||
return <SitePageNotFound />;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@/app/utils';
|
||||
import {
|
||||
SitePage,
|
||||
generateSitePageMetadata,
|
||||
generateSitePageViewport,
|
||||
} from '@/components/SitePage';
|
||||
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<RouteParams>;
|
||||
};
|
||||
|
||||
export default async function Page(props: PageProps) {
|
||||
const params = await props.params;
|
||||
const { context } = await getStaticSiteContext(params);
|
||||
const pathname = getPagePathFromParams(params);
|
||||
|
||||
return <SitePage context={context} pageParams={{ pathname }} />;
|
||||
}
|
||||
|
||||
export async function generateViewport(props: PageProps): Promise<Viewport> {
|
||||
const { context } = await getStaticSiteContext(await props.params);
|
||||
return generateSitePageViewport(context);
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: PageProps): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const { context } = await getStaticSiteContext(params);
|
||||
const pathname = getPagePathFromParams(params);
|
||||
|
||||
return generateSitePageMetadata({
|
||||
context,
|
||||
pageParams: { pathname },
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user