mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-15 15:15:55 +00:00
Compare commits
529 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63971b9803 | |||
| 64455b55e4 | |||
| 68308154b5 | |||
| 36af03f4d5 | |||
| cbc71a56b6 | |||
| 1420180220 | |||
| e1b2cf6d81 | |||
| c0e9c49679 | |||
| afc4bd4415 | |||
| bcfa8d8b19 | |||
| 43766d6519 | |||
| 42c17f5c74 | |||
| d655d3eece | |||
| 3500de95e8 | |||
| 7f03b94949 | |||
| f0cf060191 | |||
| 5a137e7b14 | |||
| 28f7fbaa8a | |||
| ff96bb5787 | |||
| 87841124e0 | |||
| a650b58276 | |||
| 388b20d44f | |||
| ea7e94fc51 | |||
| d30bcbabdb | |||
| fb858a1941 | |||
| cc2e615b9a | |||
| a862cde6b4 | |||
| e2b7bec162 | |||
| c1b577ecc9 | |||
| ba7ec14f76 | |||
| 9f0117723c | |||
| f72b6a20db | |||
| 2f798209bb | |||
| eec8f16424 | |||
| 885264f132 | |||
| f1a6dec951 | |||
| 44f41510fa | |||
| f220229e5e | |||
| 61b4cb8d7c | |||
| 185cdb4883 | |||
| 2cdba53451 | |||
| 950f6c36cf | |||
| ea37977b1e | |||
| 81da82aed7 | |||
| 39a3518651 | |||
| d903273759 | |||
| d130532f69 | |||
| 94fcd21f34 | |||
| 1d2db4ae2b | |||
| d270b4af9b | |||
| ccfbd3c1a8 | |||
| 5c4923557f | |||
| b1f608b865 | |||
| c7993756dd | |||
| d02465823f | |||
| 6b9f6b5b79 | |||
| df7de8fb0c | |||
| 72f5ce3985 | |||
| 04c80262e2 | |||
| 56d1b8fe2d | |||
| 054a1634ab | |||
| 6ff63f2db6 | |||
| b5ad0ce1e5 | |||
| 2ba7e54c81 | |||
| 0a858e74ad | |||
| b0c534fc2c | |||
| 250c194a02 | |||
| 5b10cde936 | |||
| 4aeb81bfa5 | |||
| 955cebf944 | |||
| 7c951ef437 | |||
| 68f0dbcc35 | |||
| 39c4f76aea | |||
| 2fe58e8cbb | |||
| 765141a4d8 | |||
| eb1bd3aa01 | |||
| b7284b0223 | |||
| 5bbd254742 | |||
| 611e28626a | |||
| 2a3bb0eb22 | |||
| 6816f0fa5a | |||
| 17386776ec | |||
| 9ee9082614 | |||
| 334cfdd522 | |||
| 8c961a5dbd | |||
| 4708956e77 | |||
| 813dd03dbb | |||
| 0522dbc61e | |||
| acb9f53156 | |||
| 6016846156 | |||
| cf1fae5837 | |||
| 7212345466 | |||
| e38caf78ff | |||
| 0a8dd67541 | |||
| ed684c19a9 | |||
| 52ab3681e3 | |||
| 9cc5a787e9 | |||
| 0003030bf5 | |||
| 9ad8f45d38 | |||
| a3855257c8 | |||
| 9169c2f053 | |||
| abe028bd38 | |||
| 12c2451f4f | |||
| 8daede5427 | |||
| 4c89aa60eb | |||
| d0a3f64ae4 | |||
| 433d66482d | |||
| bd553bcf9e | |||
| f5894bcf74 | |||
| 1b59e7ce36 | |||
| 32d6f5845d | |||
| 4f5cbfe732 | |||
| afba8fcb34 | |||
| 377a4899e4 | |||
| 938bdeb34b | |||
| d216eaa3f6 | |||
| 5ca5da0f21 | |||
| ebe6eb3b39 | |||
| 3bfe347096 | |||
| 7b38f89078 | |||
| 09b689b0ea | |||
| 2350baa75f | |||
| bc1eca815e | |||
| 78c10340e7 | |||
| c16890a3a6 | |||
| 3b4fe2827a | |||
| 7e807aabb5 | |||
| 973c74ee69 | |||
| efed0b0617 | |||
| ca3b9aca5d | |||
| e2afc07ab2 | |||
| e8fb84d362 | |||
| 57f951a7d9 | |||
| 28008667ed | |||
| f3affc3034 | |||
| 59da30f3c6 | |||
| 2db721112a | |||
| b60039b7d5 | |||
| 8fe9c9afea | |||
| 216ba7a556 | |||
| a2ff57081b | |||
| 40dbd9a14b | |||
| e5bac69c5c | |||
| 8d6598393f | |||
| 59b86eb9dc | |||
| 52c9f6da2f | |||
| dd65987c94 | |||
| ace61901a4 | |||
| e6c3c7635d | |||
| 5134d9e7f8 | |||
| 6821fb2937 | |||
| cfe4812045 | |||
| 14843ac343 | |||
| 8fb6d7465f | |||
| 0ef647586f | |||
| b4039627d9 | |||
| 4a295e6db4 | |||
| 5726999a5a | |||
| 58d7f3c140 | |||
| 07eab986d5 | |||
| 711cf38f9b | |||
| 4f7c0eea9f | |||
| f3448de2da | |||
| ec92075ef8 | |||
| 1e013cd87c | |||
| f033734308 | |||
| 50c05164f7 | |||
| 7fb9004b43 | |||
| 4721403986 | |||
| df848ef64e | |||
| 0b40ccd286 | |||
| caaa692f0b | |||
| 42d43e09c4 | |||
| 7ccbf55ba7 | |||
| 81f5bfc2f9 | |||
| a7a713bca2 | |||
| d6613c787f | |||
| 427f748e1c | |||
| 4f5fec7213 | |||
| 8c0a53ab40 | |||
| 2dfd5aab20 | |||
| 8d3c6562a2 | |||
| dae019c115 | |||
| 73e0cbb2d6 | |||
| 392f59450c | |||
| b4918f60ce | |||
| 8f7c304d58 | |||
| a3a944d7dd | |||
| 88a35ed057 | |||
| 67998b6f15 | |||
| ff6d1150a5 | |||
| 500c8cb649 | |||
| 6859f7d239 | |||
| dfa8a37be1 | |||
| 11a6511b7a | |||
| af98402655 | |||
| b7a0db3339 | |||
| 382a19885b | |||
| d99da6a3ae | |||
| 72cd0e59e6 | |||
| 42d88da73c | |||
| 2863fe0dc1 | |||
| 87218062be | |||
| 8a3910e208 | |||
| 6294bbb53c | |||
| a28a997851 | |||
| 28a9ee701e | |||
| 50e8b2ec52 | |||
| 315717fa2d | |||
| da67711c90 | |||
| fecdbe8dde | |||
| 9316ccd1f4 | |||
| c3b620e975 | |||
| 7a00880e5b | |||
| b6b597564d | |||
| d410381957 | |||
| 902c3c6c1a | |||
| 4fb2a4a6d7 | |||
| 2d64c78787 | |||
| fbfcca5dae | |||
| 664ae8bff6 | |||
| fa3d2aae08 | |||
| e7a591d9f6 | |||
| 2860beacdc | |||
| 231167d3e1 | |||
| c0ee60ec13 | |||
| f58b904ba6 | |||
| d5992f1a5c | |||
| fa12f9eeb6 | |||
| 33726c88a0 | |||
| 6aa3ff9c5e | |||
| 7d3fe23195 | |||
| 521052d84f | |||
| c730845e17 | |||
| 7b5a3616c4 | |||
| c9373efdb5 | |||
| 015615de8a | |||
| 0f867e533a | |||
| 7d5a6d2255 | |||
| a1da4f023d | |||
| 666e8426e1 | |||
| df2fa42bda | |||
| b3a7ad6d2c | |||
| 2932077bf9 | |||
| 957afd967f | |||
| 4c9a9d0416 | |||
| 40df91a363 | |||
| af66ff71ed | |||
| 6f8ecd6111 | |||
| fa3eb07617 | |||
| dc4268db64 | |||
| a0c06a72cc | |||
| 19c20c0be3 | |||
| fae34e56ac | |||
| ba0094a862 | |||
| b259009c9c | |||
| d88dd5ed8a | |||
| 57bb146075 | |||
| 4b67fe5317 | |||
| 938125e772 | |||
| 1c8d9febb3 | |||
| 223edae17d | |||
| 12a455dab5 | |||
| d00dc8ca72 | |||
| 2c85a302fc | |||
| 3460b78b45 | |||
| 95a1f652f9 | |||
| cc37e2ae15 | |||
| 08382ce62f | |||
| 80cb52a237 | |||
| c6637b09e2 | |||
| 778624af00 | |||
| e15757d01f | |||
| aed79fd6c3 | |||
| 0c973a3292 | |||
| 56c923c0ed | |||
| 7d7806df30 | |||
| cb5598dc19 | |||
| 47f01edcb8 | |||
| e6ddc0f07f | |||
| 373f18f451 | |||
| 3f292065cd | |||
| 5e975ab95b | |||
| a3ec264764 | |||
| 5d504ffa4c | |||
| 04999662db | |||
| f7a34706c7 | |||
| f328a41982 | |||
| 20ebecb114 | |||
| 5a69692f54 | |||
| 42ca7e1580 | |||
| c4ebb3fecd | |||
| 2a805cc082 | |||
| 89a5816ee4 | |||
| c3f6b8c003 | |||
| 0e201d5899 | |||
| ae5f1abc58 | |||
| 580101d04e | |||
| 81b4a4db53 | |||
| 8339e91e2b | |||
| 3119066728 | |||
| 326e28e9b0 | |||
| 90f0127ada | |||
| 0353a8186c | |||
| dd043df00f | |||
| 97b7c79bc6 | |||
| 634e0b4302 | |||
| 2a23f1f003 | |||
| 88ffdcc303 | |||
| b6b09d42af | |||
| ebc39e9db9 | |||
| 0df3849ea2 | |||
| 8ed1bda2f2 | |||
| d67699a1f1 | |||
| 3363a18856 | |||
| ad1dc0b914 | |||
| e4c7307c99 | |||
| f9c942c8ff | |||
| eeb977fc03 | |||
| 4b8a62122a | |||
| e44e947fc1 | |||
| 7588cfe1c5 | |||
| a9a643f222 | |||
| 77d6cb75be | |||
| 1942d221df | |||
| 041601ed65 | |||
| 5567281a85 | |||
| 4c3753046b | |||
| 89f8e16184 | |||
| a3e1125ced | |||
| 4dae8acfed | |||
| bc081c4344 | |||
| e4275b235d | |||
| 77397cac82 | |||
| b6a08bc679 | |||
| 116575c811 | |||
| 4ba7bd5ce1 | |||
| 223ab5377f | |||
| ae43ca146c | |||
| 42342893d6 | |||
| eedefdd5c9 | |||
| f5e152dd74 | |||
| 95ea22d9c5 | |||
| f23023ddfa | |||
| da74077954 | |||
| 29aaba5949 | |||
| daf41fc728 | |||
| b08eb1af59 | |||
| 8322cd3e35 | |||
| 444039c142 | |||
| 3b0eff7f71 | |||
| c765463e38 | |||
| e8ca25b67e | |||
| 528eee3fc6 | |||
| 168a4fa998 | |||
| 3ea75adba4 | |||
| cbd768a095 | |||
| 2d01653a78 | |||
| aa3357a57d | |||
| de53946896 | |||
| d51204148e | |||
| 319761a39e | |||
| 7fbf01a348 | |||
| c22490bd80 | |||
| 860fd6d02e | |||
| c43c524804 | |||
| 66bb938d85 | |||
| 0e6d928496 | |||
| f07982d9f8 | |||
| b92ecfad58 | |||
| 416bde70bc | |||
| 580f7ade15 | |||
| cdffd7c47b | |||
| 5a959cf041 | |||
| 31466546a2 | |||
| 90ead98eaa | |||
| 65e9cb9d5c | |||
| e59076a074 | |||
| 653920dec9 | |||
| 23cedd2d8b | |||
| 70c4182fd8 | |||
| b62b101065 | |||
| b9c929beeb | |||
| 2b6c593289 | |||
| c0b339e406 | |||
| da485f5144 | |||
| 139a8050a5 | |||
| da7b369ffd | |||
| 432da64919 | |||
| 7d0b422200 | |||
| fb90eb0acc | |||
| b647538e41 | |||
| e50797a5c1 | |||
| 5b1e01c786 | |||
| 813b2af06b | |||
| 6f71da88d8 | |||
| fa91eb7eaf | |||
| c756761591 | |||
| 434af90366 | |||
| 0f41e194cb | |||
| bd353489e9 | |||
| 77fd393f15 | |||
| 8701b5e078 | |||
| 61db166f92 | |||
| 0d9b9cea0f | |||
| 7bb37c7f0a | |||
| 5b2bf828e3 | |||
| c85d3f9752 | |||
| 4d7088bbf0 | |||
| dd6ab6044c | |||
| d236bf029c | |||
| d70d566abe | |||
| a25fdedea9 | |||
| 373183a2cd | |||
| e9fa50dfb5 | |||
| 57ca4e0e88 | |||
| bc90adb73a | |||
| 1505ddbd5a | |||
| cd99ed57fb | |||
| 27de1cd01d | |||
| ae78fc5394 | |||
| 40e8e69c6d | |||
| e84a46a558 | |||
| 18a72d4466 | |||
| d7aa5fa003 | |||
| 27532a5f41 | |||
| 48c18c03b9 | |||
| bba2e52e24 | |||
| 54ee0149e1 | |||
| 8fda9d2fab | |||
| 77a72d79ce | |||
| 73e2b472ef | |||
| 2f28cc1323 | |||
| f5516d4e21 | |||
| d2facb2db1 | |||
| 6eae764b7c | |||
| 970ef8f886 | |||
| ed0720699e | |||
| ad94d00d5f | |||
| c7843f7f52 | |||
| 1fe3286bdb | |||
| ce030fdf76 | |||
| 9ba8783952 | |||
| 2f3966cc53 | |||
| 8f65a22e13 | |||
| 2787d884cd | |||
| fc00b51683 | |||
| f80c3a7b37 | |||
| 4e5d6c7487 | |||
| 96f84b5917 | |||
| 6b8a5fdd1b | |||
| 89083ac790 | |||
| 70be2c6b2b | |||
| bcf877841b | |||
| a84b06bb19 | |||
| 24b7808d9a | |||
| 8364ee988e | |||
| 07f6e2d233 | |||
| 7212973f34 | |||
| af44e0436b | |||
| 3f9c35d505 | |||
| 2c13f8b95c | |||
| 4dab1c56e5 | |||
| 4723f03ef1 | |||
| 278689e0a0 | |||
| 886e204e68 | |||
| c59947ab39 | |||
| 4f0a772a21 | |||
| 903f962673 | |||
| 31d800e48b | |||
| c1c58eb919 | |||
| eec3eed100 | |||
| 99da8df3d1 | |||
| c9ea23919e | |||
| 16292def79 | |||
| 9108c56ca1 | |||
| b011ea0af8 | |||
| d33171b779 | |||
| 6aaeae269e | |||
| e5f0cc8663 | |||
| 9b5f971aa1 | |||
| ff3b708ad3 | |||
| e996904013 | |||
| f32bf1f15d | |||
| 6cda2dd57b | |||
| c60e9ba27f | |||
| 9bc3d504c8 | |||
| b5e4fd75e9 | |||
| a92cb49445 | |||
| 88f64b0d63 | |||
| 844059f9a5 | |||
| eaf2d68308 | |||
| 12bdbf988d | |||
| 29bab79005 | |||
| 8d5de97ddf | |||
| d9b5a898ab | |||
| f127d28675 | |||
| 65c1bdc455 | |||
| 0711a171d4 | |||
| c58c551f9e | |||
| f574858b9b | |||
| 3e1592e16b | |||
| 90604c4c3b | |||
| 9ffc3b606d | |||
| e00c87d3e6 | |||
| bb3ca9c165 | |||
| 8ee9757132 | |||
| 946a463979 | |||
| 7aad9f7ff8 | |||
| 3fe8f7c219 | |||
| 0278a14e8e | |||
| e5366d3a49 | |||
| 38e84ffe1a | |||
| 052e07a936 | |||
| 7e67ebc11d | |||
| c9dea94717 | |||
| 1344754ca9 | |||
| 0f4759fb89 | |||
| 6d77b647a4 | |||
| 5907bd9af4 | |||
| b66d04c9c5 | |||
| 3173d8ec2d | |||
| 31e381035e | |||
| 259074b81f | |||
| 9a2f5b9295 | |||
| 58cd70b0c1 | |||
| 821899a767 | |||
| bfcc119ff3 | |||
| 76c797498e |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve `Button` and `ButtonGroup` styling
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix scroll of page outline
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Support "objectivec" as alias for Objective C syntax
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Support custom AI providers
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@gitbook/browser-types": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
First version of the public package for typing script integrations.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes to `PageAside`
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Tweaks to AIChatButton and AIChatInput
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Allow integrations to provide tools to the Docs Assistant
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve vertical alignment of site items and fix floating page aside
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix search bar layout shift caused by ToC
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Emit a <link rel="alternate"> for the markdown version of a page
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Support new coverDefinitionDark for cards & image type
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Clear AI chat properly
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix bug in search highlight in GBO
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@gitbook/openapi-parser": patch
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve OpenAPI parsing errors
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix Custom logo not rendering on the published site
|
||||||
@@ -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
|
||||||
+29
-6
@@ -53,19 +53,42 @@ After forking this repository, you'll want to [create a branch](https://docs.git
|
|||||||
|
|
||||||
#### 3. Install dependencies and run the project locally
|
#### 3. Install dependencies and run the project locally
|
||||||
|
|
||||||
GitBook uses [Bun](https://bun.sh/) to run the project. Make sure you're using the specified version of `node` before running any of the development commands to ensure a smooth development experience.
|
##### 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
|
||||||
|
|
||||||
You can easily do this by running the command `nvm use`.
|
##### Setup steps:
|
||||||
|
|
||||||
To start your local version of GitBook, run the command `bun dev`.
|
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
|
#### 4. Preview your changes
|
||||||
|
|
||||||
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/`.
|
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/docs.gitbook.com` after running the development server.
|
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://docs.gitbook.com/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
|
You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
|
||||||
|
|
||||||
### Commit your update
|
### Commit your update
|
||||||
|
|
||||||
|
|||||||
@@ -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 }}"
|
||||||
@@ -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 }}"
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
name: 'Deploy vercel'
|
name: 'Deploy vercel'
|
||||||
description: 'Deploy GitBook to Vercel'
|
description: 'Deploy GitBook to Vercel'
|
||||||
inputs:
|
inputs:
|
||||||
vercel-org:
|
vercelOrg:
|
||||||
description: 'Vercel organization'
|
description: 'Vercel organization'
|
||||||
required: true
|
required: true
|
||||||
vercel-project:
|
vercelProject:
|
||||||
description: 'Vercel project'
|
description: 'Vercel project'
|
||||||
required: true
|
required: true
|
||||||
vercel-token:
|
vercelToken:
|
||||||
description: 'Vercel token'
|
description: 'Vercel token'
|
||||||
required: true
|
required: true
|
||||||
|
opItem:
|
||||||
|
description: '1Password item to load secrets from'
|
||||||
|
required: true
|
||||||
|
opServiceAccount:
|
||||||
|
description: '1Password service account token'
|
||||||
|
required: true
|
||||||
environment:
|
environment:
|
||||||
description: 'Environment to deploy to'
|
description: 'Environment to deploy to'
|
||||||
required: true
|
required: true
|
||||||
@@ -27,32 +33,48 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
- name: Sets env vars for environment
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "VERCEL_ENVIRONMENT=${{ inputs.environment }}" >> $GITHUB_ENV
|
|
||||||
- name: Pull Vercel Environment Information
|
- name: Pull Vercel Environment Information
|
||||||
run: bun run vercel pull --yes --environment=$VERCEL_ENVIRONMENT --token=${{ inputs.vercel-token }}
|
run: bun run vercel pull --yes --environment=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
VERCEL_ORG_ID: ${{ inputs.vercel-org }}
|
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||||
VERCEL_PROJECT_ID: ${{ inputs.vercel-project }}
|
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
|
- name: Build Project Artifacts
|
||||||
run: bun run vercel build --target=$VERCEL_ENVIRONMENT --token=${{ inputs.vercel-token }}
|
run: bun run vercel build --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
VERCEL_ORG_ID: ${{ inputs.vercel-org }}
|
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||||
VERCEL_PROJECT_ID: ${{ inputs.vercel-project }}
|
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||||
|
GITBOOK_RUNTIME: vercel
|
||||||
- name: Deploy Project Artifacts to Vercel
|
- name: Deploy Project Artifacts to Vercel
|
||||||
id: deploy
|
id: deploy
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --target=$VERCEL_ENVIRONMENT --token=${{ inputs.vercel-token }})
|
DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }})
|
||||||
echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"
|
echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"
|
||||||
env:
|
env:
|
||||||
VERCEL_ORG_ID: ${{ inputs.vercel-org }}
|
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||||
VERCEL_PROJECT_ID: ${{ inputs.vercel-project }}
|
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||||
- name: Outputs
|
- name: Outputs
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,6 @@ description: 'Install Bun and cache dependencies'
|
|||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
# We cache dependencies even if Bun is fast to avoid downloading fontawesome too many times
|
|
||||||
# and impacting bandwidth there.
|
|
||||||
- name: Cache bun
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.bun/install/cache
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/bun.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
- name: Setup bun
|
- name: Setup bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -7,316 +7,6 @@ on:
|
|||||||
env:
|
env:
|
||||||
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
|
||||||
name: Deploy to Cloudflare Pages
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
deployments: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
checks: write
|
|
||||||
statuses: write
|
|
||||||
outputs:
|
|
||||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
|
||||||
deployment-alias-url: ${{ steps.deploy.outputs.deployment-alias-url }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: ./.github/composite/setup-bun
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
env:
|
|
||||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
|
||||||
- name: Sets env vars for production
|
|
||||||
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 turbo gitbook#build:cloudflare
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
|
|
||||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
|
||||||
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
|
||||||
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
|
||||||
SENTRY_RELEASE: ${{ github.sha }}
|
|
||||||
- id: deploy
|
|
||||||
name: Deploy to Cloudflare
|
|
||||||
uses: cloudflare/wrangler-action@v3.14.0
|
|
||||||
with:
|
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
workingDirectory: ./
|
|
||||||
wranglerVersion: '3.82.0'
|
|
||||||
command: pages deploy ./packages/gitbook/.vercel/output/static --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
|
|
||||||
- name: Outputs
|
|
||||||
run: |
|
|
||||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
|
||||||
echo "Alias URL: ${{ steps.deploy.outputs.deployment-alias-url }}"
|
|
||||||
- name: Archive build output
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-output
|
|
||||||
path: .vercel/
|
|
||||||
# Until https://github.com/cloudflare/wrangler-action/issues/301 is done
|
|
||||||
- name: Update Deployment Status to Success
|
|
||||||
env:
|
|
||||||
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
|
|
||||||
run: |
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
|
||||||
-d '{"state": "success", "target_url": "${{ steps.deploy.outputs.deployment-url }}", "description": "Deployed Preview URL for commit", "context": "cloudflare/preview"}' \
|
|
||||||
https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}
|
|
||||||
deploy-v2-vercel:
|
|
||||||
name: Deploy v2 to Vercel
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
deployments: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
checks: write
|
|
||||||
statuses: write
|
|
||||||
outputs:
|
|
||||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Deploy ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
|
|
||||||
id: deploy
|
|
||||||
uses: ./.github/composite/deploy-vercel
|
|
||||||
with:
|
|
||||||
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
|
|
||||||
vercel-org: ${{ secrets.VERCEL_ORG_ID }}
|
|
||||||
vercel-project: ${{ secrets.VERCEL_PROJECT_ID }}
|
|
||||||
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
|
||||||
deploy-v2-vercel-staging:
|
|
||||||
name: Deploy v2 to Vercel (staging)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
deployments: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
checks: write
|
|
||||||
statuses: write
|
|
||||||
if: startsWith(github.ref, 'refs/heads/main')
|
|
||||||
outputs:
|
|
||||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Deploy staging
|
|
||||||
id: deploy
|
|
||||||
uses: ./.github/composite/deploy-vercel
|
|
||||||
with:
|
|
||||||
environment: staging
|
|
||||||
vercel-org: ${{ secrets.VERCEL_ORG_ID }}
|
|
||||||
vercel-project: ${{ secrets.VERCEL_PROJECT_ID }}
|
|
||||||
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
|
||||||
deploy-v2-cloudflare:
|
|
||||||
name: Deploy v2 to Cloudflare Worker
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
deployments: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
checks: write
|
|
||||||
statuses: write
|
|
||||||
outputs:
|
|
||||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: ./.github/composite/setup-bun
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
env:
|
|
||||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
|
||||||
- name: Sets env vars for production
|
|
||||||
run: |
|
|
||||||
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
|
|
||||||
echo "GITBOOK_URL=https://open-2c.gitbook.com" >> $GITHUB_ENV
|
|
||||||
echo "GITBOOK_ASSETS_PREFIX=https://static-2c.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 worker
|
|
||||||
run: bun run turbo build:v2:cloudflare
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
|
|
||||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
|
||||||
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
|
||||||
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
|
||||||
SENTRY_RELEASE: ${{ github.sha }}
|
|
||||||
- id: deploy
|
|
||||||
name: Deploy to Cloudflare
|
|
||||||
uses: cloudflare/wrangler-action@v3.14.0
|
|
||||||
with:
|
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
workingDirectory: ./
|
|
||||||
wranglerVersion: '3.82.0'
|
|
||||||
command: versions upload --config ./packages/gitbook-v2/wrangler.toml
|
|
||||||
- name: Outputs
|
|
||||||
run: |
|
|
||||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
|
||||||
comment-deployments:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Comment Deployments
|
|
||||||
if: always() && !startsWith(github.ref, 'refs/heads/main')
|
|
||||||
needs:
|
|
||||||
- deploy
|
|
||||||
- deploy-v2-vercel
|
|
||||||
- deploy-v2-cloudflare
|
|
||||||
steps:
|
|
||||||
- name: Find GitHub Comment
|
|
||||||
uses: peter-evans/find-comment@v3
|
|
||||||
id: fc
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
comment-author: 'github-actions[bot]'
|
|
||||||
body-includes: 'Summary of the deployments'
|
|
||||||
|
|
||||||
- name: Create or update GitHub comment
|
|
||||||
uses: peter-evans/create-or-update-comment@v4
|
|
||||||
with:
|
|
||||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
body: |
|
|
||||||
Summary of the deployments:
|
|
||||||
|
|
||||||
### Version 1 (production)
|
|
||||||
|
|
||||||
| Version | URL | Status |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Latest commit | [${{ needs.deploy.outputs.deployment-url }}](${{ needs.deploy.outputs.deployment-url }}) | ${{ needs.deploy.result == 'success' && '✅' || '❌' }} |
|
|
||||||
| PR | [${{ needs.deploy.outputs.deployment-alias-url }}](${{ needs.deploy.outputs.deployment-alias-url }}) | ${{ needs.deploy.result == 'success' && '✅' || '❌' }} |
|
|
||||||
|
|
||||||
### Version 2 (experimental)
|
|
||||||
|
|
||||||
| Version | URL | Status |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Vercel | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}](${{ needs.deploy-v2-vercel.outputs.deployment-url }}) | ${{ needs.deploy-v2-vercel.result == 'success' && '✅' || '❌' }} |
|
|
||||||
| Cloudflare | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v2-cloudflare.result == 'success' && '✅' || '❌' }} |
|
|
||||||
|
|
||||||
### Test content
|
|
||||||
|
|
||||||
| Site | v1 | v2 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| GitBook | [${{ needs.deploy.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com) |
|
|
||||||
| E2E | [${{ needs.deploy.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
|
|
||||||
edit-mode: replace
|
|
||||||
visual-testing:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Visual Testing
|
|
||||||
needs: deploy
|
|
||||||
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
|
|
||||||
- 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 }}
|
|
||||||
visual-testing-v2:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Visual Testing v2
|
|
||||||
needs: deploy-v2-vercel
|
|
||||||
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
|
|
||||||
- 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 }}/url/
|
|
||||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
|
||||||
ARGOS_BUILD_NAME: 'v2'
|
|
||||||
visual-testing-customers:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Visual Testing Customers
|
|
||||||
needs: deploy
|
|
||||||
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
|
|
||||||
- name: Setup Playwright
|
|
||||||
uses: ./.github/actions/setup-playwright
|
|
||||||
- name: Run Playwright tests
|
|
||||||
run: bun e2e-customers
|
|
||||||
env:
|
|
||||||
BASE_URL: ${{ needs.deploy.outputs.deployment-url }}
|
|
||||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
|
||||||
ARGOS_BUILD_NAME: 'customers'
|
|
||||||
visual-testing-customers-v2:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Visual Testing Customers v2
|
|
||||||
needs: deploy-v2-vercel
|
|
||||||
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
|
|
||||||
- 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 }}/url/
|
|
||||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
|
||||||
ARGOS_BUILD_NAME: 'customers-v2'
|
|
||||||
pagespeed-testing:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: PageSpeed Testing
|
|
||||||
needs: deploy
|
|
||||||
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 $DEPLOYMENT_URL
|
|
||||||
env:
|
|
||||||
DEPLOYMENT_URL: ${{needs.deploy.outputs.deployment-url}}
|
|
||||||
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
|
|
||||||
format:
|
format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Format
|
name: Format
|
||||||
|
|||||||
@@ -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 }}"
|
||||||
@@ -5,6 +5,9 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -36,22 +39,4 @@ jobs:
|
|||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
release-preview:
|
|
||||||
# For now it releases the cache-do to both preview and production
|
|
||||||
# Once we changed to deploy the app only on release, we should change `release:preview` in `cache-do`
|
|
||||||
name: Release Preview
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: ./.github/composite/setup-bun
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
env:
|
|
||||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
|
||||||
- name: Release preview packages
|
|
||||||
run: bun run release:preview
|
|
||||||
env:
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
.next
|
|
||||||
.vercel
|
|
||||||
.open-next
|
|
||||||
|
|
||||||
# Generated
|
|
||||||
packages/emoji-codepoints/index.ts
|
|
||||||
packages/gitbook/public/~gitbook/static/
|
|
||||||
packages/icons/src/data/*.json
|
|
||||||
|
|
||||||
# Build files
|
|
||||||
dist/
|
|
||||||
|
|
||||||
README.md
|
|
||||||
CHANGELOG.md
|
|
||||||
|
|
||||||
# v2
|
|
||||||
packages/gitbook-v2/public/~gitbook/static/
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 4
|
|
||||||
}
|
|
||||||
Vendored
+6
-1
@@ -9,8 +9,13 @@
|
|||||||
],
|
],
|
||||||
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
|
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
|
||||||
"prettier.enable": false,
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.organizeImports": "explicit"
|
"source.organizeImports.biome": "explicit",
|
||||||
|
"source.fixAll.biome": "explicit"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<h1 align="center">GitBook</h1>
|
<h1 align="center">GitBook</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://docs.gitbook.com/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a>
|
<a href="https://gitbook.com/docs/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a> - <a href="https://github.com/orgs/GitbookIO/discussions/categories/feature-requests">Feature requests</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -37,8 +37,8 @@ To run a local version of this project, please follow these simple steps.
|
|||||||
|
|
||||||
- Node.js (Version: >=20.6)
|
- Node.js (Version: >=20.6)
|
||||||
- Use nvm for easy Node management
|
- Use nvm for easy Node management
|
||||||
- Bun (Version: >=1.2.1)
|
- [Bun](https://bun.sh/) (Version: >=1.2.15)
|
||||||
- We use a text-based lockfile which isn't supported below 1.2.1
|
- We use a text-based lockfile which isn't supported below 1.2.15
|
||||||
|
|
||||||
### Set up
|
### Set up
|
||||||
|
|
||||||
@@ -62,20 +62,15 @@ bun install
|
|||||||
bun dev
|
bun dev
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
|
6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/url`.
|
||||||
|
|
||||||
examples:
|
examples:
|
||||||
|
|
||||||
- http://localhost:3000/docs.gitbook.com
|
- http://localhost:3000/url/gitbook.com/docs
|
||||||
- http://localhost:3000/open-source.gitbook.io/midjourney
|
- 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.
|
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.
|
||||||
|
|
||||||
### Other development commands
|
|
||||||
|
|
||||||
- `bun format`: format the code
|
|
||||||
- `bun lint`: lint the code
|
|
||||||
|
|
||||||
### CI and testing
|
### CI and testing
|
||||||
|
|
||||||
All pull-requests will be tested against both visual and performances testing to prevent regressions.
|
All pull-requests will be tested against both visual and performances testing to prevent regressions.
|
||||||
@@ -150,11 +145,11 @@ See `LICENSE` for more information.
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
```md
|
```md
|
||||||
[](https://gitbook.com/)
|
[](https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link)
|
||||||
```
|
```
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<a href="https://gitbook.com">
|
<a href="https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link">
|
||||||
<img
|
<img
|
||||||
src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"
|
src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"
|
||||||
/>
|
/>
|
||||||
|
|||||||
+11
-3
@@ -18,7 +18,11 @@
|
|||||||
"**/.vercel/**/*",
|
"**/.vercel/**/*",
|
||||||
"**/.cache/**/*",
|
"**/.cache/**/*",
|
||||||
"**/.wrangler/**/*",
|
"**/.wrangler/**/*",
|
||||||
"packages/openapi-parser/src/fixtures/**/*"
|
"packages/openapi-parser/src/fixtures/**/*",
|
||||||
|
"packages/emoji-codepoints/index.ts",
|
||||||
|
"packages/icons/src/data/*.json",
|
||||||
|
"packages/gitbook/worker-configuration.d.ts",
|
||||||
|
"**/*.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
@@ -67,7 +71,12 @@
|
|||||||
"useThrowOnlyError": "error"
|
"useThrowOnlyError": "error"
|
||||||
},
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noConsole": "warn",
|
"noConsole": {
|
||||||
|
"level": "warn",
|
||||||
|
"options": {
|
||||||
|
"allow": ["assert", "error", "warn"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"noExplicitAny": "warn",
|
"noExplicitAny": "warn",
|
||||||
"noImplicitAnyLet": "warn",
|
"noImplicitAnyLet": "warn",
|
||||||
"noConfusingVoidType": "warn",
|
"noConfusingVoidType": "warn",
|
||||||
@@ -119,7 +128,6 @@
|
|||||||
{
|
{
|
||||||
"include": [
|
"include": [
|
||||||
"packages/gitbook/**/*",
|
"packages/gitbook/**/*",
|
||||||
"packages/gitbook-v2/**/*",
|
|
||||||
"packages/react-openapi/**/*",
|
"packages/react-openapi/**/*",
|
||||||
"packages/react-math/**/*",
|
"packages/react-math/**/*",
|
||||||
"packages/react-contentkit/**/*",
|
"packages/react-contentkit/**/*",
|
||||||
|
|||||||
+11
-8
@@ -4,21 +4,20 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@changesets/cli": "^2.27.12",
|
"@changesets/cli": "^2.27.12",
|
||||||
"turbo": "^2.4.4",
|
"turbo": "^2.5.0",
|
||||||
"vercel": "^39.3.0"
|
"vercel": "^39.3.0"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.2.4",
|
"packageManager": "bun@1.2.15",
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@codemirror/state": "6.4.1",
|
"@codemirror/state": "6.4.1",
|
||||||
"react": "18.3.1",
|
"react": "^19.0.0",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "^19.0.0",
|
||||||
|
"esbuild": "0.24.2"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo run dev",
|
"dev": "turbo run dev",
|
||||||
"dev:v2": "turbo run dev:v2",
|
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:v2": "turbo run build:v2",
|
|
||||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||||
"typecheck": "turbo run typecheck",
|
"typecheck": "turbo run typecheck",
|
||||||
"format": "biome check --write ./",
|
"format": "biome check --write ./",
|
||||||
@@ -29,11 +28,15 @@
|
|||||||
"changeset": "changeset",
|
"changeset": "changeset",
|
||||||
"changeset-version": "changeset version && bun run format",
|
"changeset-version": "changeset version && bun run format",
|
||||||
"release": "turbo run release && changeset publish",
|
"release": "turbo run release && changeset publish",
|
||||||
"release:preview": "turbo run release:preview",
|
|
||||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
||||||
"clean": "turbo run clean"
|
"clean": "turbo run clean"
|
||||||
},
|
},
|
||||||
"workspaces": ["packages/*"],
|
"workspaces": {
|
||||||
|
"packages": ["packages/*"],
|
||||||
|
"catalog": {
|
||||||
|
"@gitbook/api": "^0.136.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
|
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# `@gitbook/browser-types`
|
||||||
|
|
||||||
|
Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@gitbook/browser-types",
|
||||||
|
"description": "Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@gitbook/api": "catalog:",
|
||||||
|
"@gitbook/icons": "workspace:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.5.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"files": ["dist", "README.md", "CHANGELOG.md"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
|
||||||
|
import type { IconName } from '@gitbook/icons';
|
||||||
|
|
||||||
|
export type GitBookIntegrationEvent = 'load' | 'unload';
|
||||||
|
|
||||||
|
export type GitBookIntegrationEventCallback = (...args: unknown[]) => void;
|
||||||
|
|
||||||
|
export type GitBookIntegrationTool = AIToolDefinition & {
|
||||||
|
/**
|
||||||
|
* Confirmation action to be displayed to the user before executing the tool.
|
||||||
|
*/
|
||||||
|
confirmation?: {
|
||||||
|
icon?: IconName;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when the tool is executed.
|
||||||
|
* The input is provided by the AI assistant following the input schema of the tool.
|
||||||
|
*/
|
||||||
|
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitBookAssistant = {
|
||||||
|
/**
|
||||||
|
* Name of the assistant displayed in the UI.
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon of the assistant displayed in the UI.
|
||||||
|
* Any FontAwesome icon name is supported.
|
||||||
|
* @example 'sparkle'
|
||||||
|
*/
|
||||||
|
icon: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when the assistant is opened.
|
||||||
|
*/
|
||||||
|
open: (query?: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to display the triggers for this assistant in the UI.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
ui?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitBookGlobal = {
|
||||||
|
/**
|
||||||
|
* Register an event listener.
|
||||||
|
*/
|
||||||
|
addEventListener: (
|
||||||
|
type: GitBookIntegrationEvent,
|
||||||
|
func: GitBookIntegrationEventCallback
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an event listener.
|
||||||
|
*/
|
||||||
|
removeEventListener: (
|
||||||
|
type: GitBookIntegrationEvent,
|
||||||
|
func: GitBookIntegrationEventCallback
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a custom tool to be exposed to the AI assistant.
|
||||||
|
*/
|
||||||
|
registerTool: (tool: GitBookIntegrationTool) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a custom assistant to be available on the site.
|
||||||
|
* Returns a registration object that lets the integration signal readiness
|
||||||
|
* and dispose the assistant when no longer needed.
|
||||||
|
*/
|
||||||
|
registerAssistant: (assistant: GitBookAssistant) => GitBookAssistantRegistration;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
/**
|
||||||
|
* Global `window.GitBook` object accessible by integrations.
|
||||||
|
*/
|
||||||
|
GitBook?: GitBookGlobal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GitBookAssistantRegistration = ((this: void) => void) & {
|
||||||
|
/**
|
||||||
|
* Call this when the integration has finished initializing and is ready to
|
||||||
|
* receive `open` calls.
|
||||||
|
*/
|
||||||
|
ready: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister the assistant.
|
||||||
|
*/
|
||||||
|
dispose: () => void;
|
||||||
|
};
|
||||||
@@ -13,11 +13,12 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"types": [
|
"types": [
|
||||||
"bun-types" // add Bun global
|
"bun-types" // add Bun global
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.wrangler
|
|
||||||
worker-configuration.d.ts
|
|
||||||
dist/
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# @gitbook/cache-do
|
|
||||||
|
|
||||||
## 0.1.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- b7a5106: Disable cloudflare observability in production
|
|
||||||
|
|
||||||
## 0.1.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 9b8d519: Experiment with optimizing billable duration in Cloudflare by using multiple RPC sessions instead of one
|
|
||||||
- 636b868: First version of a new cache backend powered by Cloudflare Durable Objects
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 56f5fa1: Enable Workers observability with a sampling of 0.1
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# `@gitbook/cache-do`
|
|
||||||
|
|
||||||
Cache backend, powered by Cloudflare Durable Objects. The cache is optimized for GitBook use-cases.
|
|
||||||
|
|
||||||
### Performances
|
|
||||||
|
|
||||||
The cache backend is optimized for performances by being distributed and accessible close to the worker locations that are reading it.
|
|
||||||
|
|
||||||
### Geo-distribution
|
|
||||||
|
|
||||||
To achieve a good balance between **performances** and **consistency**, cache objects are distributed over 7 locations, representing continents.
|
|
||||||
|
|
||||||
It makes it possible to purge all 7 locations in one go and achieve fast consistency.
|
|
||||||
|
|
||||||
### Concepts
|
|
||||||
|
|
||||||
**Cache tag**: unique tag in the cache environment. A cache tag groups multiple keys that should be purged together in one operation.
|
|
||||||
Cache tags should not contain a large set of unique keys. Exceeding thousands could lead to performances or reliability issues.
|
|
||||||
|
|
||||||
**Cache key**: unique key in the cache environment. Each key should be assigned to a `tag`.
|
|
||||||
|
|
||||||
**Location**: cache is distributed over 7 unique locations, one for each continent.
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@gitbook/cache-do",
|
|
||||||
"type": "module",
|
|
||||||
"private": true,
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"development": "./src/index.ts",
|
|
||||||
"default": "./dist/index.js"
|
|
||||||
},
|
|
||||||
"./api": {
|
|
||||||
"types": "./dist/api.d.ts",
|
|
||||||
"development": "./src/api.ts",
|
|
||||||
"default": "./dist/api.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"version": "0.1.1",
|
|
||||||
"dependencies": {
|
|
||||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
|
||||||
"lru_map": "^0.4.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.5.3",
|
|
||||||
"wrangler": "^3.109.2"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"generate": "wrangler types --experimental-include-runtime",
|
|
||||||
"build": "tsc",
|
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"dev": "tsc -w",
|
|
||||||
"release": "wrangler deploy",
|
|
||||||
"release:preview": "wrangler deploy && wrangler deploy --env preview"
|
|
||||||
},
|
|
||||||
"files": ["dist", "src", "bin", "data", "README.md", "CHANGELOG.md"]
|
|
||||||
}
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
import { DurableObject } from 'cloudflare:workers';
|
|
||||||
import { decode, encode } from '@msgpack/msgpack';
|
|
||||||
import { LRUMap } from 'lru_map';
|
|
||||||
|
|
||||||
export interface CacheObjectDescriptor {
|
|
||||||
get: <Value = unknown>(key: string) => Promise<Value | undefined>;
|
|
||||||
set: <Value = unknown>(key: string, value: Value, expiresAt: number) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Value stored in a chunked binary msgpack format.
|
|
||||||
* Stored under the key `prop.${key}.${index}`.
|
|
||||||
*/
|
|
||||||
interface CacheObjectProp<Value = unknown> {
|
|
||||||
value: Value;
|
|
||||||
expiresAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expiration clock stored under the key `exp.${expiresAt}.${key}`.
|
|
||||||
*/
|
|
||||||
interface CacheObjectExp {
|
|
||||||
/** Key of the property */
|
|
||||||
k: string;
|
|
||||||
/** Number of chunks */
|
|
||||||
c: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Durable Object class being deployed as a distributed cache.
|
|
||||||
*/
|
|
||||||
export class CacheObject extends DurableObject {
|
|
||||||
private lru = new LRUMap<string, { match: CacheObjectProp | undefined }>(500);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a descriptor to access the cache object.
|
|
||||||
* The goal is to minimize the amount of RPC sessions between the client and the cache object.
|
|
||||||
* One session is opened per request on the client side and used to perform multiple operations.
|
|
||||||
* https://developers.cloudflare.com/workers/runtime-apis/rpc/#return-functions-from-rpc-methods
|
|
||||||
*/
|
|
||||||
public open(): CacheObjectDescriptor {
|
|
||||||
return {
|
|
||||||
get: async <Value = unknown>(key: string) => {
|
|
||||||
return this.get<Value>(key);
|
|
||||||
},
|
|
||||||
set: async <Value = unknown>(key: string, value: Value, expiresAt: number) => {
|
|
||||||
await this.set(key, value, expiresAt);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value of a property.
|
|
||||||
*/
|
|
||||||
public async get<Value = unknown>(key: string) {
|
|
||||||
return this.logOperation({ operation: 'get', key }, async (setLog) => {
|
|
||||||
// Try the memory state first.
|
|
||||||
const memoryEntry = this.lru.get(key);
|
|
||||||
if (memoryEntry) {
|
|
||||||
setLog({ memory: true });
|
|
||||||
setLog({ memoryMatch: !!memoryEntry.match });
|
|
||||||
if (!memoryEntry.match) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isExpired = memoryEntry.match.expiresAt < Date.now();
|
|
||||||
setLog({ memoryExpired: isExpired });
|
|
||||||
|
|
||||||
if (!isExpired) {
|
|
||||||
return memoryEntry.match.value as Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.getFromStorage<Value>(key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value of a property from the DO storage.
|
|
||||||
*/
|
|
||||||
public async getFromStorage<Value = unknown>(key: string) {
|
|
||||||
return this.logOperation({ operation: 'getFromStorage', key }, async (setLog) => {
|
|
||||||
const entries = await this.ctx.storage.list<Uint8Array>({
|
|
||||||
prefix: getStoragePropKey(key),
|
|
||||||
noCache: true,
|
|
||||||
});
|
|
||||||
if (entries.size) {
|
|
||||||
const entry = decodeChunks<CacheObjectProp<Value>>(entries);
|
|
||||||
setLog({ chunks: entries.size, chunksSize: entry?.size ?? 0 });
|
|
||||||
if (entry && entry.value.expiresAt > Date.now()) {
|
|
||||||
// Found
|
|
||||||
this.lru.set(key, { match: entry.value });
|
|
||||||
return entry.value.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not found
|
|
||||||
this.lru.set(key, { match: undefined });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a value in the cache object.
|
|
||||||
*/
|
|
||||||
public async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
|
|
||||||
return this.logOperation({ operation: 'set', key }, async (setLog) => {
|
|
||||||
const prop: CacheObjectProp<Value> = {
|
|
||||||
value,
|
|
||||||
expiresAt,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.lru.set(key, { match: prop });
|
|
||||||
await this.ctx.storage.transaction(async (tx) => {
|
|
||||||
const entries = encodeChunks(key, prop);
|
|
||||||
const chunks = Object.keys(entries).length;
|
|
||||||
setLog({ chunks });
|
|
||||||
|
|
||||||
const clockValue: CacheObjectExp = {
|
|
||||||
k: key,
|
|
||||||
c: chunks,
|
|
||||||
};
|
|
||||||
|
|
||||||
await tx.put(getGCClockKey(key, expiresAt), clockValue);
|
|
||||||
await tx.put(entries);
|
|
||||||
|
|
||||||
const currentAlarm = await tx.getAlarm();
|
|
||||||
if (!currentAlarm) {
|
|
||||||
// Set an alarm to garbage collect all entries that have expired in 12h.
|
|
||||||
await tx.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Purge all keys in the cache object.
|
|
||||||
*/
|
|
||||||
public async purge() {
|
|
||||||
return this.logOperation({ operation: 'purge' }, async (setLog) => {
|
|
||||||
const result = new Set<string>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// List all the keys in the cache object.
|
|
||||||
const entries = await this.ctx.storage.list<CacheObjectExp>({
|
|
||||||
prefix: 'exp.',
|
|
||||||
noCache: true,
|
|
||||||
});
|
|
||||||
setLog({ entries: entries.size });
|
|
||||||
entries.forEach((exp) => {
|
|
||||||
result.add(exp.k);
|
|
||||||
});
|
|
||||||
} catch (_error) {}
|
|
||||||
|
|
||||||
await this.reset();
|
|
||||||
return Array.from(result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alarm to garbage collect all entries that have expired.
|
|
||||||
*/
|
|
||||||
async alarm() {
|
|
||||||
return this.logOperation({ operation: 'alarm' }, async (setLog) => {
|
|
||||||
try {
|
|
||||||
const entries = await this.ctx.storage.list<CacheObjectExp>({
|
|
||||||
prefix: 'exp.',
|
|
||||||
noCache: true,
|
|
||||||
});
|
|
||||||
setLog({ entries: entries.size });
|
|
||||||
const toDeleteSet = new Set<string>();
|
|
||||||
|
|
||||||
for (const [key, exp] of entries) {
|
|
||||||
const timestamp = Number.parseInt(key.split('.')[1]);
|
|
||||||
if (timestamp < Date.now()) {
|
|
||||||
toDeleteSet.add(key);
|
|
||||||
for (let i = 0; i < exp.c; i++) {
|
|
||||||
toDeleteSet.add(getStoragePropChunkKey(exp.k, i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the keys by batch of 128.
|
|
||||||
const toDelete = Array.from(toDeleteSet);
|
|
||||||
setLog({ toDelete: toDelete.length });
|
|
||||||
for (let i = 0; i < toDelete.length; i += 128) {
|
|
||||||
await this.ctx.storage.delete(toDelete.slice(i, i + 128));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are still keys to delete, set an alarm to continue the deletion in 12h.
|
|
||||||
if (toDelete.length) {
|
|
||||||
await this.ctx.storage.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
|
|
||||||
}
|
|
||||||
} catch (_error) {
|
|
||||||
await this.reset();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the cache object.
|
|
||||||
*/
|
|
||||||
async reset() {
|
|
||||||
return this.logOperation({ operation: 'reset' }, async () => {
|
|
||||||
this.lru.clear();
|
|
||||||
await this.ctx.storage.deleteAll();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time and log an operation.
|
|
||||||
*/
|
|
||||||
async logOperation<T>(
|
|
||||||
log: Record<string, unknown>,
|
|
||||||
fn: (update: (log: Record<string, unknown>) => void) => Promise<T>
|
|
||||||
): Promise<T> {
|
|
||||||
const objectId = this.ctx.id.name ?? this.ctx.id.toString();
|
|
||||||
const update: Record<string, unknown> = {};
|
|
||||||
const start = performance.now();
|
|
||||||
try {
|
|
||||||
return await fn((arg) => {
|
|
||||||
Object.assign(update, arg);
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
const duration = performance.now() - start;
|
|
||||||
console.log({ ...log, ...update, objectId, duration });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStoragePropKey(key: string): string {
|
|
||||||
return `prop.${key}.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStoragePropChunkKey(key: string, index: number): string {
|
|
||||||
return `${getStoragePropKey(key)}${index}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGCClockRootKey(timestamp: number): string {
|
|
||||||
return `exp.${timestamp}.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGCClockKey(key: string, expiresAt: number): string {
|
|
||||||
return `${getGCClockRootKey(expiresAt)}${key}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeChunks<T>(key: string, value: T): Record<string, Uint8Array> {
|
|
||||||
const buf = encode(value);
|
|
||||||
const entries: Record<string, Uint8Array> = {};
|
|
||||||
const chunks = chunkUint8Array(buf, 128 * 1024);
|
|
||||||
|
|
||||||
for (let index = 0; index < chunks.length; index++) {
|
|
||||||
entries[getStoragePropChunkKey(key, index)] = chunks[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeChunks<T>(entries: Map<string, Uint8Array>): { value: T; size: number } | undefined {
|
|
||||||
const chunks = Array.from(entries.entries())
|
|
||||||
.map(([key, value]) => {
|
|
||||||
const index = Number.parseInt(key.split('.').pop()!);
|
|
||||||
return [index, value] as const;
|
|
||||||
})
|
|
||||||
.sort(([a], [b]) => a - b)
|
|
||||||
.map(([, value]) => value);
|
|
||||||
|
|
||||||
if (chunks.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buf = mergeUint8Array(chunks);
|
|
||||||
return { value: decode(buf) as T, size: buf.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
function chunkUint8Array(input: Uint8Array, chunkSize: number): Uint8Array[] {
|
|
||||||
const chunks: Uint8Array[] = [];
|
|
||||||
for (let i = 0; i < input.length; i += chunkSize) {
|
|
||||||
chunks.push(input.slice(i, i + chunkSize));
|
|
||||||
}
|
|
||||||
return chunks;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeUint8Array(chunks: Uint8Array[]): Uint8Array {
|
|
||||||
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
||||||
const result = new Uint8Array(totalLength);
|
|
||||||
let offset = 0;
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
result.set(chunk, offset);
|
|
||||||
offset += chunk.length;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import type { CacheObject } from './CacheObject';
|
|
||||||
|
|
||||||
export type CacheLocationId = ContinentCode;
|
|
||||||
const allLocations: CacheLocationId[] = ['AF', 'AS', 'NA', 'SA', 'AN', 'EU', 'OC'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Location hint for the CacheObject durable object.
|
|
||||||
*/
|
|
||||||
const doLocationHints: {
|
|
||||||
[key in CacheLocationId]: DurableObjectLocationHint;
|
|
||||||
} = {
|
|
||||||
AF: 'afr',
|
|
||||||
AS: 'apac',
|
|
||||||
NA: 'wnam',
|
|
||||||
SA: 'sam',
|
|
||||||
AN: 'oc',
|
|
||||||
EU: 'weur',
|
|
||||||
OC: 'oc',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client to access a cache tag.
|
|
||||||
*/
|
|
||||||
export class CacheObjectStub {
|
|
||||||
private stub: DurableObjectStub<CacheObject>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
/** Binding to the CacheObject durable object */
|
|
||||||
private doNamespace: DurableObjectNamespace<CacheObject>,
|
|
||||||
/** ID of the location to target */
|
|
||||||
private locationId: CacheLocationId,
|
|
||||||
/** Name of the tag */
|
|
||||||
private tag: string
|
|
||||||
) {
|
|
||||||
const groupId = getCacheObjectIdName(this.locationId, this.tag);
|
|
||||||
this.stub = this.doNamespace.get(this.doNamespace.idFromName(groupId), {
|
|
||||||
// Initialize the object with a locaiton hint,
|
|
||||||
// as we might want to purge all locations before the object is created.
|
|
||||||
// https://developers.cloudflare.com/durable-objects/reference/data-location/
|
|
||||||
locationHint: doLocationHints[this.locationId],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a descriptor to the cache object.
|
|
||||||
* It can be used to perform multiple operations in a single RPC session.
|
|
||||||
* Ex:
|
|
||||||
* ```ts
|
|
||||||
* using desc = cache.open();
|
|
||||||
* await desc.set('key', 'value', Date.now() + 1000);
|
|
||||||
* await desc.get('key');
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
async open() {
|
|
||||||
return await this.stub.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a value from the cache.
|
|
||||||
*/
|
|
||||||
async get<Value = unknown>(key: string) {
|
|
||||||
return (await this.stub.get(key)) as Value | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a value in the cache.
|
|
||||||
*/
|
|
||||||
async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
|
|
||||||
return await this.stub.set(key, value, expiresAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Purge all keys in the cache tag.
|
|
||||||
*/
|
|
||||||
async purge() {
|
|
||||||
const keys = new Set<string>();
|
|
||||||
await Promise.all(
|
|
||||||
allLocations.map(async (locationId) => {
|
|
||||||
const groupId = getCacheObjectIdName(locationId, this.tag);
|
|
||||||
const cacheGroup = this.doNamespace.get(this.doNamespace.idFromName(groupId), {
|
|
||||||
// Initialize the object with a locaiton hint,
|
|
||||||
// as we might want to purge all locations before the object is created.
|
|
||||||
// https://developers.cloudflare.com/durable-objects/reference/data-location/
|
|
||||||
locationHint: doLocationHints[this.locationId],
|
|
||||||
});
|
|
||||||
const locationkeys = await cacheGroup.purge();
|
|
||||||
locationkeys.forEach((key) => keys.add(key));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCacheObjectIdName(locationId: CacheLocationId, tag: string): string {
|
|
||||||
return `${locationId}:${tag}`;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './CacheObjectStub';
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { WorkerEntrypoint } from 'cloudflare:workers';
|
|
||||||
|
|
||||||
export * from './CacheObject';
|
|
||||||
|
|
||||||
export default class Worker extends WorkerEntrypoint {
|
|
||||||
fetch() {
|
|
||||||
return new Response('Hello, world!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
main = "./src/index.ts"
|
|
||||||
name = "gitbook-open-cache"
|
|
||||||
compatibility_date = "2024-09-02"
|
|
||||||
|
|
||||||
durable_objects.bindings = [
|
|
||||||
{name = "CACHE", class_name = "CacheObject"}
|
|
||||||
]
|
|
||||||
|
|
||||||
migrations = [
|
|
||||||
{tag = "v1", new_classes = ["CacheObject"]}
|
|
||||||
]
|
|
||||||
|
|
||||||
[observability]
|
|
||||||
enabled = false
|
|
||||||
|
|
||||||
[env.preview]
|
|
||||||
name = "gitbook-open-cache-preview"
|
|
||||||
durable_objects.bindings = [
|
|
||||||
{name = "CACHE", class_name = "CacheObject"}
|
|
||||||
]
|
|
||||||
migrations = [
|
|
||||||
{tag = "v1", new_classes = ["CacheObject"]}
|
|
||||||
]
|
|
||||||
|
|
||||||
[env.preview.observability]
|
|
||||||
enabled = true
|
|
||||||
head_sampling_rate = 0.1
|
|
||||||
@@ -1,5 +1,23 @@
|
|||||||
# @gitbook/cache-tags
|
# @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
|
## 0.1.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "0.1.0",
|
"version": "0.3.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "0.96.1",
|
"@gitbook/api": "catalog:",
|
||||||
"assert-never": "^1.2.1"
|
"assert-never": "^1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function getCacheTag(
|
|||||||
| {
|
| {
|
||||||
tag: 'computed-document';
|
tag: 'computed-document';
|
||||||
space: string;
|
space: string;
|
||||||
integration: string;
|
sourceType: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* All data related to the URL of a content
|
* All data related to the URL of a content
|
||||||
@@ -84,6 +84,14 @@ export function getCacheTag(
|
|||||||
organization: string;
|
organization: string;
|
||||||
openAPISpec: string;
|
openAPISpec: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* All data related to a translation
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
tag: 'translation';
|
||||||
|
organization: string;
|
||||||
|
translation: string;
|
||||||
|
}
|
||||||
): string {
|
): string {
|
||||||
switch (spec.tag) {
|
switch (spec.tag) {
|
||||||
case 'user':
|
case 'user':
|
||||||
@@ -99,18 +107,31 @@ export function getCacheTag(
|
|||||||
case 'document':
|
case 'document':
|
||||||
return `space:${spec.space}:document:${spec.document}`;
|
return `space:${spec.space}:document:${spec.document}`;
|
||||||
case 'computed-document':
|
case 'computed-document':
|
||||||
return `space:${spec.space}:computed-document:${spec.integration}`;
|
return `space:${spec.space}:computed-document:${spec.sourceType}`;
|
||||||
case 'site':
|
case 'site':
|
||||||
return `site:${spec.site}`;
|
return `site:${spec.site}`;
|
||||||
case 'integration':
|
case 'integration':
|
||||||
return `integration:${spec.integration}`;
|
return `integration:${spec.integration}`;
|
||||||
case 'openapi':
|
case 'openapi':
|
||||||
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
|
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
|
||||||
|
case 'translation':
|
||||||
|
return `organization:${spec.organization}:translation:${spec.translation}`;
|
||||||
default:
|
default:
|
||||||
assertNever(spec);
|
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.
|
* Get the tags for a computed content source.
|
||||||
*/
|
*/
|
||||||
@@ -123,6 +144,10 @@ export function getComputedContentSourceCacheTags(
|
|||||||
) {
|
) {
|
||||||
const tags: string[] = [];
|
const tags: string[] = [];
|
||||||
|
|
||||||
|
if (!('dependencies' in source)) {
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
// We add the dependencies as tags, to ensure that the computed content is invalidated
|
// We add the dependencies as tags, to ensure that the computed content is invalidated
|
||||||
// when the dependencies are updated.
|
// when the dependencies are updated.
|
||||||
const dependencies = Object.values(source.dependencies ?? {});
|
const dependencies = Object.values(source.dependencies ?? {});
|
||||||
@@ -146,6 +171,15 @@ export function getComputedContentSourceCacheTags(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'translation':
|
||||||
|
tags.push(
|
||||||
|
getCacheTag({
|
||||||
|
tag: 'translation',
|
||||||
|
organization: inContext.organizationId,
|
||||||
|
translation: dependency.ref.translation,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Do not throw for unknown dependency types
|
// Do not throw for unknown dependency types
|
||||||
// as it might mean we are lacking behind the API version
|
// as it might mean we are lacking behind the API version
|
||||||
@@ -158,18 +192,22 @@ export function getComputedContentSourceCacheTags(
|
|||||||
getCacheTag({
|
getCacheTag({
|
||||||
tag: 'computed-document',
|
tag: 'computed-document',
|
||||||
space: inContext.spaceId,
|
space: inContext.spaceId,
|
||||||
integration: source.integration,
|
sourceType: source.type,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We invalidate the computed content when a new version of the integration is deployed.
|
// 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(
|
tags.push(
|
||||||
getCacheTag({
|
getCacheTag({
|
||||||
tag: 'integration',
|
tag: 'integration',
|
||||||
integration: source.integration,
|
integration,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
# @gitbook/colors
|
# @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
|
## 0.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "0.2.0",
|
"version": "0.3.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ export function colorScale(
|
|||||||
const mixColor = mix?.color ? rgbToOklch(hexToRgbArray(mix.color)) : null;
|
const mixColor = mix?.color ? rgbToOklch(hexToRgbArray(mix.color)) : null;
|
||||||
const foregroundColor = rgbToOklch(hexToRgbArray(foreground));
|
const foregroundColor = rgbToOklch(hexToRgbArray(foreground));
|
||||||
const backgroundColor = rgbToOklch(hexToRgbArray(background));
|
const backgroundColor = rgbToOklch(hexToRgbArray(background));
|
||||||
|
let mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
|
||||||
|
|
||||||
if (mixColor && mix?.ratio && mix.ratio > 0) {
|
if (mixColor && mix?.ratio && mix.ratio > 0) {
|
||||||
// If defined, we mix in a (tiny) bit of the mix color with the base color.
|
// If defined, we mix in a (tiny) bit of the mix color with the base color.
|
||||||
@@ -192,7 +193,20 @@ export function colorScale(
|
|||||||
baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H;
|
baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
|
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 = [];
|
const result = [];
|
||||||
|
|
||||||
@@ -200,13 +214,31 @@ export function colorScale(
|
|||||||
const targetL =
|
const targetL =
|
||||||
foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
|
foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
|
||||||
|
|
||||||
if (index === 8 && !mix && Math.abs(baseColor.L - targetL) < 0.2) {
|
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.
|
// Original colour is close enough to target, so let's use the original colour as step 9.
|
||||||
result.push(hex);
|
result.push(hex);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chromaRatio = index < 8 ? (index + 1) * 0.05 : 1;
|
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 = {
|
const shade = {
|
||||||
L: targetL, // Blend lightness
|
L: targetL, // Blend lightness
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"lib": ["es2023"],
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"module": "ESNext",
|
||||||
"allowJs": true,
|
"target": "es2022",
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowJs": true,
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"types": ["./.wrangler/types/runtime.d.ts"]
|
"types": [
|
||||||
|
"bun-types" // add Bun global
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": ["//"],
|
||||||
|
"tasks": {
|
||||||
|
"generate": {
|
||||||
|
"inputs": ["bin/**/*", "package.json"],
|
||||||
|
"outputs": ["src/data/*.json", "dist/data/*.json"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# cloudflare
|
|
||||||
.open-next
|
|
||||||
|
|
||||||
# Symbolic links
|
|
||||||
public
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# gitbook-v2
|
|
||||||
|
|
||||||
## 0.1.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 05ffd0e: Improving data cache management for computed content
|
|
||||||
- Updated dependencies [05ffd0e]
|
|
||||||
- @gitbook/cache-tags@0.1.0
|
|
||||||
|
|
||||||
## 0.1.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 3e11678: fix: lost section groups
|
|
||||||
|
|
||||||
## 0.1.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- cfccc44: Setup structure and deployment for new version
|
|
||||||
Vendored
-5
@@ -1,5 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {import('next').NextConfig}
|
|
||||||
*/
|
|
||||||
const nextConfig = {
|
|
||||||
experimental: {
|
|
||||||
useCache: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
env: {
|
|
||||||
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
|
|
||||||
GITBOOK_URL: process.env.GITBOOK_URL,
|
|
||||||
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
|
||||||
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
|
|
||||||
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// default open-next.config.ts file created by @opennextjs/cloudflare
|
|
||||||
|
|
||||||
import cache from '@opennextjs/cloudflare/kvCache';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
default: {
|
|
||||||
override: {
|
|
||||||
wrapper: 'cloudflare-node',
|
|
||||||
converter: 'edge',
|
|
||||||
incrementalCache: async () => cache,
|
|
||||||
tagCache: 'dummy',
|
|
||||||
queue: 'dummy',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
middleware: {
|
|
||||||
external: true,
|
|
||||||
override: {
|
|
||||||
wrapper: 'cloudflare-edge',
|
|
||||||
converter: 'edge',
|
|
||||||
proxyExternalRequest: 'fetch',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
dangerous: {
|
|
||||||
enableCacheInterception: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "gitbook-v2",
|
|
||||||
"version": "0.1.2",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"next": "^15.2.0",
|
|
||||||
"react": "^19.0.0",
|
|
||||||
"react-dom": "^19.0.0",
|
|
||||||
"@gitbook/api": "0.96.1",
|
|
||||||
"@gitbook/cache-tags": "workspace:*",
|
|
||||||
"@sindresorhus/fnv1a": "^3.1.0",
|
|
||||||
"server-only": "^0.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"gitbook": "*",
|
|
||||||
"@opennextjs/cloudflare": "^0.5.3",
|
|
||||||
"tailwindcss": "^3.4.0",
|
|
||||||
"postcss": "^8"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"generate": "rm -rf ./public && cp -r ../gitbook/public ./public",
|
|
||||||
"dev:v2": "env-cmd --silent -f ../../.env.local next --turbopack",
|
|
||||||
"build": "next build",
|
|
||||||
"build:v2": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"build:v2:cloudflare": "opennextjs-cloudflare",
|
|
||||||
"dev:v2:cloudflare": "wrangler dev --port 8771",
|
|
||||||
"unit": "bun test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
import { serveLLMsTxt } from '@/routes/llms';
|
|
||||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
_request: NextRequest,
|
|
||||||
{ params }: { params: Promise<RouteLayoutParams> }
|
|
||||||
) {
|
|
||||||
const context = await getDynamicSiteContext(await params);
|
|
||||||
return serveLLMsTxt(context);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
import { serveRobotsTxt } from '@/routes/robots';
|
|
||||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
_request: NextRequest,
|
|
||||||
{ params }: { params: Promise<RouteLayoutParams> }
|
|
||||||
) {
|
|
||||||
const context = await getDynamicSiteContext(await params);
|
|
||||||
return serveRobotsTxt(context);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
import { servePagesSitemap } from '@/routes/sitemap';
|
|
||||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
_request: NextRequest,
|
|
||||||
{ params }: { params: Promise<RouteLayoutParams> }
|
|
||||||
) {
|
|
||||||
const context = await getDynamicSiteContext(await params);
|
|
||||||
return servePagesSitemap(context);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
import { serveRootSitemap } from '@/routes/sitemap';
|
|
||||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
_request: NextRequest,
|
|
||||||
{ params }: { params: Promise<RouteLayoutParams> }
|
|
||||||
) {
|
|
||||||
const context = await getDynamicSiteContext(await params);
|
|
||||||
return serveRootSitemap(context);
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import {
|
|
||||||
fetchSiteContextByURL,
|
|
||||||
fetchSiteContextByURLLookup,
|
|
||||||
getBaseContext,
|
|
||||||
} from '@v2/lib/context';
|
|
||||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
|
||||||
|
|
||||||
export type RouteParamMode = 'url-host' | 'url';
|
|
||||||
|
|
||||||
export type RouteLayoutParams = {
|
|
||||||
mode: string;
|
|
||||||
|
|
||||||
/** URL encoded site URL */
|
|
||||||
siteURL: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RouteParams = RouteLayoutParams & {
|
|
||||||
pagePath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the static context when rendering statically a site.
|
|
||||||
*/
|
|
||||||
export function getStaticSiteContext(params: RouteLayoutParams) {
|
|
||||||
const siteURL = getSiteURLFromParams(params);
|
|
||||||
return fetchSiteContextByURL(
|
|
||||||
getBaseContext({
|
|
||||||
siteURL,
|
|
||||||
urlMode: getModeFromParams(params.mode),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
url: siteURL.toString(),
|
|
||||||
visitorAuthToken: null,
|
|
||||||
redirectOnError: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the site context when rendering dynamically.
|
|
||||||
* The context will depend on the request.
|
|
||||||
*/
|
|
||||||
export async function getDynamicSiteContext(params: RouteLayoutParams) {
|
|
||||||
const siteURL = getSiteURLFromParams(params);
|
|
||||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
|
||||||
|
|
||||||
return fetchSiteContextByURLLookup(
|
|
||||||
getBaseContext({
|
|
||||||
siteURL,
|
|
||||||
urlMode: getModeFromParams(params.mode),
|
|
||||||
}),
|
|
||||||
siteURLData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the decoded page path from the params.
|
|
||||||
*/
|
|
||||||
export function getPagePathFromParams(params: RouteParams) {
|
|
||||||
const decoded = decodeURIComponent(params.pagePath);
|
|
||||||
return decoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSiteURLFromParams(params: RouteLayoutParams) {
|
|
||||||
const decoded = decodeURIComponent(params.siteURL);
|
|
||||||
const url = new URL(`https://${decoded}`);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModeFromParams(mode: string): RouteParamMode {
|
|
||||||
if (mode === 'url-host') {
|
|
||||||
return 'url-host';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'url';
|
|
||||||
}
|
|
||||||
@@ -1,587 +0,0 @@
|
|||||||
import { type ComputedContentSource, GitBookAPI } from '@gitbook/api';
|
|
||||||
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
|
|
||||||
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
|
|
||||||
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
|
|
||||||
import type { GitBookDataFetcher } from './types';
|
|
||||||
|
|
||||||
interface DataFetcherInput {
|
|
||||||
/**
|
|
||||||
* API host to use.
|
|
||||||
*/
|
|
||||||
apiEndpoint: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API token.
|
|
||||||
*/
|
|
||||||
apiToken: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const commonInput: DataFetcherInput = {
|
|
||||||
apiEndpoint: GITBOOK_API_URL,
|
|
||||||
apiToken: GITBOOK_API_TOKEN,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a data fetcher using an API token.
|
|
||||||
* The data are being cached by Next.js built-in cache.
|
|
||||||
*/
|
|
||||||
export function createDataFetcher(input: DataFetcherInput = commonInput): GitBookDataFetcher {
|
|
||||||
return {
|
|
||||||
apiEndpoint: input.apiEndpoint,
|
|
||||||
|
|
||||||
async api() {
|
|
||||||
return getAPI(input);
|
|
||||||
},
|
|
||||||
|
|
||||||
withToken({ apiToken }) {
|
|
||||||
return createDataFetcher({
|
|
||||||
...input,
|
|
||||||
apiToken,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// API that are tied to the token
|
|
||||||
//
|
|
||||||
getPublishedContentSite(params) {
|
|
||||||
return getPublishedContentSite(input, {
|
|
||||||
organizationId: params.organizationId,
|
|
||||||
siteId: params.siteId,
|
|
||||||
siteShareKey: params.siteShareKey,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getSiteRedirectBySource(params) {
|
|
||||||
return getSiteRedirectBySource(input, {
|
|
||||||
organizationId: params.organizationId,
|
|
||||||
siteId: params.siteId,
|
|
||||||
siteShareKey: params.siteShareKey,
|
|
||||||
source: params.source,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getRevision(params) {
|
|
||||||
return getRevision(input, {
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
revisionId: params.revisionId,
|
|
||||||
metadata: params.metadata,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getRevisionPages(params) {
|
|
||||||
return getRevisionPages(input, {
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
revisionId: params.revisionId,
|
|
||||||
metadata: params.metadata,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getRevisionFile(params) {
|
|
||||||
return getRevisionFile(input, {
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
revisionId: params.revisionId,
|
|
||||||
fileId: params.fileId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getRevisionPageByPath(params) {
|
|
||||||
return getRevisionPageByPath(input, {
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
revisionId: params.revisionId,
|
|
||||||
path: params.path,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getReusableContent(params) {
|
|
||||||
return getReusableContent(input, {
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
revisionId: params.revisionId,
|
|
||||||
reusableContentId: params.reusableContentId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getLatestOpenAPISpecVersionContent(params) {
|
|
||||||
return getLatestOpenAPISpecVersionContent(input, {
|
|
||||||
organizationId: params.organizationId,
|
|
||||||
slug: params.slug,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getSpace(params) {
|
|
||||||
return getSpace(input, {
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
shareKey: params.shareKey,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getChangeRequest(params) {
|
|
||||||
return getChangeRequest(input, {
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
changeRequestId: params.changeRequestId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getDocument(params) {
|
|
||||||
return getDocument(input, {
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
documentId: params.documentId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getComputedDocument(params) {
|
|
||||||
return getComputedDocument(input, {
|
|
||||||
organizationId: params.organizationId,
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
source: params.source,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getEmbedByUrl(params) {
|
|
||||||
return getEmbedByUrl(input, {
|
|
||||||
url: params.url,
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
searchSiteContent(params) {
|
|
||||||
return searchSiteContent(input, params);
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// API that are not tied to the token
|
|
||||||
// where the data is the same for all users
|
|
||||||
//
|
|
||||||
getUserById(userId) {
|
|
||||||
return getUserById(commonInput, userId);
|
|
||||||
},
|
|
||||||
getPublishedContentByUrl(params) {
|
|
||||||
return getPublishedContentByUrl(commonInput, {
|
|
||||||
url: params.url,
|
|
||||||
visitorAuthToken: params.visitorAuthToken,
|
|
||||||
redirectOnError: params.redirectOnError,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getUserById(input: DataFetcherInput, userId: string) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('days');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getAPI(input).users.getUserById(userId);
|
|
||||||
return res.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (checkHasErrorCode(error, 404)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSpace(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
spaceId: string;
|
|
||||||
shareKey: string | undefined;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('days');
|
|
||||||
cacheTag(
|
|
||||||
getCacheTag({
|
|
||||||
tag: 'space',
|
|
||||||
space: params.spaceId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await getAPI(input).spaces.getSpaceById(params.spaceId, {
|
|
||||||
shareKey: params.shareKey,
|
|
||||||
});
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getChangeRequest(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
spaceId: string;
|
|
||||||
changeRequestId: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('minutes');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getAPI(input).spaces.getChangeRequestById(
|
|
||||||
params.spaceId,
|
|
||||||
params.changeRequestId
|
|
||||||
);
|
|
||||||
cacheTag(
|
|
||||||
getCacheTag({
|
|
||||||
tag: 'change-request',
|
|
||||||
space: params.spaceId,
|
|
||||||
changeRequest: res.data.id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return res.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (checkHasErrorCode(error, 404)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRevision(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
spaceId: string;
|
|
||||||
revisionId: string;
|
|
||||||
metadata: boolean;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('max');
|
|
||||||
|
|
||||||
const res = await getAPI(input).spaces.getRevisionById(params.spaceId, params.revisionId, {
|
|
||||||
metadata: params.metadata,
|
|
||||||
});
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRevisionPages(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
spaceId: string;
|
|
||||||
revisionId: string;
|
|
||||||
metadata: boolean;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('max');
|
|
||||||
|
|
||||||
const res = await getAPI(input).spaces.listPagesInRevisionById(
|
|
||||||
params.spaceId,
|
|
||||||
params.revisionId,
|
|
||||||
{
|
|
||||||
metadata: params.metadata,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return res.data.pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRevisionFile(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
spaceId: string;
|
|
||||||
revisionId: string;
|
|
||||||
fileId: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('max');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getAPI(input).spaces.getFileInRevisionById(
|
|
||||||
params.spaceId,
|
|
||||||
params.revisionId,
|
|
||||||
params.fileId
|
|
||||||
);
|
|
||||||
return res.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (checkHasErrorCode(error, 404)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRevisionPageByPath(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
spaceId: string;
|
|
||||||
revisionId: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('max');
|
|
||||||
|
|
||||||
const encodedPath = encodeURIComponent(params.path);
|
|
||||||
try {
|
|
||||||
const res = await getAPI(input).spaces.getPageInRevisionByPath(
|
|
||||||
params.spaceId,
|
|
||||||
params.revisionId,
|
|
||||||
encodedPath
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (checkHasErrorCode(error, 404)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDocument(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
spaceId: string;
|
|
||||||
documentId: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('max');
|
|
||||||
|
|
||||||
const res = await getAPI(input).spaces.getDocumentById(params.spaceId, params.documentId);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getComputedDocument(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
spaceId: string;
|
|
||||||
organizationId: string;
|
|
||||||
source: ComputedContentSource;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('days');
|
|
||||||
|
|
||||||
cacheTag(
|
|
||||||
...getComputedContentSourceCacheTags(
|
|
||||||
{
|
|
||||||
spaceId: params.spaceId,
|
|
||||||
organizationId: params.organizationId,
|
|
||||||
},
|
|
||||||
params.source
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, {
|
|
||||||
source: params.source,
|
|
||||||
});
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getReusableContent(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
spaceId: string;
|
|
||||||
revisionId: string;
|
|
||||||
reusableContentId: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('max');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getAPI(input).spaces.getReusableContentInRevisionById(
|
|
||||||
params.spaceId,
|
|
||||||
params.revisionId,
|
|
||||||
params.reusableContentId
|
|
||||||
);
|
|
||||||
return res.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (checkHasErrorCode(error, 404)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLatestOpenAPISpecVersionContent(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
organizationId: string;
|
|
||||||
slug: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheTag(
|
|
||||||
getCacheTag({
|
|
||||||
tag: 'openapi',
|
|
||||||
organization: params.organizationId,
|
|
||||||
openAPISpec: params.slug,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
cacheLife('days');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getAPI(input).orgs.getLatestOpenApiSpecVersionContent(
|
|
||||||
params.organizationId,
|
|
||||||
params.slug
|
|
||||||
);
|
|
||||||
return res.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (checkHasErrorCode(error, 404)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPublishedContentByUrl(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
url: string;
|
|
||||||
visitorAuthToken: string | null;
|
|
||||||
redirectOnError: boolean;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
const { url, visitorAuthToken, redirectOnError } = params;
|
|
||||||
|
|
||||||
const hostname = new URL(url).hostname;
|
|
||||||
cacheTag(
|
|
||||||
getCacheTag({
|
|
||||||
tag: 'url',
|
|
||||||
hostname,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
cacheLife('days');
|
|
||||||
|
|
||||||
const res = await getAPI(input).urls.getPublishedContentByUrl({
|
|
||||||
url,
|
|
||||||
visitorAuthToken: visitorAuthToken ?? undefined,
|
|
||||||
redirectOnError,
|
|
||||||
});
|
|
||||||
|
|
||||||
if ('site' in res.data) {
|
|
||||||
cacheTag(
|
|
||||||
getCacheTag({
|
|
||||||
tag: 'site',
|
|
||||||
site: res.data.site,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPublishedContentSite(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
organizationId: string;
|
|
||||||
siteId: string;
|
|
||||||
siteShareKey: string | undefined;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheTag(
|
|
||||||
getCacheTag({
|
|
||||||
tag: 'site',
|
|
||||||
site: params.siteId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
cacheLife('days');
|
|
||||||
|
|
||||||
const res = await getAPI(input).orgs.getPublishedContentSite(
|
|
||||||
params.organizationId,
|
|
||||||
params.siteId,
|
|
||||||
{
|
|
||||||
shareKey: params.siteShareKey,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSiteRedirectBySource(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
organizationId: string;
|
|
||||||
siteId: string;
|
|
||||||
siteShareKey: string | undefined;
|
|
||||||
source: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheTag(
|
|
||||||
getCacheTag({
|
|
||||||
tag: 'site',
|
|
||||||
site: params.siteId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
cacheLife('days');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getAPI(input).orgs.getSiteRedirectBySource(
|
|
||||||
params.organizationId,
|
|
||||||
params.siteId,
|
|
||||||
{
|
|
||||||
shareKey: params.siteShareKey,
|
|
||||||
source: params.source,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
} catch (error) {
|
|
||||||
// 422 is returned when the source is invalid
|
|
||||||
// we don't want to throw but just return null
|
|
||||||
if (checkHasErrorCode(error, 422)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkHasErrorCode(error, 404)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEmbedByUrl(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: {
|
|
||||||
url: string;
|
|
||||||
spaceId: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
cacheLife('weeks');
|
|
||||||
|
|
||||||
const api = getAPI(input);
|
|
||||||
const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url });
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchSiteContent(
|
|
||||||
input: DataFetcherInput,
|
|
||||||
params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
|
|
||||||
) {
|
|
||||||
'use cache';
|
|
||||||
|
|
||||||
const { organizationId, siteId, query, scope } = params;
|
|
||||||
|
|
||||||
cacheLife('days');
|
|
||||||
|
|
||||||
const res = await getAPI(input).orgs.searchSiteContent(organizationId, siteId, {
|
|
||||||
query,
|
|
||||||
...scope,
|
|
||||||
});
|
|
||||||
return res.data.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAPI(input: DataFetcherInput) {
|
|
||||||
const { apiEndpoint, apiToken } = input;
|
|
||||||
const api = new GitBookAPI({
|
|
||||||
authToken: apiToken ?? undefined,
|
|
||||||
endpoint: apiEndpoint,
|
|
||||||
userAgent: GITBOOK_USER_AGENT,
|
|
||||||
});
|
|
||||||
|
|
||||||
return api;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkHasErrorCode(error: unknown, code: number) {
|
|
||||||
return error instanceof Error && 'code' in error && error.code === code;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './api';
|
|
||||||
export * from './types';
|
|
||||||
export * from './utils';
|
|
||||||
export * from './urls';
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { RevisionPageDocument, Space } from '@gitbook/api';
|
|
||||||
import type { GitBookDataFetcher } from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the document for a page.
|
|
||||||
*/
|
|
||||||
export async function getPageDocument(
|
|
||||||
dataFetcher: GitBookDataFetcher,
|
|
||||||
space: Space,
|
|
||||||
page: RevisionPageDocument
|
|
||||||
) {
|
|
||||||
if (page.documentId) {
|
|
||||||
return dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId });
|
|
||||||
}
|
|
||||||
if (page.computed) {
|
|
||||||
return dataFetcher.getComputedDocument({
|
|
||||||
organizationId: space.organization,
|
|
||||||
spaceId: space.id,
|
|
||||||
source: page.computed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
/**
|
|
||||||
* Main host on which GitBook is running.
|
|
||||||
*/
|
|
||||||
export const GITBOOK_URL =
|
|
||||||
process.env.NODE_ENV === 'development'
|
|
||||||
? 'http://localhost:3000'
|
|
||||||
: ((process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : undefined) ??
|
|
||||||
process.env.GITBOOK_URL ??
|
|
||||||
'');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* URL at which static assets are served.
|
|
||||||
*/
|
|
||||||
export const GITBOOK_ASSETS_URL =
|
|
||||||
process.env.NODE_ENV === 'development'
|
|
||||||
? 'http://localhost:3000'
|
|
||||||
: (process.env.GITBOOK_ASSETS_PREFIX ?? GITBOOK_URL);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GitBook app URL.
|
|
||||||
*/
|
|
||||||
export const GITBOOK_APP_URL = process.env.NEXT_PUBLIC_GITBOOK_APP_URL ?? 'https://app.gitbook.com';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default GitBook API URL endpoint.
|
|
||||||
*/
|
|
||||||
export const GITBOOK_API_URL = process.env.GITBOOK_API_URL ?? 'https://api.gitbook.com';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default GitBook API token.
|
|
||||||
* It can be use to avoid rate-limiting.
|
|
||||||
*/
|
|
||||||
export const GITBOOK_API_TOKEN = process.env.GITBOOK_API_TOKEN ?? null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User agent to use for API requests.
|
|
||||||
*/
|
|
||||||
export const GITBOOK_USER_AGENT = process.env.GITBOOK_USER_AGENT ?? 'GitBook-Open/2.0.0';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to disable tracking of events into site insights.
|
|
||||||
* This is used to disable tracking in development mode.
|
|
||||||
*/
|
|
||||||
export const GITBOOK_DISABLE_TRACKING = Boolean(
|
|
||||||
!!process.env.GITBOOK_DISABLE_TRACKING || process.env.NODE_ENV !== 'production'
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hostname serving the integrations.
|
|
||||||
*/
|
|
||||||
export const GITBOOK_INTEGRATIONS_HOST =
|
|
||||||
process.env.GITBOOK_INTEGRATIONS_HOST ?? 'integrations.gitbook.com';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Endpoint to use for resizing images.
|
|
||||||
* It should be a Cloudflare domain with image resizing enabled.
|
|
||||||
*/
|
|
||||||
export const GITBOOK_IMAGE_RESIZE_URL = process.env.GITBOOK_IMAGE_RESIZE_URL ?? null;
|
|
||||||
export const GITBOOK_IMAGE_RESIZE_SIGNING_KEY =
|
|
||||||
process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY ?? null;
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
import 'server-only';
|
|
||||||
|
|
||||||
import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env';
|
|
||||||
import type { GitBookSpaceLinker } from '../links';
|
|
||||||
import { type SignatureVersion, generateImageSignature } from './signatures';
|
|
||||||
import type { ImageResizer } from './types';
|
|
||||||
|
|
||||||
interface CloudflareImageJsonFormat {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
original: {
|
|
||||||
file_size: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
format: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developers.cloudflare.com/images/image-resizing/resize-with-workers/
|
|
||||||
*/
|
|
||||||
export interface CloudflareImageOptions {
|
|
||||||
format?: 'webp' | 'avif' | 'json' | 'jpeg';
|
|
||||||
fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
dpr?: number;
|
|
||||||
anim?: boolean;
|
|
||||||
quality?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an image resizer for a rendering context.
|
|
||||||
*/
|
|
||||||
export function createImageResizer({
|
|
||||||
host,
|
|
||||||
linker,
|
|
||||||
}: {
|
|
||||||
/** The linker to use to create URLs. */
|
|
||||||
linker: GitBookSpaceLinker;
|
|
||||||
/** The host name of the current site. */
|
|
||||||
host: string;
|
|
||||||
}): ImageResizer {
|
|
||||||
if (!GITBOOK_IMAGE_RESIZE_URL || !GITBOOK_IMAGE_RESIZE_SIGNING_KEY) {
|
|
||||||
return createNoopImageResizer();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getResizedImageURL: (urlInput) => {
|
|
||||||
if (!checkIsSizableImageURL(urlInput)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cachedSignature: {
|
|
||||||
signature: string;
|
|
||||||
version: SignatureVersion;
|
|
||||||
} | null = null;
|
|
||||||
|
|
||||||
return async (options) => {
|
|
||||||
cachedSignature ??= await generateImageSignature({
|
|
||||||
host,
|
|
||||||
url: urlInput,
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = new URL(
|
|
||||||
linker.toAbsoluteURL(linker.toPathInContent('/~gitbook/image'))
|
|
||||||
);
|
|
||||||
url.searchParams.set('url', getImageAPIUrl(urlInput));
|
|
||||||
|
|
||||||
if (options.width) {
|
|
||||||
url.searchParams.set('width', options.width.toString());
|
|
||||||
}
|
|
||||||
if (options.height) {
|
|
||||||
url.searchParams.set('height', options.height.toString());
|
|
||||||
}
|
|
||||||
if (options.dpr) {
|
|
||||||
url.searchParams.set('dpr', options.dpr.toString());
|
|
||||||
}
|
|
||||||
if (options.quality) {
|
|
||||||
url.searchParams.set('quality', options.quality.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
url.searchParams.set('sign', cachedSignature.signature);
|
|
||||||
url.searchParams.set('sv', cachedSignature.version);
|
|
||||||
|
|
||||||
return url.toString();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getImageSize: async (input, options) => {
|
|
||||||
if (!checkIsSizableImageURL(input)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getImageSize(input, options);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an image resizer that doesn't do any resizing.
|
|
||||||
*/
|
|
||||||
export function createNoopImageResizer(): ImageResizer {
|
|
||||||
return {
|
|
||||||
getResizedImageURL: () => null,
|
|
||||||
getImageSize: async (_input) => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a URL is an HTTP URL.
|
|
||||||
*/
|
|
||||||
export function checkIsHttpURL(input: string | URL): boolean {
|
|
||||||
if (!URL.canParse(input)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const parsed = new URL(input);
|
|
||||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an image URL is resizable.
|
|
||||||
* Skip it for non-http(s) URLs (data, etc).
|
|
||||||
* Skip it for SVGs.
|
|
||||||
* Skip it for GitBook images (to avoid recursion).
|
|
||||||
*/
|
|
||||||
export function checkIsSizableImageURL(input: string): boolean {
|
|
||||||
if (!URL.canParse(input)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.includes('/~gitbook/image')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = new URL(input);
|
|
||||||
if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!checkIsHttpURL(parsed)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the size of an image.
|
|
||||||
*/
|
|
||||||
export async function getImageSize(
|
|
||||||
input: string,
|
|
||||||
defaultSize: Partial<CloudflareImageOptions> = {}
|
|
||||||
): Promise<{ width: number; height: number } | null> {
|
|
||||||
if (!checkIsSizableImageURL(input)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await resizeImage(input, {
|
|
||||||
// Abort the request after 2 seconds to avoid blocking rendering for too long
|
|
||||||
signal: AbortSignal.timeout(2000),
|
|
||||||
// Measure size and resize it to the most common size
|
|
||||||
// to optimize caching
|
|
||||||
...defaultSize,
|
|
||||||
format: 'json',
|
|
||||||
anim: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const json = (await response.json()) as CloudflareImageJsonFormat;
|
|
||||||
return {
|
|
||||||
width: json.original.width,
|
|
||||||
height: json.original.height,
|
|
||||||
};
|
|
||||||
} catch (_error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a Cloudflare Image Resize operation on an image.
|
|
||||||
*/
|
|
||||||
export async function resizeImage(
|
|
||||||
input: string,
|
|
||||||
options: CloudflareImageOptions & {
|
|
||||||
signal?: AbortSignal;
|
|
||||||
}
|
|
||||||
): Promise<Response> {
|
|
||||||
const { signal, ...resizeOptions } = options;
|
|
||||||
|
|
||||||
const parsed = new URL(input);
|
|
||||||
if (parsed.protocol === 'data:') {
|
|
||||||
throw new Error('Cannot resize data: URLs');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsed.hostname === 'localhost') {
|
|
||||||
throw new Error('Cannot resize localhost URLs');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since Cloudflare Images options on fetch are not supported on Cloudflare Pages,
|
|
||||||
// we need to use the Cloudflare Image Resize API directly.
|
|
||||||
if (!GITBOOK_IMAGE_RESIZE_URL) {
|
|
||||||
throw new Error('GITBOOK_IMAGE_RESIZE_URL is not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await fetch(
|
|
||||||
`${GITBOOK_IMAGE_RESIZE_URL}${stringifyOptions(
|
|
||||||
resizeOptions
|
|
||||||
)}/${encodeURIComponent(input)}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
// Pass the `Accept` header, as Cloudflare uses this to validate the format.
|
|
||||||
Accept:
|
|
||||||
resizeOptions.format === 'json'
|
|
||||||
? 'application/json'
|
|
||||||
: `image/${resizeOptions.format || 'jpeg'}`,
|
|
||||||
},
|
|
||||||
signal,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyOptions(options: CloudflareImageOptions): string {
|
|
||||||
return Object.entries({ ...options }).reduce((rest, [key, value]) => {
|
|
||||||
return `${rest}${rest ? ',' : ''}${key}=${value}`;
|
|
||||||
}, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Because of a bug in Cloudflare, 127.0.0.1 is replaced by localhost.
|
|
||||||
* We protect against it by converting to a special token, and then parsing
|
|
||||||
* the token in the image API.
|
|
||||||
*/
|
|
||||||
const GITBOOK_LOCALHOST_TOKEN = '$GITBOOK_LOCALHOST$';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare a URL for the GitBook Open Image API.
|
|
||||||
*/
|
|
||||||
export function getImageAPIUrl(url: string): string {
|
|
||||||
return url.replaceAll('127.0.0.1', GITBOOK_LOCALHOST_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseImageAPIURL(url: string): string {
|
|
||||||
return url.replaceAll(GITBOOK_LOCALHOST_TOKEN, '127.0.0.1');
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { describe, expect, it } from 'bun:test';
|
|
||||||
import { appendBasePathToLinker, createLinker } from './links';
|
|
||||||
|
|
||||||
const root = createLinker({
|
|
||||||
host: 'docs.company.com',
|
|
||||||
pathname: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
const variantInSection = createLinker({
|
|
||||||
host: 'docs.company.com',
|
|
||||||
pathname: '/section/variant',
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('toPathInContent', () => {
|
|
||||||
it('should return the correct path', () => {
|
|
||||||
expect(root.toPathInContent('some/path')).toBe('/some/path');
|
|
||||||
expect(variantInSection.toPathInContent('some/path')).toBe('/section/variant/some/path');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle leading slash', () => {
|
|
||||||
expect(root.toPathInContent('/some/path')).toBe('/some/path');
|
|
||||||
expect(variantInSection.toPathInContent('/some/path')).toBe('/section/variant/some/path');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('toAbsoluteURL', () => {
|
|
||||||
it('should return the correct path', () => {
|
|
||||||
expect(root.toAbsoluteURL('some/path')).toBe('https://docs.company.com/some/path');
|
|
||||||
expect(variantInSection.toAbsoluteURL('some/path')).toBe(
|
|
||||||
'https://docs.company.com/some/path'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('appendBasePathToLinker', () => {
|
|
||||||
const prefixedRoot = appendBasePathToLinker(root, '/section/variant');
|
|
||||||
const prefixedVariantInSection = appendBasePathToLinker(variantInSection, '/base');
|
|
||||||
|
|
||||||
describe('toPathInContent', () => {
|
|
||||||
it('should return the correct path', () => {
|
|
||||||
expect(prefixedRoot.toPathInContent('some/path')).toBe('/section/variant/some/path');
|
|
||||||
expect(prefixedVariantInSection.toPathInContent('some/path')).toBe(
|
|
||||||
'/section/variant/base/some/path'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('toAbsoluteURL', () => {
|
|
||||||
it('should return the correct path', () => {
|
|
||||||
expect(prefixedRoot.toAbsoluteURL('some/path')).toBe(
|
|
||||||
'https://docs.company.com/some/path'
|
|
||||||
);
|
|
||||||
expect(prefixedVariantInSection.toAbsoluteURL('some/path')).toBe(
|
|
||||||
'https://docs.company.com/some/path'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { getPagePath } from '@/lib/pages';
|
|
||||||
import type { RevisionPage, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic interface to generate links based on a given context.
|
|
||||||
*
|
|
||||||
* URL levels:
|
|
||||||
*
|
|
||||||
* https://docs.company.com/section/variant/page
|
|
||||||
*
|
|
||||||
* toPathInContent('some/path') => /section/variant/some/path
|
|
||||||
* toPathForPage({ pages, page }) => /section/variant/some/path
|
|
||||||
* toAbsoluteURL('some/path') => https://docs.company.com/some/path
|
|
||||||
*/
|
|
||||||
export interface GitBookSpaceLinker {
|
|
||||||
/**
|
|
||||||
* Generate an absolute path for a relative path to the current content.
|
|
||||||
*/
|
|
||||||
toPathInContent(relativePath: string): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an absolute path for a page in the current content.
|
|
||||||
* The result should NOT be passed to `toPathInContent`.
|
|
||||||
*/
|
|
||||||
toPathForPage(input: {
|
|
||||||
pages: RevisionPage[];
|
|
||||||
page: RevisionPageDocument | RevisionPageGroup;
|
|
||||||
anchor?: string;
|
|
||||||
}): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an absolute URL for a given path relative to the host of the current content.
|
|
||||||
*/
|
|
||||||
toAbsoluteURL(absolutePath: string): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a link (URL or path) for a GitBook content URL (url of another site)
|
|
||||||
*/
|
|
||||||
toLinkForContent(url: string): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a linker to resolve links in a context being served on a specific URL.
|
|
||||||
*/
|
|
||||||
export function createLinker(
|
|
||||||
/** Where the top of the space is served on */
|
|
||||||
servedOn: {
|
|
||||||
protocol?: string;
|
|
||||||
host: string;
|
|
||||||
pathname: string;
|
|
||||||
}
|
|
||||||
): GitBookSpaceLinker {
|
|
||||||
if (servedOn.host.includes('/')) {
|
|
||||||
throw new Error('Host cannot include a slash');
|
|
||||||
}
|
|
||||||
|
|
||||||
const linker: GitBookSpaceLinker = {
|
|
||||||
toPathInContent(relativePath: string): string {
|
|
||||||
return joinPaths(servedOn.pathname, relativePath);
|
|
||||||
},
|
|
||||||
|
|
||||||
toAbsoluteURL(absolutePath: string): string {
|
|
||||||
return `${servedOn.protocol ?? 'https:'}//${joinPaths(servedOn.host, absolutePath)}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
toPathForPage({ pages, page, anchor }) {
|
|
||||||
return linker.toPathInContent(getPagePath(pages, page)) + (anchor ? `#${anchor}` : '');
|
|
||||||
},
|
|
||||||
|
|
||||||
toLinkForContent(url: string): string {
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return linker;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append a prefix to a linker.
|
|
||||||
*/
|
|
||||||
export function appendBasePathToLinker(
|
|
||||||
linker: GitBookSpaceLinker,
|
|
||||||
basePath: string
|
|
||||||
): GitBookSpaceLinker {
|
|
||||||
const linkerWithPrefix: GitBookSpaceLinker = {
|
|
||||||
toPathInContent(relativePath: string): string {
|
|
||||||
return linker.toPathInContent(joinPaths(basePath, relativePath));
|
|
||||||
},
|
|
||||||
|
|
||||||
toAbsoluteURL(absolutePath: string): string {
|
|
||||||
return linker.toAbsoluteURL(absolutePath);
|
|
||||||
},
|
|
||||||
|
|
||||||
toPathForPage({ pages, page, anchor }) {
|
|
||||||
return (
|
|
||||||
linkerWithPrefix.toPathInContent(getPagePath(pages, page)) +
|
|
||||||
(anchor ? `#${anchor}` : '')
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
toLinkForContent(url: string): string {
|
|
||||||
return linker.toLinkForContent(url);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return linkerWithPrefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
function joinPaths(prefix: string, path: string): string {
|
|
||||||
const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
||||||
const suffixPath = path.startsWith('/') ? path.slice(1) : path;
|
|
||||||
return prefixPath + suffixPath;
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { CustomizationThemeMode, type PublishedSiteContent } from '@gitbook/api';
|
|
||||||
import { headers } from 'next/headers';
|
|
||||||
|
|
||||||
export enum MiddlewareHeaders {
|
|
||||||
/**
|
|
||||||
* Type of the route (static or dynamic)
|
|
||||||
*/
|
|
||||||
RouteType = 'x-gitbook-route-type',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL of the site (without the pathname)
|
|
||||||
*/
|
|
||||||
SiteURL = 'x-gitbook-site-url',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The data associated with the URL.
|
|
||||||
*/
|
|
||||||
SiteURLData = 'x-gitbook-site-url-data',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The mode of the URL (url or url-host)
|
|
||||||
*/
|
|
||||||
URLMode = 'x-gitbook-url-mode',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The theme of the page (light or dark)
|
|
||||||
*/
|
|
||||||
Theme = 'x-gitbook-theme',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The customization override to apply.
|
|
||||||
*/
|
|
||||||
Customization = 'x-gitbook-customization',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Token to use for the API.
|
|
||||||
*/
|
|
||||||
APIToken = 'x-gitbook-token',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The visitor token used to access this content
|
|
||||||
*/
|
|
||||||
VisitorAuthToken = 'x-gitbook-visitor-token',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the URL mode from the middleware headers.
|
|
||||||
* This function should only be called in a server action or a dynamic route.
|
|
||||||
*/
|
|
||||||
export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> {
|
|
||||||
const headersList = await headers();
|
|
||||||
const mode = headersList.get(MiddlewareHeaders.URLMode);
|
|
||||||
if (!mode) {
|
|
||||||
throw new Error('URL mode is not set by the middleware');
|
|
||||||
}
|
|
||||||
|
|
||||||
return mode as 'url' | 'url-host';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the site URL data from the middleware headers.
|
|
||||||
* This function should only be called in a server action or a dynamic route.
|
|
||||||
*/
|
|
||||||
export async function getSiteURLDataFromMiddleware(): Promise<PublishedSiteContent> {
|
|
||||||
const headersList = await headers();
|
|
||||||
const siteURLData = headersList.get(MiddlewareHeaders.SiteURLData);
|
|
||||||
|
|
||||||
if (!siteURLData) {
|
|
||||||
throw new Error(
|
|
||||||
'Site URL data is not set by the middleware. This should only be called in a server action or a dynamic route.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.parse(siteURLData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the URL from the middleware headers.
|
|
||||||
* This function should only be called in a server action or a dynamic route.
|
|
||||||
*/
|
|
||||||
export async function getSiteURLFromMiddleware(): Promise<string> {
|
|
||||||
const headersList = await headers();
|
|
||||||
const siteURL = headersList.get(MiddlewareHeaders.SiteURL);
|
|
||||||
if (!siteURL) {
|
|
||||||
throw new Error('URL mode is not set by the middleware');
|
|
||||||
}
|
|
||||||
|
|
||||||
return siteURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For preview, the theme can be set via query string (?theme=light).
|
|
||||||
* This function should only be called in a dynamic route.
|
|
||||||
*/
|
|
||||||
export async function getThemeFromMiddleware() {
|
|
||||||
const headersList = await headers();
|
|
||||||
const queryStringTheme = headersList.get(MiddlewareHeaders.Theme);
|
|
||||||
if (!queryStringTheme) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryStringTheme === 'light'
|
|
||||||
? CustomizationThemeMode.Light
|
|
||||||
: CustomizationThemeMode.Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the visitor auth token from the middleware headers.
|
|
||||||
* This function should only be called in a dynamic route.
|
|
||||||
*/
|
|
||||||
export async function getVisitorAuthTokenFromMiddleware(): Promise<string | null> {
|
|
||||||
const headersList = await headers();
|
|
||||||
const visitorAuthToken = headersList.get(MiddlewareHeaders.VisitorAuthToken);
|
|
||||||
if (!visitorAuthToken) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return visitorAuthToken;
|
|
||||||
}
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
import { GitBookAPIError } from '@gitbook/api';
|
|
||||||
import type { NextRequest } from 'next/server';
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
import { getContentSecurityPolicy } from '@/lib/csp';
|
|
||||||
import { removeLeadingSlash, removeTrailingSlash } from '@/lib/paths';
|
|
||||||
import { serveResizedImage } from '@/routes/image';
|
|
||||||
import { getPublishedContentByURL } from '@v2/lib/data';
|
|
||||||
import { MiddlewareHeaders } from '@v2/lib/middleware';
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: ['/((?!_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)'],
|
|
||||||
};
|
|
||||||
|
|
||||||
type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
|
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
/**
|
|
||||||
* Serve image resizing requests (all requests containing `/~gitbook/image`).
|
|
||||||
* All URLs containing `/~gitbook/image` are rewritten to `/~gitbook/image`
|
|
||||||
* and serve from a single route handler.
|
|
||||||
*
|
|
||||||
* In GitBook v1: image resizing was done at the root of the hostname (docs.company.com/~gitbook/image)
|
|
||||||
* In GitBook v2: image resizing is done at the content level (docs.company.com/section/variant/~gitbook/image)
|
|
||||||
*/
|
|
||||||
if (request.nextUrl.pathname.endsWith('/~gitbook/image')) {
|
|
||||||
return serveResizedImage(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route all requests to a site
|
|
||||||
const extracted = extractURL(request);
|
|
||||||
if (extracted) {
|
|
||||||
return serveSiteByURL(request, extracted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the rest with the router default logic
|
|
||||||
return NextResponse.next();
|
|
||||||
} catch (error) {
|
|
||||||
return serveErrorResponse(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serve site by URL.
|
|
||||||
*/
|
|
||||||
async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
|
|
||||||
const dynamicHeaders = getDynamicHeaders(request);
|
|
||||||
const { url, mode } = urlWithMode;
|
|
||||||
|
|
||||||
const result = await getPublishedContentByURL({
|
|
||||||
url: url.toString(),
|
|
||||||
visitorAuthToken: null,
|
|
||||||
redirectOnError: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
throw result.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = result;
|
|
||||||
|
|
||||||
if ('redirect' in data) {
|
|
||||||
return NextResponse.redirect(data.redirect);
|
|
||||||
}
|
|
||||||
|
|
||||||
const routeType = dynamicHeaders ? 'dynamic' : 'static';
|
|
||||||
|
|
||||||
const requestHeaders = new Headers(request.headers);
|
|
||||||
requestHeaders.set(MiddlewareHeaders.RouteType, routeType);
|
|
||||||
requestHeaders.set(MiddlewareHeaders.URLMode, mode);
|
|
||||||
requestHeaders.set(MiddlewareHeaders.SiteURL, `${url.origin}${data.basePath}`);
|
|
||||||
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(data));
|
|
||||||
if (dynamicHeaders) {
|
|
||||||
for (const [key, value] of Object.entries(dynamicHeaders)) {
|
|
||||||
requestHeaders.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass a x-forwarded-host and origin that are equal to ensure Next doesn't block server actions when proxied
|
|
||||||
requestHeaders.set('x-forwarded-host', request.nextUrl.host);
|
|
||||||
requestHeaders.set('origin', request.nextUrl.origin);
|
|
||||||
|
|
||||||
const route = [
|
|
||||||
'sites',
|
|
||||||
routeType,
|
|
||||||
mode,
|
|
||||||
encodeURIComponent(url.host + data.basePath),
|
|
||||||
encodePathInSiteContent(data.pathname),
|
|
||||||
].join('/');
|
|
||||||
|
|
||||||
console.log(`rewriting to ${route}`);
|
|
||||||
|
|
||||||
const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
|
|
||||||
const response = NextResponse.rewrite(rewrittenURL, {
|
|
||||||
request: {
|
|
||||||
headers: requestHeaders,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add Content Security Policy header
|
|
||||||
response.headers.set('content-security-policy', getContentSecurityPolicy());
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serve an error response.
|
|
||||||
*/
|
|
||||||
function serveErrorResponse(error: Error) {
|
|
||||||
if (error instanceof GitBookAPIError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message },
|
|
||||||
{ status: 500, headers: { 'content-type': 'application/json' } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL of the GitBook content can be passed in 3 different ways:
|
|
||||||
* - The request URL is in the `X-GitBook-URL` header.
|
|
||||||
* - Hostname is in the `X-GitBook-Host` header and the pathname is the path in the request URL.
|
|
||||||
* - The request URL is matching `/url/:url`
|
|
||||||
*/
|
|
||||||
function extractURL(request: NextRequest): URLWithMode | null {
|
|
||||||
const xGitbookUrl = request.headers.get('x-gitbook-url');
|
|
||||||
if (xGitbookUrl) {
|
|
||||||
return {
|
|
||||||
url: new URL(xGitbookUrl),
|
|
||||||
mode: 'url-host',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const xGitbookHost = request.headers.get('x-gitbook-host');
|
|
||||||
if (xGitbookHost) {
|
|
||||||
return {
|
|
||||||
url: new URL(`https://${xGitbookHost}${request.nextUrl.pathname}`),
|
|
||||||
mode: 'url-host',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefix = '/url/';
|
|
||||||
if (request.nextUrl.pathname.startsWith(prefix)) {
|
|
||||||
return {
|
|
||||||
url: new URL(`https://${request.nextUrl.pathname.slice(prefix.length)}`),
|
|
||||||
mode: 'url',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate if a request is dynamic or static.
|
|
||||||
*/
|
|
||||||
function getDynamicHeaders(_request: NextRequest): null | Record<string, string> {
|
|
||||||
// TODO:
|
|
||||||
// - check token in query string
|
|
||||||
// - check token in cookies
|
|
||||||
// - check special headers or query string
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encode path in a site content.
|
|
||||||
* Special paths are not encoded and passed to be handled by the route handlers.
|
|
||||||
*/
|
|
||||||
function encodePathInSiteContent(rawPathname: string) {
|
|
||||||
const pathname = removeLeadingSlash(removeTrailingSlash(rawPathname));
|
|
||||||
|
|
||||||
if (pathname.match(/^~gitbook\/ogimage\/\S+$/)) {
|
|
||||||
return pathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (pathname) {
|
|
||||||
case '~gitbook/icon':
|
|
||||||
case '~gitbook/image':
|
|
||||||
case 'llms.txt':
|
|
||||||
case 'sitemap.xml':
|
|
||||||
case 'robots.txt':
|
|
||||||
return pathname;
|
|
||||||
default:
|
|
||||||
return encodeURIComponent(pathname || '/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import config from '../gitbook/tailwind.config';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
...config,
|
|
||||||
content: [
|
|
||||||
'../gitbook/src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
'../gitbook/src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
'../gitbook/src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "esnext",
|
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"incremental": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@v2/*": ["./src/*"],
|
|
||||||
"@/*": ["../gitbook/src/*"]
|
|
||||||
},
|
|
||||||
"types": [
|
|
||||||
"bun-types" // add Bun global
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"next-env.d.ts",
|
|
||||||
".next/types/**/*.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
"../gitbook/types/**/*.d.ts",
|
|
||||||
"../gitbook/cf-env.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"packages/openapi-parser",
|
|
||||||
"packages/react-openapi",
|
|
||||||
"packages/react-math",
|
|
||||||
"packages/gitbook"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
main = ".open-next/worker.js"
|
|
||||||
name = "gitbook-v2"
|
|
||||||
compatibility_date = "2024-09-23"
|
|
||||||
compatibility_flags = ["nodejs_compat"]
|
|
||||||
assets = { directory = ".open-next/assets", binding = "ASSETS" }
|
|
||||||
@@ -1,30 +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
|
|
||||||
|
|
||||||
### Silent logs
|
|
||||||
# SILENT=true
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
@@ -23,9 +24,6 @@ next-env.d.ts
|
|||||||
# visual tests
|
# visual tests
|
||||||
screenshots/
|
screenshots/
|
||||||
|
|
||||||
# Sentry Config File
|
|
||||||
.sentryclirc
|
|
||||||
|
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
@@ -34,3 +32,8 @@ screenshots/
|
|||||||
# Generated public files
|
# Generated public files
|
||||||
/public/~gitbook/static/*
|
/public/~gitbook/static/*
|
||||||
!/public/~gitbook/static/images
|
!/public/~gitbook/static/images
|
||||||
|
|
||||||
|
# cloudflare
|
||||||
|
.open-next
|
||||||
|
.wrangler
|
||||||
|
worker-configuration.d.ts
|
||||||
@@ -1,5 +1,463 @@
|
|||||||
# gitbook
|
# 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
|
## 0.6.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
@@ -331,7 +789,7 @@
|
|||||||
- 4cbcc5b: Rollback of scalar modal while fixing perf issue
|
- 4cbcc5b: Rollback of scalar modal while fixing perf issue
|
||||||
- 3996110: Optimize images rendered in community ads
|
- 3996110: Optimize images rendered in community ads
|
||||||
- 133c3e7: Update design of Checkbox to be more consistent and readable
|
- 133c3e7: Update design of Checkbox to be more consistent and readable
|
||||||
- 5096f7f: Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency
|
- 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
|
- 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
|
- 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`
|
- b075f0f: Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"exclude": ["/~gitbook/static/*"]
|
|
||||||
}
|
|
||||||
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
import type { CacheObject } from '@gitbook/cache-do';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface CloudflareEnv {
|
|
||||||
CACHE?: DurableObjectNamespace<CacheObject>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,234 +1,252 @@
|
|||||||
import { type TestsCase, runTestCases } from './util';
|
import { type TestsCase, runTestCases, waitForCookiesDialog } from './util';
|
||||||
|
|
||||||
/** A list of test cases to run on the customers' docs sites. */
|
/** A list of test cases to run on the customers' docs sites. */
|
||||||
const testCases: TestsCase[] = [
|
const testCases: TestsCase[] = [
|
||||||
{
|
{
|
||||||
name: 'Snyk',
|
name: 'Snyk',
|
||||||
baseUrl: 'https://docs.snyk.io',
|
contentBaseURL: 'https://docs.snyk.io',
|
||||||
tests: [
|
tests: [
|
||||||
{ name: 'Home', url: '/' },
|
{ name: 'Home', url: '/', run: waitForCookiesDialog },
|
||||||
{ name: 'OpenAPI', url: '/snyk-api/reference/apps' },
|
{ name: 'OpenAPI', url: '/snyk-api/reference/apps', run: waitForCookiesDialog },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: 'Nexthink',
|
// name: 'Nexthink',
|
||||||
baseUrl: 'https://docs.nexthink.com',
|
// contentBaseURL: 'https://docs.nexthink.com',
|
||||||
tests: [{ name: 'Home', url: '/', screenshot: { waitForTOCScrolling: false } }],
|
// tests: [
|
||||||
},
|
// {
|
||||||
|
// name: 'Home',
|
||||||
|
// url: '/',
|
||||||
|
// screenshot: { waitForTOCScrolling: false },
|
||||||
|
// run: waitForCookiesDialog,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
name: 'asiksupport-stg.dto.kemkes.go.id',
|
name: 'asiksupport-stg.dto.kemkes.go.id',
|
||||||
baseUrl: 'https://asiksupport-stg.dto.kemkes.go.id',
|
contentBaseURL: 'https://asiksupport-stg.dto.kemkes.go.id',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'jasons-tutorials.gitbook.io',
|
name: 'jasons-tutorials.gitbook.io',
|
||||||
baseUrl: 'https://jasons-tutorials.gitbook.io',
|
contentBaseURL: 'https://jasons-tutorials.gitbook.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'faq.deltaemulator.com',
|
name: 'faq.deltaemulator.com',
|
||||||
baseUrl: 'https://faq.deltaemulator.com',
|
contentBaseURL: 'https://faq.deltaemulator.com',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.dify.ai',
|
name: 'docs.dify.ai',
|
||||||
baseUrl: 'https://docs.dify.ai',
|
contentBaseURL: 'https://docs.dify.ai',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'seeddao.gitbook.io',
|
name: 'seeddao.gitbook.io',
|
||||||
baseUrl: 'https://seeddao.gitbook.io',
|
contentBaseURL: 'https://seeddao.gitbook.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'faq.altstore.io',
|
name: 'faq.altstore.io',
|
||||||
baseUrl: 'https://faq.altstore.io',
|
contentBaseURL: 'https://faq.altstore.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'support.audacityteam.org',
|
name: 'support.audacityteam.org',
|
||||||
baseUrl: 'https://support.audacityteam.org',
|
contentBaseURL: 'https://support.audacityteam.org',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.gmgn.ai',
|
name: 'docs.gmgn.ai',
|
||||||
baseUrl: 'https://docs.gmgn.ai',
|
contentBaseURL: 'https://docs.gmgn.ai',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.spicychat.ai',
|
name: 'docs.spicychat.ai',
|
||||||
baseUrl: 'https://docs.spicychat.ai',
|
contentBaseURL: 'https://docs.spicychat.ai',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.portainer.io',
|
name: 'docs.portainer.io',
|
||||||
baseUrl: 'https://docs.portainer.io',
|
contentBaseURL: 'https://docs.portainer.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.chirptoken.io',
|
name: 'docs.chirptoken.io',
|
||||||
baseUrl: 'https://docs.chirptoken.io',
|
contentBaseURL: 'https://docs.chirptoken.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.dexscreener.com',
|
name: 'docs.dexscreener.com',
|
||||||
baseUrl: 'https://docs.dexscreener.com',
|
contentBaseURL: 'https://docs.dexscreener.com',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.pancakeswap.finance',
|
name: 'docs.pancakeswap.finance',
|
||||||
baseUrl: 'https://docs.pancakeswap.finance',
|
contentBaseURL: 'https://docs.pancakeswap.finance',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'book.character.ai',
|
name: 'book.character.ai',
|
||||||
baseUrl: 'https://book.character.ai',
|
contentBaseURL: 'https://book.character.ai',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'docs.tradeonnova.io',
|
|
||||||
baseUrl: 'https://docs.tradeonnova.io',
|
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'azcoiner.gitbook.io',
|
name: 'azcoiner.gitbook.io',
|
||||||
baseUrl: 'https://azcoiner.gitbook.io',
|
contentBaseURL: 'https://azcoiner.gitbook.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.midas.app',
|
name: 'docs.midas.app',
|
||||||
baseUrl: 'https://docs.midas.app',
|
contentBaseURL: 'https://docs.midas.app',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.keeper.io',
|
name: 'docs.keeper.io',
|
||||||
baseUrl: 'https://docs.keeper.io',
|
contentBaseURL: 'https://docs.keeper.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'adiblar.gitbook.io',
|
name: 'adiblar.gitbook.io',
|
||||||
baseUrl: 'https://adiblar.gitbook.io',
|
contentBaseURL: 'https://adiblar.gitbook.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.gradient.network',
|
name: 'docs.gradient.network',
|
||||||
baseUrl: 'https://docs.gradient.network',
|
contentBaseURL: 'https://docs.gradient.network',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mygate-network.gitbook.io',
|
|
||||||
baseUrl: 'https://mygate-network.gitbook.io',
|
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// name: 'mygate-network.gitbook.io',
|
||||||
|
// contentBaseURL: 'https://mygate-network.gitbook.io',
|
||||||
|
// tests: [{ name: 'Home', url: '/' }],
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
name: 'treasurenft.gitbook.io',
|
name: 'treasurenft.gitbook.io',
|
||||||
baseUrl: 'https://treasurenft.gitbook.io',
|
contentBaseURL: 'https://treasurenft.gitbook.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'browndust2.gitbook.io',
|
name: 'browndust2.gitbook.io',
|
||||||
baseUrl: 'https://browndust2.gitbook.io',
|
contentBaseURL: 'https://browndust2.gitbook.io',
|
||||||
tests: [{ name: 'Home', url: '/', screenshot: { waitForTOCScrolling: false } }],
|
tests: [{ name: 'Home', url: '/', screenshot: { waitForTOCScrolling: false } }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'junookyo.gitbook.io',
|
name: 'junookyo.gitbook.io',
|
||||||
baseUrl: 'https://junookyo.gitbook.io',
|
contentBaseURL: 'https://junookyo.gitbook.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'meshnet.nordvpn.com',
|
name: 'meshnet.nordvpn.com',
|
||||||
baseUrl: 'https://meshnet.nordvpn.com',
|
contentBaseURL: 'https://meshnet.nordvpn.com',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'manual.bubble.io',
|
name: 'manual.bubble.io',
|
||||||
baseUrl: 'https://manual.bubble.io',
|
contentBaseURL: 'https://manual.bubble.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.tickettool.xyz',
|
name: 'docs.tickettool.xyz',
|
||||||
baseUrl: 'https://docs.tickettool.xyz',
|
contentBaseURL: 'https://docs.tickettool.xyz',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'wiki.redmodding.org',
|
name: 'wiki.redmodding.org',
|
||||||
baseUrl: 'https://wiki.redmodding.org',
|
contentBaseURL: 'https://wiki.redmodding.org',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'docs.cherry-ai.com',
|
|
||||||
baseUrl: 'https://docs.cherry-ai.com',
|
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
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',
|
name: 'docs.snyk.io',
|
||||||
baseUrl: 'https://docs.snyk.io',
|
contentBaseURL: 'https://docs.snyk.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.realapp.link',
|
name: 'docs.realapp.link',
|
||||||
baseUrl: 'https://docs.realapp.link',
|
contentBaseURL: 'https://docs.realapp.link',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.plaza.finance',
|
name: 'docs.plaza.finance',
|
||||||
baseUrl: 'https://docs.plaza.finance',
|
contentBaseURL: 'https://docs.plaza.finance',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.publicai.io',
|
name: 'docs.publicai.io',
|
||||||
baseUrl: 'https://docs.publicai.io',
|
contentBaseURL: 'https://docs.publicai.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'hyperliquid.gitbook.io',
|
name: 'hyperliquid.gitbook.io',
|
||||||
baseUrl: 'https://hyperliquid.gitbook.io',
|
contentBaseURL: 'https://hyperliquid.gitbook.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.umbraco.com',
|
name: 'docs.umbraco.com',
|
||||||
baseUrl: 'https://docs.umbraco.com',
|
contentBaseURL: 'https://docs.umbraco.com',
|
||||||
tests: [{ name: 'Home', url: '/welcome', screenshot: { waitForTOCScrolling: false } }],
|
tests: [
|
||||||
|
{
|
||||||
|
name: 'Home',
|
||||||
|
url: '/welcome',
|
||||||
|
run: waitForCookiesDialog,
|
||||||
|
screenshot: { waitForTOCScrolling: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'sosovalue-white-paper.gitbook.io',
|
name: 'sosovalue-white-paper.gitbook.io',
|
||||||
baseUrl: 'https://sosovalue-white-paper.gitbook.io',
|
contentBaseURL: 'https://sosovalue-white-paper.gitbook.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'docs.revrobotics.com',
|
|
||||||
baseUrl: 'https://docs.revrobotics.com',
|
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// name: 'docs.revrobotics.com',
|
||||||
|
// contentBaseURL: 'https://docs.revrobotics.com',
|
||||||
|
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
name: 'chartschool.stockcharts.com',
|
name: 'chartschool.stockcharts.com',
|
||||||
baseUrl: 'https://chartschool.stockcharts.com',
|
contentBaseURL: 'https://chartschool.stockcharts.com',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.soniclabs.com',
|
name: 'docs.soniclabs.com',
|
||||||
baseUrl: 'https://docs.soniclabs.com',
|
contentBaseURL: 'https://docs.soniclabs.com',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.meshchain.ai',
|
name: 'docs.meshchain.ai',
|
||||||
baseUrl: 'https://docs.meshchain.ai',
|
contentBaseURL: 'https://docs.meshchain.ai',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.thousandeyes.com',
|
name: 'docs.thousandeyes.com',
|
||||||
baseUrl: 'https://docs.thousandeyes.com',
|
contentBaseURL: 'https://docs.thousandeyes.com',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.raydium.io',
|
name: 'docs.raydium.io',
|
||||||
baseUrl: 'https://docs.raydium.io',
|
contentBaseURL: 'https://docs.raydium.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.fluentbit.io',
|
name: 'docs.fluentbit.io',
|
||||||
baseUrl: 'https://docs.fluentbit.io',
|
contentBaseURL: 'https://docs.fluentbit.io',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
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' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user