Compare commits

..

1333 Commits

Author SHA1 Message Date
github-actions[bot] 1a878dd451 Version Packages 2026-09-11 10:33:39 +00:00
Brett Jephson c08d05ade5 Fix select filters on table and cards blocks (#4608)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 10:33:02 +00:00
Peter White b9453a92f8 Fix preview auth redirects dropping content paths (#4602) 2026-09-11 08:42:28 +02:00
Peter White 2c4d40ad97 Expose the site MCP tools through WebMCP in published docs (#4604) 2026-09-11 08:30:36 +02:00
Tomek 688515efcf Support fit-content width sizing for ContentKit buttons (#4605)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-10 15:50:37 +02:00
Tomek 931cbe717e Restore Edit on Git page actions (#4597)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: Viktor Renkema <49148610+viktorrenkema@users.noreply.github.com>
Co-authored-by: conico974 <nicodorseuil@yahoo.fr>
2026-09-10 12:37:37 +02:00
Tomek 3ef1802e7e Add close button for modals in published sites (#4600)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-09 12:23:17 +02:00
Greg Bergé 8a0e0df84e Fix search section links not scrolling to the target section (#4598)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:40:58 +02:00
Peter White a9041a309a Fix fallback URL cleanup after successful page navigation (#4599) 2026-09-09 11:22:09 +02:00
conico974 625c108196 Add isRevalidationRequest function to handle specific requests (#4596) 2026-09-08 14:18:32 +02:00
Viktor Renkema 8e87856501 Fix page not found in preview links for sites with a custom homepage (#4594) 2026-09-08 11:27:07 +00:00
Tomek 9b822abbb6 Include page descriptions in markdown output (#4595)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-08 12:38:54 +02:00
Peter White 81dba6455b Resolve stable refs in HTML blocks of published markdown pages (#4592) 2026-09-08 09:45:14 +02:00
Johan Preynat 1faa57b812 Keep revision search results in preview (#4582) 2026-09-07 19:46:57 +02:00
Tomek 40150f0a6c Serve markdown with text/plain content-type for ChatGPT (#4591)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-07 17:13:19 +02:00
Brett Jephson bf29570bd5 RND-12706: measuring tabs fix (#4588)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 15:40:43 +01:00
Peter White e8e979b6c0 Serve permanent site redirects (#4586) 2026-09-07 16:07:40 +02:00
Tomek 1f250d1fa8 Add site structure e2e tests for external links (#4584) 2026-09-07 09:31:50 +02:00
Brett Jephson 46b3a18b25 Fix nested select css (#4583)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 16:20:52 +01:00
Nolann B. 2ff77e15b5 Attribute a markdown request from the page actions menu (#4581) 2026-09-04 12:17:00 +02:00
Tomek d7c867aa63 Fix MCP/Markdown copy confirmation closing dropdown instantly (#4579)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 16:08:53 +02:00
Tomek bcd1c41aa1 Scroll to top when selecting current page in search (#4578)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-03 11:35:07 +02:00
Viktor Renkema 77058e3f17 Support merged cells (#4500)
Co-authored-by: OpenAI Codex <codex@openai.com>
2026-09-03 10:26:43 +02:00
Tomek 8ad7465801 Simplify carousel overflow with edge masks & fix carousel scroll (#4577)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-02 14:32:53 +00:00
arnau 1b7ac0e41b Open inline Ask AI questions in the configured assistant (#4574) 2026-09-02 12:05:37 +02:00
Tomek 5f88f4e680 Improve cross-space page breadcrumbs (#4571)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-02 11:43:35 +02:00
Peter White 7009bb9311 Drop the body-anchored cover gate from paragraph contrast text (#4573) 2026-09-02 11:24:54 +02:00
Johan Preynat e460ec381e Preserve linked page destinations in published search (#4566) 2026-09-01 15:06:26 +02:00
Johan Preynat 0a82628298 Trap keyboard focus within published search (#4567) 2026-09-01 13:12:01 +02:00
conico974 18ff11b68b Bump @opennextjs/aws and @opennextjs/cloudflare versions (#4570) 2026-09-01 12:14:55 +02:00
Peter White 3bf55cef10 Allow adding query params to the @webframe.navigate action (#4569) 2026-09-01 10:14:07 +02:00
Peter White e041e49f11 Wait for integrations to register before showing the cookie banner (#4568) 2026-09-01 09:09:15 +02:00
Johan Preynat c1b5be05eb Allow search to target any section (#4564) 2026-08-31 18:32:02 +02:00
Johan Preynat 7543eb83ad Prewarm Turbopuffer when published search opens (#4563) 2026-08-31 13:32:47 +00:00
Tomek 523b7cd4b6 Support external links (#4561)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-31 13:11:13 +02:00
spastorelli af698a49f5 Extend MCP tests (#4562) 2026-08-31 11:59:46 +02:00
conico974 db67585ee2 Remove MCP authentication path from internal paths registry (#4560) 2026-08-28 14:57:21 +02:00
Peter White a566153d98 Chunk oversized visitor authentication cookies (#4554) 2026-08-28 12:28:53 +02:00
conico974 530f98a9c4 Skip internal paths in URL lookup (#4557) 2026-08-28 12:24:29 +02:00
Tomek 39391259c8 Disable zoom for inline images (#4555) 2026-08-28 11:32:19 +02:00
Nolann B. 35be3835f3 Skip rendering work for off-screen OpenAPI blocks (#4502) 2026-08-27 18:41:33 +02:00
Tomek 4452fd793c Fix link title showing site section name (4542) (#4553)
Co-authored-by: Kai Kewley <kaikewley@gmail.com>
2026-08-27 09:57:32 +02:00
conico974 90566879d2 Improve image signature handling and resizing functionality (#4511) 2026-08-26 15:55:57 +02:00
Zeno Kapitein 40a879ad5f Keep section group dropdown columns readable when space is tight (#4549)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:05:28 +02:00
Johan Preynat cc441c99c6 Fix published search scope fusion (#4552) 2026-08-26 14:22:33 +02:00
conico974 64e143a15b Bump Next.js to version 16.3.3 (#4547) 2026-08-26 12:32:56 +02:00
Peter White 8c10d92f77 Keep the section group menu open over the collapsed sidebar rail (#4548) 2026-08-26 12:16:13 +02:00
Zeno Kapitein 0a22eb5340 Stop expecting a cookies dialog in the faq.wanttopay.net e2e test (#4550)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 11:54:31 +02:00
Zeno Kapitein d9fc4608e3 Stop suggesting previously asked questions in the GitBook Assistant (#4546)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 18:00:30 +02:00
Zeno Kapitein ded2f560e3 Show loose sections in a section group dropdown as secondary links unless the group starts with one (#4545)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:14:58 +02:00
Zeno Kapitein a9c5d1b546 Fix the gap between columns in large section group dropdowns (#4544)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:45:41 +02:00
Zeno Kapitein 0ff21b7ae2 Hide the section tabs scroll button when all tabs already fit (#4540)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:35:38 +02:00
Zeno Kapitein 94a496c561 Match the Docs Embed widget to the page it is embedded in (#4533)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:34:54 +02:00
Johan Preynat 148a43ac01 Preserve backend ranking across site sections (#4541) 2026-08-25 14:55:17 +02:00
Johan Preynat 5da854f09c Preserve backend ranking in published search (#4537) 2026-08-25 12:54:24 +02:00
spastorelli b772f74c2b Evaluate <if> blocks when serving markdown pages (#4538) 2026-08-25 12:18:53 +02:00
Zeno Kapitein e0bd04f641 Render headings with the site's heading font (#4531)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:08:03 +02:00
conico974 8d7c3edda8 Disable metadata fetching in revision-related API calls (#4459) 2026-08-24 13:55:50 +02:00
conico974 80951e9889 Remove CODEOWNERS file for API data cache functions (#4536) 2026-08-24 13:43:37 +02:00
Nolann B. 185dd8337e Defer tracking scripts until after load and idle (#4529) 2026-08-24 12:18:30 +02:00
Peter White da7fb13d83 Fail closed in /~gitbook/revalidate when GITBOOK_SECRET is unset (#4535) 2026-08-24 12:11:40 +02:00
conico974 49c993f181 Add API token expiration check (#4532) 2026-08-21 11:29:38 +02:00
Zeno Kapitein 90b54682e5 Redesign the site OAuth consent screen for published sites MCP (RND-12071) (#4530)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 22:45:46 +02:00
Zeno Kapitein 1dccf8f06c Fix table-of-contents page group's first item cut off after navigation (RND-11314) (#4391)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: zenoachtig <8304582+zenoachtig@users.noreply.github.com>
2026-08-20 15:28:02 +02:00
Nolann B. 810244e8a9 Stop preloading zoom-modal images at render time (#4528) 2026-08-20 15:02:00 +02:00
Tomek 0dd2f4fbcd Show containing section name when hovering direct space links (RND-12437) (#4525) 2026-08-20 14:06:10 +02:00
Nolann B. 8a700222e6 Only track embed view events once the frame is shown (RND-12362) (#4517) 2026-08-20 14:04:14 +02:00
Nolann B. b13fd91afc Compute the AI chat time greeting after mount to avoid a hydration mismatch (#4527) 2026-08-20 13:52:52 +02:00
Nolann B. 8a6baaba6a Load the admin toolbar and its CSS lazily (RND-12524) (#4526) 2026-08-20 13:47:49 +02:00
Zeno Kapitein 48fba7cb49 Fix: highlight a TOC link entry when on the page or section it points to (RND-11155) (#4476)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:48:45 +02:00
Zeno Kapitein 6928a9b00a Hide card fields that render no content (#4523)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:48:37 +02:00
Nolann B. 7bab574c63 Self-host default fonts and inline only the ones a site uses (RND-12524) (#4522) 2026-08-20 12:36:55 +02:00
conico974 75bdff3ff3 Add support for AI agent detection and robots directives in markdown (#4524) 2026-08-20 12:18:17 +02:00
Peter White 56c25587db Make keyboard shortcuts work on non-QWERTY layouts (#4516) 2026-08-19 15:03:34 +00:00
Zeno Kapitein cf4efc7213 Fix search input losing focus before hydration (RND-11849) (#4438)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-19 12:30:11 +02:00
Zeno Kapitein 1ef71609e7 Show the light/dark toggle on laptop-width screens (#4515)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:29:58 +02:00
Zeno Kapitein 195c9e6b84 Apply and remember the ?theme= light/dark override in the docs embed (#4380)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 11:07:23 +02:00
Zeno Kapitein 09f39f8300 Vertically center breadcrumb icons (#4513) 2026-08-19 08:52:10 +00:00
Peter White 9002f6598a Fix embed search suggested questions not opening the assistant (#4514) 2026-08-18 17:19:25 +02:00
Zeno Kapitein 01c9b059c6 Require Ctrl/Cmd to zoom Mermaid diagrams with the wheel (RND-12338) (#4465)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-18 16:37:49 +02:00
Zeno Kapitein 6d02b8ab72 Fix TS code block comment punctuation color in dark mode (RND-12204) (#4460)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-18 16:36:07 +02:00
Peter White 177ef8582a Fix Docs Embed host actions not reaching the assistant (#4512) 2026-08-18 16:33:03 +02:00
Zeno Kapitein 78c589ffba Align Previous/Next page navigation to opposite edges (RND-11235) (#4494)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-18 13:36:08 +02:00
Nolann B. e4b214e6bd Fix large OpenAPI payload section not being scrollable (RND-12291) (#4510) 2026-08-18 13:17:33 +02:00
Tomek 64ce9e180d Render site-space custom home page at root (#4475)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-18 12:20:52 +02:00
conico974 c64d3a50e8 Refactor CSS compatibility checks (#4504) 2026-08-18 10:30:43 +02:00
spastorelli fd070ce9ca Redirect to the upstream auth logout URL on ~gitbook/auth/logout (#4489) 2026-08-18 10:27:48 +02:00
Tomek 048c4e4c70 Fix tooltip data-popup-open state conflict and AI messages collapsible (#4509)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-18 10:22:47 +02:00
conico974 fb01dc9ecb Remove unused Cloudflare image resizing function (#4507) 2026-08-17 17:26:32 +02:00
Nolann B. d33e570bc7 Fix the spacebar being swallowed in the search bar (RND-12484) (#4508) 2026-08-17 13:21:21 +00:00
Tomek 87fd234d55 Fix dropdown icon rotation by reading dropdown open state (#4503)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-17 14:55:09 +02:00
Claire Chabas 0f32eb17d1 Fix toggle chevron alignment in ToC (#4505) 2026-08-17 14:41:44 +02:00
Tomek 3e29680792 Stabilize visual CI tests (#4506)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-17 13:14:47 +02:00
Nolann B. b3db1c58c8 Migrate headless UI primitives from Radix and react-aria to Base UI (#4499) 2026-08-15 21:05:32 +02:00
Nolann B. 0f994016f4 Update OpenAPI customer test URL (#4498) 2026-08-13 18:43:33 +02:00
Nolann B. 419094b79d Stop every DOM change from restyling the whole document (#4492) 2026-08-13 17:18:48 +02:00
Greg Bergé 95d23775ce Migrate repository to Oxlint and Oxfmt (#4497) 2026-08-13 13:18:42 +00:00
spastorelli aa98859560 Detect and pass visitor type for site resolution (#4490) 2026-08-13 14:33:10 +02:00
Tomek ea19801cfa feat: add greeting customization to AI chat (#4483) 2026-08-13 11:17:26 +00:00
Peter White 13059b843c Fix Docs Embed being stuck on the Search tab (RND-12435) (#4496) 2026-08-13 06:57:13 +00:00
Johan Preynat 3db14ad950 Fix search result links for page matches (RND-11916) (#4488) 2026-08-12 17:43:57 +02:00
Nolann B. 5fac9e37ee Move the OpenAPI package from react-aria to Base UI (#4473) 2026-08-12 14:53:09 +02:00
Nolann B. 1424c566a0 Load the OpenAPI renderer only on pages that use it (#4472) 2026-08-12 14:53:09 +02:00
conico974 65f99eafe5 Reduce bundle size on the server (#4464) 2026-08-12 13:22:51 +02:00
Tomek 7594a334d8 Show image thumbnails for file attachments (#4486)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-11 13:09:16 +02:00
conico974 827e4f9856 Fix variant switcher URL for non-default variants (#4487) 2026-08-11 13:09:14 +02:00
Peter White a9a1a609c5 Fix sticky search results when switching the search filter RND-12265 (#4485) 2026-08-11 10:15:36 +02:00
conico974 61405a3036 feat: add CSS browser compatibility check for pull requests (#4478) 2026-08-07 15:44:52 +02:00
conico974 634a24ff03 Fix text visibility issues on pages with background covers in Firefox and iOS Safari (#4477) 2026-08-07 12:46:26 +02:00
Nolann B. 2ccd43ee5e Lazy-load the Mermaid code block (#4468) 2026-08-06 08:59:03 +02:00
Nolann B. cd506e3f79 Split the Shiki highlighter from the plain token helpers (#4467) 2026-08-06 08:59:02 +02:00
Brett Jephson a6644074cb Fix: select no longer stored in url (#4466)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 11:59:56 +01:00
conico974 4be043d96e Add support for visitor token revalidation without redirection (#4463) 2026-08-05 09:50:26 +02:00
Brett Jephson 673f4b6076 RND-11832: button with select slug action (#4462) 2026-08-04 14:12:13 +01:00
Brett Jephson d0a63bab91 Tab - reinstate scroll hash (#4458)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:42:30 +02:00
conico974 ca9a1dd396 Block crawler from using ask endpoint (#4457) 2026-08-03 11:47:17 +02:00
claude[bot] 996d7ec021 Scroll to in-page heading even when the URL hash is unchanged (RND-12290) (#4454)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Tomek Gargula <tomasz@gitbook.com>
2026-07-31 10:36:37 +02:00
Brett Jephson f9ad9b5356 RND-11830: select store URL param (#4439)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 16:05:36 +01:00
Nolann B. 332089eca9 Don't forward cookies through the OpenAPI Scalar proxy (#4451) 2026-07-30 10:06:17 +02:00
Zeno Kapitein 6ac4cf4b76 Align card and column breakpoints (#4449) 2026-07-29 12:13:25 +00:00
Zeno Kapitein 5c74eef602 Make the table search empty state more prominent (#4418)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 16:13:02 +02:00
Zeno Kapitein 1ee1853995 Add an assistant tool to rate its own previous response (#4425)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 16:10:41 +02:00
Tomek fa9c9b38b7 Force software rendering in Playwright for consistent images (#4444)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-27 14:01:55 +00:00
conico974 b4d9c6b95e Fix BUILD_VERSION assignment to ensure fallback to current timestamp (#4445) 2026-07-27 15:47:54 +02:00
Tomek 588964279e Remove search value from URL on unfocus (#4443)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 10:46:02 +00:00
Nolann B. a80d41200a Run the OpenAPI Scalar proxy on GitBook's origin (#4440) 2026-07-24 18:36:27 +02:00
Nolann B. ccb9d7bda4 Submit sendFeedback MCP tool findings through submitSiteAgentFeedback (#4437) 2026-07-24 10:34:50 +02:00
Nolann B. 14562e9e5b Serve noindex on internal search/assistant URLs (?q= / ?ask=) (#4433) 2026-07-23 15:00:13 +02:00
spastorelli 580d186ddd Clean up old forward resume authorize to sites oauth server (#4436) 2026-07-23 14:08:56 +02:00
Tomek 038008c853 Fix heading anchor links on touch devices (#4435)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-23 11:56:21 +00:00
conico974 8c890522ed Patch next for dev, and fix issue in dev with inline icon (#4432) 2026-07-22 16:00:36 +02:00
conico974 e9558a6df7 Revert "Simplify search query handling to in-memory storage only (#4419)" (#4431) 2026-07-22 14:53:29 +02:00
Brett Jephson b5f3c1416a RND-11857: Prompt block with default expanded options (#4429) 2026-07-22 13:33:06 +01:00
conico974 b9d383bed3 Bump dependencies for @opennextjs/aws, @opennextjs/cloudflare, and next to latest versions (#4430) 2026-07-22 11:01:17 +02:00
spastorelli b6e7f2d2db Render the Site OAuth consent screen from the server's signed marker (#4428) 2026-07-22 09:59:30 +02:00
Tomek 94ef1769ec Simplify search query handling to in-memory storage only (#4419)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-22 07:54:50 +00:00
Zeno Kapitein ae9367dafe Only show the assistant activity heading when there's activity to reveal (#4424)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 14:47:58 +02:00
Zeno Kapitein cef18701be Add an assistant tool to submit page feedback (#4408)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 13:25:14 +02:00
Zeno Kapitein bf674a47d9 Add supporting context to AI tool confirmation dialogs (#4422)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:45:51 +02:00
conico974 1eb763f9c5 Implement API token cookie handling with chunking support (#4421) 2026-07-20 15:38:07 +02:00
Nolann B. db176ba0ea Add "On this page" on models page (#4417) 2026-07-20 13:17:02 +02:00
Nolann B. 03bbacf319 Add missing link reference to OpenAPI models (#4416) 2026-07-20 12:53:28 +02:00
Tomek 484cc11627 Fix ScrollContainer scroll buttons not reflecting overflow immediately (#4414)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-20 08:47:04 +00:00
spastorelli 0dee4155a2 Add consent flow env vars to next config (#4413) 2026-07-17 15:16:46 +00:00
spastorelli 4d7c01587e Add Sites OAuth consent screen (#4405) 2026-07-17 15:58:27 +02:00
Greg Bergé 6083a88845 Expose the current page context to integration block webframes (#4411)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:06:38 +02:00
Nolann B. bf6a7af72b Defer search index loading until search is opened (#4407) 2026-07-16 21:53:24 +02:00
Utku Ufuk 703e654a37 Split the default-scope site search into two parallel API requests (#4395) 2026-07-16 15:23:30 +03:00
conico974 1b571aeaa9 Fix scroll behavior for navigation (#4406) 2026-07-16 14:21:01 +02:00
Zeno Kapitein c41cf9c12a Add cover image background mode and mask (#4213)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:02:53 +02:00
Zeno Kapitein 3d376845d7 Add a carousel layout for cards blocks (#4393)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:17:00 +02:00
Nolann B. bcd4d4e980 Remove MCP test that recorded real feedback in production (#4399) 2026-07-15 10:42:20 +02:00
Addison 88b1919a45 Add centered GitBook logo to README (#4401)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 17:31:04 +09:00
Steven H cf94386c16 fix an issue where certain keywords could cause an exception when rendering emojis (#4397) 2026-07-14 19:08:09 +00:00
Zeno Kapitein e77bf32d51 Update customer e2e test URL after redirect (#4398)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:24:14 +00:00
Zeno Kapitein a69a307de2 Take a near-neutral extreme tint as the exact site background (#4392) 2026-07-13 20:37:08 +02:00
Zeno Kapitein 39156ee9c3 Support button sizes in the button block (#4394)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 20:03:52 +02:00
spastorelli 47ac3e2c81 Only advertise OAuth PRM doc for the auth MCP endpoint on non-VA sites (#4387) 2026-07-10 15:24:33 +02:00
Zeno Kapitein e73b182925 Declare embed standalone bundle as a Turbo build output (RND-11876) (#4386)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 13:51:48 +02:00
claude[bot] a9a52fee72 RND-11356: remove redundant GBO best-section re-selection (#4375)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2026-07-10 13:25:26 +02:00
Greg Bergé cb92754575 Add a navigate action to integration block webframes (#4362)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 11:24:33 +02:00
Zeno Kapitein 49d35aa026 Let visitors type a follow-up while the Assistant is answering (RND-11789) (#4377)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-09 21:00:01 +02:00
Nolann B. 98b2df42ef Add sendFeedback MCP tool for reporting documentation findings (#4384)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-09 15:17:32 +02:00
Zeno Kapitein 6cf4278f14 Only show "Back to [space]" for table-of-contents cross-space links (#4381)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 14:32:11 +02:00
Zeno Kapitein 86c44299eb docs(agents): discourage multi-line block comments (#4382)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-09 14:16:26 +02:00
claude[bot] 89c4a0f808 feat(mcp): add askQuestion tool to the site MCP server (#4372)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2026-07-09 13:11:05 +02:00
Nolann B. 8e9a49de1a Separate prompt block actions (#4379) 2026-07-08 16:11:26 +02:00
Zeno Kapitein 4e9071d1b5 Fix docs embed navigateToPage for cross-space/section deep links (#4374)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 11:02:49 +02:00
Nolann B. 597fe34d3b Sync responses selector with the Responses collapsibles (#4376) 2026-07-08 09:08:21 +02:00
claude[bot] e14609cb3e fix(theme): resolve system theme before first paint to avoid dark/light flash (RND-11643) (#4373)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-07 20:10:02 +02:00
Zeno Kapitein 57f3077006 Keep the page outline and Ask button in view while scrolling API reference pages (#4371)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 19:10:55 +02:00
conico974 e24632c80b Integrate GitBook image resizing service and remove cdn-cgi mode (#4285) 2026-07-07 16:21:32 +02:00
Zeno Kapitein ad3399b42e Refine the per-paragraph ask button (shorter tooltip, hide inside cards) (#4365)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:27:04 +02:00
Brett Jephson f3408ed495 A11y fix: table structure (#4368) 2026-07-06 16:58:23 +01:00
Brett Jephson 8676ad19e2 RND-11788: hide unlabelled button from screen readers (#4369) 2026-07-06 16:58:02 +01:00
Samy Pessé 3a31100efe Version Packages (#4335)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-06 12:52:44 +00:00
Greg Bergé 3fedaaa08c Install eval-estree-expression from npm instead of a GitHub commit (#4367)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:31:54 +00:00
Nolann B. bcea23e886 Handle nullable expressed via anyOf/oneOf with a null member (#4366) 2026-07-06 12:33:02 +02:00
Zeno Kapitein fdea8f1c47 Show section, variant & section groups in page breadcrumbs (#4357)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:49:47 +02:00
Zeno Kapitein 139b897521 Improve language selector on mobile (#4361) 2026-07-06 11:03:39 +02:00
Greg Bergé 37c2dd8819 Fix flaky customer screenshots caused by the cookie banner (#4363)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 22:06:20 +02:00
Zeno Kapitein 62ca6d3ce5 Show a "Back to [space]" shortcut when a link leads to another space (#4358)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:36:07 +02:00
Nolann B. 6102413d6b Reuse shared AI mode helpers across AI actions and UI (#4355) 2026-07-02 23:19:07 +02:00
Zeno Kapitein eaef259a80 Refresh e2e customer list (#4360)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:34:09 +02:00
Zeno Kapitein 578a3a829b Fix section tab dropdowns clipping their contents during animation (#4356)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:06:41 +00:00
Zeno Kapitein c923c654bd Fix centered paragraphs shifting left with the paragraph ask button (#4354)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:19:24 +02:00
Nolann B. 65fd3cc101 Gate AI features server-actions (#4353) 2026-07-02 11:37:03 +02:00
Zeno Kapitein bb30db105d Add a margin button to ask the assistant about a paragraph (#4328) 2026-07-02 08:34:49 +02:00
Nolann B. 6146f8e183 Reduce oversized searchDocumentation MCP responses (#4352) 2026-07-01 18:00:02 +02:00
Nolann B. 7e55cd5e4c Add "Available in MCP" badge for OpenAPI operations (#4350) 2026-07-01 17:38:04 +02:00
conico974 cb2dfa76af Keep first 3 remote results pinned at the top (#4349) 2026-06-30 13:47:04 +02:00
Viktor Renkema 77efd449ae Allow quickly viewing which pages have changed in a CR/revision (#4348) 2026-06-30 12:49:31 +02:00
Zeno Kapitein af259fbe86 Scrap recommended questions from Search e2e test (#4351) 2026-06-30 12:29:21 +02:00
conico974 2b31b0213c Refactor error handling and remove unused caching logic (#4345) 2026-06-30 08:16:23 +02:00
Nolann B. f60afe1b25 Add missing MCP annotations (#4347) 2026-06-29 10:15:57 +02:00
Nolann B. b7c3b847e8 Prevent insights tracking failures from breaking MCP tool requests (#4346) 2026-06-27 09:31:20 +02:00
Zeno Kapitein 2fa65e6949 Support per-table search override (#4340)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 10:46:47 +02:00
Nolann B. c76b4fdccc Fix OpenAPI sticky preview column overflowing onto the next operation (#4341) 2026-06-25 15:10:26 +02:00
Zeno Kapitein 9f651339f0 Redesign the 404 page with related-page suggestions and a search input (#4338)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 12:30:42 +02:00
Zeno Kapitein 5ce3b1e578 Turn off search on cards (for now) (#4339) 2026-06-24 11:26:14 +02:00
Nolann B. d172684b88 Some small OpenAPI design tweaks (#4337) 2026-06-23 21:20:28 +02:00
conico974 937c8b8eb2 Remove 'remote' from cache usage in specific functions (#4312) 2026-06-23 17:59:43 +02:00
Zeno Kapitein 36dbdb48cc Support agent goal query param for markdownAsk (#4324)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 09:47:51 +02:00
Zeno Kapitein 2dc76a2c3c Fix empty grouped section dropdown in embedded sites (#4334)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 09:47:15 +02:00
Samy Pessé 0edc378c57 Version Packages (#4306)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-23 00:10:23 +02:00
Zeno Kapitein 2eb313194a Remove old PageAction checks (#4333) 2026-06-22 17:12:35 +02:00
Brett Jephson 882dfcda83 Tidy relating to prompt (#4332) 2026-06-22 15:48:58 +02:00
Brett Jephson 3ff88ba22f Adds the prompt block (#4329) 2026-06-22 11:53:25 +01:00
Nolann B. 51bd768042 Make the AI Assistant panel resizable (#4330) 2026-06-20 21:41:59 +02:00
spastorelli daadd91eba Fix NextLink prefetch side effect on unsigned claims persistance (#4325) 2026-06-19 11:39:12 +02:00
Nolann B. f4469b2df3 Support OpenAPI one-operation-per-page mode (#4314) 2026-06-19 10:35:09 +02:00
Nolann B. 91b325ca08 Add AI chat reference button on text selection (#4326) 2026-06-19 10:18:07 +02:00
Nolann B. 1616028426 Align code block action buttons vertically with the block (#4327) 2026-06-19 10:03:37 +02:00
Nolann B. 6c15d8314a Update OpenAPI border radius (#4323) 2026-06-18 23:25:19 +02:00
Zeno Kapitein 6c967c9bcc Add search and filters to table blocks (#4322)
Co-authored-by: utkuufuk <utkuufuk@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:41:40 +02:00
Johan Preynat c44d822fe1 Trust cookieless integration content host for WebFrames (#4321)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 18:08:25 +02:00
Zeno Kapitein af3cda1c53 Fix wide content overflowing hint blocks (#4320)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:32:53 +02:00
Tomek a0ad403a91 Stabilize Playwright Chrome text rendering across platforms (#4318) 2026-06-17 09:21:16 +00:00
Zeno Kapitein 0ca49523a2 Remove external link button from link tooltip hover card (#4316)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 10:52:02 +02:00
Zeno Kapitein 76550415ac Fix ⌘-C opening the Assistant on Dvorak keyboard layouts (#4311)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 10:51:20 +02:00
Nolann B. 509389c7e5 Update @gitbook/api to latest version (#4315) 2026-06-16 14:30:10 +02:00
Nolann B. 88b94b5d36 Handle circular oneOf with discriminator and allOf in OpenAPI schemas (#4070) 2026-06-16 13:34:17 +02:00
Greg Bergé 251630c49f Use page actions items list for ordering and default action (#4302)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:19:20 +02:00
Zeno Kapitein acf2719d06 Stabilize AI Assistant visual regression tests (#4305)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:19:56 +02:00
Zeno Kapitein ddba8031ec Reference the current page when opening the Assistant (#4308)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:35:15 +02:00
Zeno Kapitein 72a1596ae8 Fix AI assistant forms overflowing the chat panel (#4309)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 17:22:57 +02:00
Nolann B. 5b36c59061 Fix PDF export failing on API Reference pages (#4307) 2026-06-12 13:43:38 +00:00
Zeno Kapitein 77c717c251 Group an assistant turn's tool calls into a single expandable (#4303)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 15:26:27 +02:00
Samy Pessé 60310b4c83 Version Packages (#4278)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-12 14:55:55 +02:00
conico974 6eabaecbad Implement getVariables method for expression evaluation (#4304) 2026-06-12 14:26:17 +02:00
Zeno Kapitein 423bbd06d9 Create Claude launch.json (#4301) 2026-06-12 12:52:24 +02:00
conico974 36f19c8018 Add LLMs directive for documentation links (#4300) 2026-06-12 12:09:25 +02:00
Zeno Kapitein 2ecbdd8f62 add navigateToPage assistant tool (#4299)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:57:55 +02:00
Nolann B. c727cc8b9d Fix inflated visitor counts from unstable anonymous visitorId (#4293) 2026-06-12 00:52:59 +02:00
Zeno Kapitein 2c729d461e Rework Assistant “based on your context” popover wording (#4298) 2026-06-11 22:19:32 +02:00
Tomek d83ea4ce14 chore(a11y): increase width of the collapse/expand arrow in menu items (#4297) 2026-06-11 14:02:38 +00:00
spastorelli bac8608e78 Add support for passing visitor claims to integration webframes (#4291) 2026-06-11 09:52:26 +02:00
Greg Bergé 7a1236a3a5 Fix search results linking to page instead of section anchor (#4295)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:23:59 +00:00
Zeno Kapitein ed80bd9e7a Add recent search (and chat) queries (#4225) 2026-06-10 18:45:07 +02:00
Zeno Kapitein 7036b67307 Fix window.GitBook.registerAssistant is being called twice (#4296) 2026-06-10 15:54:38 +00:00
Zeno Kapitein 6bd522b322 Update site section tabs with better column calc and animations (#4252) 2026-06-10 17:35:38 +02:00
Zeno Kapitein 74fc3f74d7 Fix: Submit feedback confirmation does not auto close (#4240) 2026-06-10 17:35:29 +02:00
conico974 3e384c679e Add suggested follow-up questions to AI answer markdown response (#4284) 2026-06-10 17:29:08 +02:00
Greg Bergé f1fa1061db Upgrade bun.lock (#4294) 2026-06-10 17:09:10 +02:00
Addison 4f754a73a3 Add information about GitBook to page markdown (#4292) 2026-06-09 19:53:07 +02:00
conico974 62d6d28888 Fix context handling in resolveContentRef for page resolution issues (#4290) 2026-06-09 15:10:03 +02:00
conico974 fd73a9912b Fix caching strategy for icon fetching (#4289) 2026-06-08 13:46:32 +02:00
Johan Preynat 595435ac4a Ensure local search matches remote filter when there are no sections (#4287) 2026-06-04 19:14:54 +02:00
Addison aa9d41a1b6 Update footer for llms (#4286) 2026-06-04 16:19:02 +02:00
conico974 30089b404c Adjust caching strategy for icon fetching based on runtime environment (#4283)
Co-authored-by: Steven H <steven@gitbook.io>
2026-06-02 12:00:38 +02:00
Nolann B. 92b68560e1 Add Ask AI in CodeBlock (#4220) 2026-06-02 00:40:54 +02:00
Nolann B. f158064817 Match OpenAPI operation paths regardless of trailing slash (#4282) 2026-05-29 12:16:24 +02:00
Greg Bergé 2885a137f8 Fix heading anchor alignment with heading text and translate its accessibility label (#4281) 2026-05-29 09:38:47 +02:00
spastorelli 0ffb74a6f8 Fix issue where MCP POST requests with expired tokens returns 307 instead of 401 (#4280) 2026-05-28 16:35:12 +02:00
Nolann B. b116a0a2a4 Fix OpenAPI code sample box not scrolling on initial load (#4279) 2026-05-28 12:04:32 +02:00
Greg Bergé b9bb535d52 Full screen view for Mermaid diagrams (#4275)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:17:21 +02:00
Samy Pessé 257ca09a5e Version Packages (#4263)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tomek Gargula <tomasz@gitbook.com>
2026-05-28 07:12:45 +00:00
Tomek f405fffa06 feat: add support for icons in tabs (#4277) 2026-05-27 15:22:25 +00:00
Tomek 16a54b6bb9 Improve accessibility in published docs (#4276) 2026-05-27 12:40:25 +00:00
Greg Bergé 526a928420 Fix annotation hover interaction when card has a target link (#4274) 2026-05-27 12:02:20 +02:00
conico974 c201abab5f Don't display agents instructions for page action (#4272) 2026-05-27 10:41:53 +02:00
Greg Bergé 3697dfefed Add page actions layout visibility (#4270) 2026-05-22 12:37:37 +02:00
spastorelli 287203d197 Show MCP page actions for VA & adaptive sites (#4268) 2026-05-22 08:47:13 +02:00
conico974 0b38f6c93b Update regional cache settings (#4267) 2026-05-20 14:25:59 +02:00
conico974 8ce64f4862 Remove remote from use cache (#4259) 2026-05-20 14:12:51 +02:00
Greg Bergé 127ef54b0f Add updates tag filtering (#4265) 2026-05-20 12:42:51 +02:00
Greg Bergé 67f31aaa7a Always allow LLMs to read the content (#4264) 2026-05-20 09:39:35 +02:00
Greg Bergé 51fef84d39 Fix logo size variations (#4266) 2026-05-19 20:55:45 +02:00
Greg Bergé 0c577a00de Allow CORS for the same root domain (#4262)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:41:07 +02:00
Samy Pessé 8b6dc6dbbd Version Packages (#4255)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2026-05-19 13:24:41 +02:00
Antoine Coutellier 2e7ba40a25 feat: support integrations inside reusable content blocks (#4250) 2026-05-19 11:29:02 +02:00
Greg Bergé b70b8fd2ca Fix llms.txt not including all content (#4260) 2026-05-18 21:33:07 +02:00
Greg Bergé a9ba996953 Fix left/right aligment of images with caption (#4261) 2026-05-18 21:32:22 +02:00
Brett Jephson 47c8ccba6f Fix: clipping of FA icons (#4257) 2026-05-18 15:09:41 +01:00
conico974 e9842feeef Bump Next.js to version 16.2.6 and fix oom on dev (#4258) 2026-05-18 11:12:47 +02:00
Greg Bergé f4b4fe15c1 Add missing translations (#4254) 2026-05-16 07:16:49 +02:00
Greg Bergé 06b9fd542a Do not add target blank on mailto (#4256) 2026-05-15 20:43:39 +02:00
Taran Vohra 76077f9f6d Fix publish workflow (#4245) 2026-05-15 10:51:43 +02:00
Greg Bergé 7b65f7c0d5 Update iframely embed URL (#4253) 2026-05-14 19:04:34 +02:00
Greg Bergé 37a9453917 Allow to copy in embed (#4251) 2026-05-13 04:49:14 +00:00
Viktor Renkema d994875bea Support code block // [!code -- or ++] notation (#4248) 2026-05-12 15:39:21 +02:00
Claire Chabas a9b5521f3a Fix anchor URL resolution (#4249) 2026-05-11 16:02:20 +02:00
Greg Bergé 91fadb08ed Fix turbo burning Mac CPU (#4247) 2026-05-11 11:36:23 +00:00
Samy Pessé b55605f25e Version Packages (#4199)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2026-05-08 13:00:03 +00:00
Addison 6ba5e54d19 add isComposing to search (#4244) 2026-05-08 18:47:20 +09:00
Samy Pessé 56aeb19e49 Fix events tracked from the middleware missing some properties (#4242) 2026-05-07 23:15:34 +02:00
conico974 ebab8188f6 Bump @opennextjs/aws and @opennextjs/cloudflare versions (#4243) 2026-05-07 17:01:05 +02:00
Zeno Kapitein 8064c95267 Fix publish workflow npm upgrade in Bun repo (#4239) 2026-05-07 09:36:14 +02:00
conico974 2cef2e02c8 Bump Next.js version to 16.2.5 (#4241) 2026-05-06 22:02:10 +02:00
conico974 8bfced2e0d Add negotiator for markdown acceptance in middleware (#4238)
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 14:59:38 +02:00
conico974 61ee4e3637 Add tests for markdown serving based on user agents (#4227) 2026-05-06 13:52:09 +02:00
Greg Bergé 927aa89213 Add "Go to home" button on 404 page (#4236) 2026-05-05 13:12:12 +00:00
Greg Bergé f29d1e2f16 Round images when site customization border style is rounded (#4237) 2026-05-05 14:50:24 +02:00
conico974 3a9efb8aed bust revision page markdown (#4235) 2026-05-05 14:39:34 +02:00
conico974 f71ab8426c Bust page document data cache (#4234) 2026-05-04 16:15:27 +02:00
conico974 a3d1558f99 Disable regional cache for debugging purposes (#4233) 2026-05-04 14:03:29 +02:00
conico974 ca53869e2d Reduce cache TTL for regional cache and log cache debug (#4232) 2026-05-04 13:32:36 +02:00
conico974 33598e665b Add logging for cache retrieval in GitbookIncrementalCache (#4231) 2026-05-04 11:47:47 +02:00
Samy Pessé 9045ba5248 Remove progressive rollout for markdown stable links (#4230) 2026-05-01 21:56:28 +02:00
Rodrigo Castro 7061fc2223 Fix hidden pages in sitemap exports (#4216) 2026-04-30 14:04:37 +01:00
Samy Pessé 0c9e3952b7 Increase rollout of markdown stable links to 50% (#4228) 2026-04-30 11:37:37 +02:00
Samy Pessé 4b78672135 Fix links to other site-spaces / sections in llms.txt (#4222) 2026-04-30 08:43:32 +02:00
conico974 10289e4881 Revert search worker changes and add error handling (#4226)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 21:59:05 +02:00
conico974 8b6a6df1d6 Move search into a web worker (#4223)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 18:42:44 +02:00
conico974 83df84ac28 Improve markdown serving logic (#4224)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 18:28:45 +02:00
Greg Bergé 5e5075a137 Make Mermaid controls shown only on hover (#4221) 2026-04-29 17:13:31 +02:00
Greg Bergé 90c1f68e19 Speed up Mermaid rendering (#4219) 2026-04-29 15:40:24 +02:00
Zeno Kapitein 0e10124626 Adapt max logo height (#4218) 2026-04-28 14:59:45 +02:00
Zeno Kapitein 01e1c2ae6a Add space between ask button and content (#4217) 2026-04-28 14:59:14 +02:00
Zeno Kapitein 65558cfbb1 Redesign search results (#4198)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2026-04-28 14:49:46 +02:00
conico974 10d894ce19 Integrate local search functionality (#4078)
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 14:14:47 +02:00
Samy Pessé d1a2958212 Pass visitor session to streamAskQuestion (#4211) 2026-04-24 21:07:46 +02:00
Viktor Renkema 894b6adc4c Support marks colors for text and background (#4212) 2026-04-24 17:23:10 +02:00
Nolann B. b3b819f8e5 Disable MCP server when MCP page action is off (#4210) 2026-04-23 18:01:43 +02:00
spastorelli 2b98af0cc0 Fix not found pages showing duplicate trademark badges (#4209) 2026-04-23 16:31:40 +01:00
Samy Pessé 54a56ed772 Remove rollout for agent ask (#4208) 2026-04-23 14:48:06 +02:00
Samy Pessé 071627d4f0 Improve the footer for agent to query the docs (#4206) 2026-04-22 22:56:28 +02:00
Samy Pessé 7595706b92 Allow AI agents to ask questions using ?ask= (#4205) 2026-04-22 08:43:57 +02:00
conico974 be080248b1 Block search indexation in deployment configuration (#4203) 2026-04-20 16:22:34 +02:00
Samy Pessé d3d51507e5 Add "Content-Signal" to robots.txt (#4202) 2026-04-20 14:14:59 +02:00
Addison a17ab65a6d Show variants in fully hidden sections (#4201) 2026-04-20 16:41:24 +09:00
Zeno Kapitein e3aca9e4f6 Fix embeddable search links (#4196) 2026-04-18 00:43:07 +02:00
spastorelli 045f603849 Add ~gitbook/auth/logout endpoint in GBO (#4195) 2026-04-16 22:40:56 +02:00
Greg Bergé b2854500a1 Allow to scroll to top of the page (#4194) 2026-04-16 16:20:50 +02:00
Samy Pessé ead88591c2 Version Packages (#4193)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-16 14:19:37 +02:00
Peter White aa1fc246e2 Add support for sticky first column in tables (#4188) 2026-04-16 10:33:15 +00:00
Samy Pessé e792cffeb3 Version Packages (#4178)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2026-04-15 19:07:34 +02:00
Zeno Kapitein 119e3bc369 Refine Assistant phase UI (#4187) 2026-04-15 18:29:58 +02:00
Greg Bergé 823e7d5c4b Fix tags appearing on top of page even if disabled (#4192) 2026-04-15 18:26:06 +02:00
Claire Chabas f5f659e7fc Display updates blocks headings in page outline (#4167) 2026-04-15 16:20:51 +02:00
Brett Jephson 723e579958 Fix: whitespace of Expandable in Column (#4190) 2026-04-15 14:07:20 +01:00
conico974 f27901c453 Update deployment ID logic for environment-specific prefixes (#4191) 2026-04-15 14:52:44 +02:00
Zeno Kapitein 789c2fab60 Adjust empty TOC logic and layout (#4189) 2026-04-15 14:04:13 +02:00
conico974 3fa727c80b Migrate to Next 16 (#4162) 2026-04-15 13:20:25 +02:00
Zeno Kapitein 88c38fa505 Add Search tab to Docs Embed, refactor search into an embeddable frame (#4185) 2026-04-15 12:09:59 +02:00
Claire Chabas bda9fe2d76 Make ToC groups collapsible and other styles fixes/improvements (#4177) 2026-04-14 19:41:51 +02:00
Nolann B. 2bdade3b57 Fix HTML lang attribute not reflecting language variants (#4186) 2026-04-14 13:14:39 +02:00
Zeno Kapitein 8d2a95b168 Docs Embed: Better support light/dark mode overrides (#4181) 2026-04-14 10:50:05 +02:00
Taran Vohra 68c842bd4c Translations dropdown should show language spaces that best matches the current space title (#4184) 2026-04-14 13:11:55 +05:30
Nolann B. ecb4292551 Fix llms.txt and llms-full.txt language variants filtering (#4179) 2026-04-13 21:53:42 +02:00
Zeno Kapitein 7fd381c0b1 Hide empty sidebar on OpenAPI pages (#4180) 2026-04-13 20:29:57 +02:00
Zeno Kapitein 709964fe3e Rework page layout (#4013) 2026-04-13 14:48:24 +02:00
Samy Pessé afc7a38bef Version Packages (#4163)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-13 14:16:46 +02:00
Greg Bergé bbcfd72a6d Fix toolbar design (#4175) 2026-04-13 11:10:34 +00:00
spastorelli 8242f18b8a Extend gitbook/expr std lib with some additional methods (#4173)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2026-04-13 10:19:52 +00:00
Samy Pessé 4f7fba08df Prepare for step.content to be optional (#4174) 2026-04-13 11:47:21 +02:00
conico974 f17e292ba4 Refactor test setup for improved mock restoration (#4176) 2026-04-13 11:23:54 +02:00
Greg Bergé 6e1074ec4d Reduce search debounce (#4170) 2026-04-09 17:43:14 +02:00
Nolann B. c5e5857623 Exclude language variants from llms.txt and llms-full.txt (#4165) 2026-04-08 14:30:31 +02:00
Nolann B. 11df1fd395 Fix MCP getPage tool returning page not found (#4166) 2026-04-08 14:28:39 +02:00
spastorelli 569d4046be Expose a ~gitbook/mcp/auth endpoint for non-VA adaptive content sites (#4155) 2026-04-07 12:50:39 +02:00
Samy Pessé 50653abd08 Improve AI agent detection and readability by outputting 200 for page not found (#4164) 2026-04-04 12:35:43 +02:00
Greg Bergé 84082225af Respect locale to format date in update block (#4160) 2026-04-03 15:36:12 +00:00
Nolann B. 7d882cef5f Fix HTML lang attribute not reflecting site language variants (#4161) 2026-04-03 17:31:37 +02:00
Zeno Kapitein 126d4b78b6 Improve Assistant form controls, i18n, and UX (#4159) 2026-04-03 09:47:19 +02:00
Nolann B. e8224bad6c Support system as a default theme mode (#4151) 2026-04-01 00:39:34 +02:00
Samy Pessé 6063437513 Version Packages (#4158)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-01 00:22:45 +02:00
Nolann B. ab17ff47b0 Update missing CustomIconName (#4157) 2026-04-01 00:09:06 +02:00
Samy Pessé d97b404c7a Version Packages (#4156)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-31 21:14:44 +02:00
Nolann B. 3c141f0231 Bump fontawesome to 7.2.0 (#4154) 2026-03-31 20:38:12 +02:00
Samy Pessé e3d391e01b Version Packages (#4102)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-31 17:01:13 +02:00
Nolann B. 62cfec874a Setup AGENTS.md and CLAUDE.md (#4147) 2026-03-31 16:38:28 +02:00
Nolann B. ccd21e4943 Update fontawesome-pro to 1.0.23 (#4153) 2026-03-31 16:37:13 +02:00
conico974 696d9d0d01 Update vercel dependency to version 50.37.3 (#4152) 2026-03-31 12:07:00 +02:00
Nolann B. b77c4fc5b9 Add getPage MCP tool (#4148) 2026-03-31 11:01:51 +02:00
Zeno Kapitein 7e1ea48080 Revise AI Chat e2e tests (#4150) 2026-03-30 17:58:35 +02:00
Nolann B. b5d6f2e908 Fix translation quality across 11 locales (#4146) 2026-03-30 13:17:03 +02:00
Nolann B. 832b44f0ac Use site-level path for MCP URL instead of space-level path (#4149) 2026-03-30 13:09:25 +02:00
Nolann B. 4909044831 Add Claude Code and Codex connect actions (#4145) 2026-03-30 09:55:45 +02:00
Nolann B. 4ed84c209a Localize announcement message (#4144) 2026-03-30 09:47:17 +02:00
Nolann B. ca01888fe2 Localize footer links title (#4143) 2026-03-30 09:46:37 +02:00
Nolann B. 6855c05781 Localize header link titles (#4142) 2026-03-30 09:46:20 +02:00
Nolann B. ab867eb69c Add top-level locale field to GitBookBaseContext (#4140) 2026-03-27 18:16:19 +01:00
Samy Pessé deabce98ae Sort results by score to mix records and pages (#4077) 2026-03-27 15:25:09 +01:00
Samy Pessé f884477a65 Associate questions asked in the embeddable assistant to the proper channel (#4141) 2026-03-27 15:07:42 +01:00
Nolann B. c56b569a05 Support expand-all options for OpenAPI (#4116) 2026-03-25 23:53:51 +01:00
Peter White 1ba412ff6c Fix OneTrust issue for customer E2E test (#4137) 2026-03-25 17:37:43 +01:00
Peter White 7c2823cd26 Fix full width header alignment (#4138) 2026-03-25 17:01:22 +01:00
Peter White 16bfafe32b Add option to sticky header row in tables (#4134) 2026-03-25 14:04:35 +01:00
Zeno Kapitein da648cabd8 Fix regressions in Assistant UX (#4136) 2026-03-24 18:49:44 +01:00
spastorelli a36e437580 Add login button to not found page for adaptive content site with configured fallback/login URL (#4133) 2026-03-23 16:25:03 +01:00
Samy Pessé d5dd5e7ba1 Include site.id in UTM utm_content for Trademark link (#4135) 2026-03-23 14:08:29 +01:00
Nolann B. c4a2604b4d Fix PDF export crash when page contains an OpenAPI block (#4128) 2026-03-20 11:15:19 +01:00
Nolann B. 7f4e644733 Small OpenAPI improvements (#4131) 2026-03-20 11:15:03 +01:00
Nolann B. 5d3f78b112 Fix flaky internal tests (#4132) 2026-03-20 10:50:20 +01:00
Nolann B. 60bcf43304 Fix allOf merging for annotation-only schemas (#4118) 2026-03-20 10:03:56 +01:00
Samy Pessé b40465e7b7 Fix normalization of URL causing invalid redirections. (#4130) 2026-03-19 19:27:00 +01:00
Samy Pessé 3151864f81 Track events from the proper server URL (#4129) 2026-03-19 18:55:26 +01:00
Greg Bergé 80803c92f2 Fix code block styling (#4127) 2026-03-19 16:07:03 +01:00
Samy Pessé 31e77d4e17 Track events for markdown page views, llms.txt, RSS, MCP connections (#4110) 2026-03-18 17:37:58 +01:00
Taran Vohra 8e5a2aa6e6 pan and zoom controls for mermaid block (#4125) 2026-03-18 20:32:16 +05:30
Taran Vohra fe6f6420e3 Cleanup and remove old preview format using /url/preview (#4117) 2026-03-18 17:50:12 +05:30
Nolann B. aff6285eb9 Deep-merge schema properties in OpenAPISchema (#4124) 2026-03-18 13:09:04 +01:00
conico974 5f132bc979 Add try-catch for error handling and improve logging (#4123) 2026-03-17 17:42:34 +01:00
Nolann B. 4297de3571 Add localizedTitle support for site title (#4115) 2026-03-17 12:17:41 +01:00
Nolann B. 6455f2e3ff Fix parent schema metadata lost during oneOf/anyOf allOf flattening (#4119) 2026-03-17 10:31:12 +01:00
Nolann B. dee52fa80a Fix schema title display stripping spaces (#4120) 2026-03-17 10:27:56 +01:00
spastorelli 4ac29817a2 Expose a ~gitbook/auth/login URL to handle redirection to upstream auth with current location passed (#4113) 2026-03-16 16:00:31 +01:00
spastorelli f09ca60930 Fix crash for PDF route when the page include an Ask AI button (#4121) 2026-03-16 15:41:16 +01:00
Greg Bergé a7c0f7251b Allow <br> in OpenAPI descriptions (#4112) 2026-03-13 11:51:50 +01:00
Greg Bergé efc6117665 Fix code block layout (#4108) 2026-03-13 10:24:18 +01:00
Taran Vohra 6c613d0df3 Fix auth redirects with the new preview URL (#4111) 2026-03-13 13:52:09 +05:30
Taran Vohra 45d32b5084 Site Preview URLs (#4096) 2026-03-12 20:27:59 +05:30
conico974 0f62a629b4 Handle unexpected errors in getExposableError (#4109) 2026-03-12 15:54:01 +01:00
Peter White 7ab63516bd Import site redirect patterns from @gitbook/api (#4107) 2026-03-12 10:06:33 +00:00
Nolann B. 9dd2db6678 Support x-gitbook-token-placeholder as TryIt prefill fallback for basic auth (#4105) 2026-03-11 20:35:13 +01:00
Nolann B. 4787b7e910 Use localized titles/descriptions for site entities (#4106) 2026-03-11 18:25:18 +01:00
Taran Vohra be02e33ada Fix an issue where source path containing special characters were not getting resolved to a site redirect (#4104) 2026-03-11 21:33:42 +05:30
Zeno Kapitein 3ce7e529f7 Fix Variant dropdown not showing in GitBook embed (#4100) 2026-03-11 13:57:13 +01:00
Samy Pessé 0f162a45a7 Version Packages (#4082)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2026-03-11 12:56:28 +00:00
Steven H 02ba0ef601 Disable Workers Observability (#4099) 2026-03-11 11:02:44 +00:00
Nolann B. bdc6cd9aba Use OpenAPI publicURL as Scalar specUrl (#4093) 2026-03-11 11:21:14 +01:00
Zeno Kapitein b3875a19d7 Add assistantName property to Docs Embed (#4095) 2026-03-11 10:46:10 +01:00
conico974 884d900e38 Remove check for invalid characters in the url (#4098) 2026-03-11 10:43:32 +01:00
Taran Vohra f68755695e Update workflows with preview URL envs (#4097)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 12:13:49 +05:30
Greg Bergé 9ffde72ced Reset scroll between section navigation (#4089) 2026-03-10 18:18:40 +01:00
Viktor Renkema 9e3629b12b Apply color if tags have this set (#4094) 2026-03-10 17:02:25 +01:00
Greg Bergé 48f81c80f0 Use break-words for code (#4092) 2026-03-10 13:36:08 +00:00
Greg Bergé 0baa8e5022 Fix documentation links opening with /~gitbook/embed/ (#4090) 2026-03-10 14:10:50 +01:00
conico974 59b9663ce3 Add decodeURLPath function for URL decoding and validation (#3915) 2026-03-09 16:29:22 +01:00
Greg Bergé 9be66e3e8a Add visual test for Mermaid blocks (#4087) 2026-03-09 14:22:34 +01:00
Aiko 9a0f35c26d Fix tab content cropping inside stepper on mobile (#4088) 2026-03-09 19:11:21 +06:00
Nolann B. f1826807a7 Add backward compatibility for OpenAPI spec content URL (#4086) 2026-03-09 09:47:26 +01:00
Nolann B. 887c3f8eac Add token verification on OpenAPI proxy (#4085) 2026-03-08 16:50:43 +01:00
Nolann B. d666212674 Add support for localized site section titles (#3980) 2026-03-07 20:52:13 +01:00
conico974 8cb9131d83 Revert middleware matcher changes (#4084) 2026-03-06 17:13:53 +01:00
Greg Bergé 98b36616cb Add interactive controls to docs assistant (#4073) 2026-03-06 16:58:48 +01:00
Nolann B. eceec6f641 Move ~scalar/proxy route to site route group (#4083) 2026-03-05 19:06:06 +01:00
conico974 9f4d93502e Add real IP extraction to analytics event proxy (#4056) 2026-03-05 16:49:44 +01:00
Nolann B. 8ab419a47e Support x-enable-proxy at operation level (#4080) 2026-03-05 10:05:17 +01:00
Samy Pessé 56ce5dfd74 Version Packages (#4048) 2026-03-04 20:32:40 +01:00
Zeno Kapitein 370b731972 Docs Embed: Make trademark optional (#4079)
Co-authored-by: Greg Bergé <berge.greg@gmail.com>
2026-03-04 14:53:42 +01:00
conico974 b7fb9e57ca Don't use server actions for search (#4067) 2026-03-03 18:27:44 +01:00
Viktor Renkema 5b9f5fa991 Improve wrapping of tags within TOC item (#4075) 2026-03-03 15:26:44 +01:00
Greg Bergé bf1f11794e Fix scroll reset when changing page (#4072) 2026-03-02 16:13:47 +00:00
Nolann B. 8621c7f571 Support alternative security scope groups (#3976) 2026-03-02 17:04:31 +01:00
Zeno Kapitein 4a6f91a19d Show social accounts in header (#4068) 2026-03-02 11:06:49 +01:00
Greg Bergé 38a1c85fee Fix PDF export page URL for /~space/ URLs (#4065) 2026-03-02 09:49:22 +01:00
Nolann B. cb71511cda Wrap MCP tracking call with waitUntil (#4071) 2026-02-27 17:46:01 +01:00
Nolann B. afa476c51f Add server-side insight event tracking for MCP route (#4063) 2026-02-27 17:16:21 +01:00
Zeno Kapitein 1e9ed753a1 Fix typecheck in API 0.165.0 (#4069) 2026-02-27 16:00:05 +01:00
John Cashman 22da7961d1 Include headings inside columns in published page outline (#4045)
Co-authored-by: Greg Bergé <berge.greg@gmail.com>
2026-02-27 14:27:54 +00:00
Nolann B. e074283bbf Add server-side proxy for Scalar API client to bypass CORS (#4050) 2026-02-27 12:18:07 +01:00
Zeno Kapitein afa868e0e1 Show "Ask Assistant" search result immediately (#4066) 2026-02-27 11:55:21 +01:00
Greg Bergé c5bdde4401 Harden visitor session detection to improve GBO toolbar visibility (#4061) 2026-02-27 11:47:05 +01:00
Greg Bergé 5efe13e622 Fix Firefox mobile sidepanel TOC scroll by constraining sheet flex height (#4062) 2026-02-27 11:21:17 +01:00
Greg Bergé 41ef3435bc Clarify the toolbar message (#4053) 2026-02-27 11:19:57 +01:00
Claire Chabas 157d5fb9e7 Fix cards styles (#4058) 2026-02-26 16:29:39 +01:00
Greg Bergé ec28a7b686 Support scroll to text fragment (#4060) 2026-02-26 16:00:12 +01:00
Viktor Renkema b3e9ff9839 Support icons/emojis in tags (#4054) 2026-02-26 14:45:12 +01:00
Claire Chabas aa2292036c Fix inline code word break (#4057) 2026-02-26 13:28:33 +01:00
Claire Chabas f7c4538b16 Prevent hash link scroll to top in Safari (#4055) 2026-02-26 13:27:04 +01:00
Claire Chabas b9b444c24e Fix inline code overflowing in table cells (#4052) 2026-02-25 18:23:00 +01:00
conico974 6f1b795d80 Improve sharded tag cache performance (#4051) 2026-02-25 17:48:09 +01:00
Samy Pessé 0803db07ef Track opening of search results for records and in the assistant (#4047) 2026-02-25 11:40:39 +01:00
Samy Pessé a9adbc6370 Version Packages (#3991)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-25 11:37:48 +01:00
Greg Bergé f49b0af3fe Fix E2E tests (#4044) 2026-02-25 09:57:47 +01:00
Samy Pessé 3d8e89cbe9 Show favicon for record search results in the assistant (#4042) 2026-02-24 18:57:22 +01:00
Greg Bergé f0a54bcb27 Fix PDF export links to preserve published/custom domains (#4043) 2026-02-24 17:02:01 +00:00
Claire Chabas c348583a18 Prevent inline code from breaking on multiple lines (#4041) 2026-02-24 13:48:33 +01:00
Greg Bergé a4b238dc0c Support close button in embed (#4036) 2026-02-24 11:35:03 +01:00
Zeno Kapitein 529afa3fac Open Assistant from page action without default question (#4038) 2026-02-24 10:04:18 +01:00
Peter White 577bf26d1e Fix anchor ID collisions by removing non-content UX IDs (#4035) 2026-02-24 09:53:36 +01:00
Samy Pessé 4808f36718 Update @gitbook/api to 0.163.0 (#4039) 2026-02-23 19:00:02 +01:00
Samy Pessé a100c5a6c8 Prepare search for upcoming records (#4037) 2026-02-23 18:34:01 +01:00
Zeno Kapitein 4962662fb1 Improve search breadcrumbs (#4034) 2026-02-23 13:40:57 +00:00
Zeno Kapitein f7c251e2d2 Improve Reddit social account URL handling (#4033) 2026-02-23 11:49:26 +01:00
Greg Bergé 40524d4d4a Upgrade Argos & Playwright (#4031) 2026-02-23 10:04:59 +01:00
Nolann B. 1a7ef78fe2 Handle non-array required field in OpenAPI schema (#4030) 2026-02-21 13:34:12 +01:00
Greg Bergé 5f3f4da2d2 Stabilize E2E (#4028) 2026-02-20 15:35:50 +01:00
Nolann B. 93eea0b6d9 Strip trailing slash from OpenAPI server URLs (#4029) 2026-02-20 14:40:36 +01:00
Viktor Renkema 9dfa9c2db0 Add support for rendering Tags (#4006) 2026-02-20 10:03:49 +01:00
spastorelli d9ff25d28e Integrate sites OAuth server flow in GBO for protected resources like MCP (#4015) 2026-02-19 15:48:49 +01:00
Samy Pessé ae30a576e8 Integrate Ask AI with upcoming insights by passing visitor session IDs (#4027) 2026-02-19 14:36:23 +01:00
Nolann B. 2e495cb28a Add Global Privacy Control (GPC) support (#4023) 2026-02-18 21:32:56 +01:00
Nolann B. f517035e38 Remove available scopes list from OAuth2 endpoints (#4022) 2026-02-18 21:20:58 +01:00
spastorelli 83bd0d74cd Fix issue where customizations stored in cookie is shared between different site previews (#4025) 2026-02-18 19:10:54 +01:00
Greg Bergé 3cfefa40fd Support rendering Mermaid blocks (#4018) 2026-02-18 17:00:34 +01:00
Claire Chabas 59ffb95417 Support hint block custom icon (#4007) 2026-02-18 15:02:33 +01:00
Nolann B. 2ebb18dc5a Make OpenAPI Response structure clearer (#4021) 2026-02-18 14:32:07 +01:00
Claire Chabas 1deb897de1 Bump GitBook API (#4019) 2026-02-18 12:53:29 +01:00
Nolann B. 11d9b80e77 Use precedence for OpenAPI servers (#4020) 2026-02-18 01:20:39 +01:00
John Cashman e73d9afd86 fix(pdf): keep headings together across page breaks (#4016) 2026-02-17 15:59:42 +01:00
Greg Bergé 729501c977 Fix language dropdown contrast (#4017) 2026-02-17 14:27:09 +01:00
Greg Bergé 414866cbec Fix external links logic for proxy (#4012) 2026-02-17 11:41:30 +01:00
Claire Chabas 9d5e397574 Remove code syntax highlighting patch for Safari (#4014) 2026-02-16 19:09:19 +01:00
Nolann B. 92c9b8b056 Cache isAIUserAgent result (#4011) 2026-02-16 13:11:43 +00:00
Greg Bergé a73caf69ff Allow to pass a token to the embed script (#4005) 2026-02-16 12:48:02 +01:00
conico974 f72f250b16 Add deploymentId to nextConfig for custom deployment method (#4010) 2026-02-16 12:34:35 +01:00
Viktor Renkema bd330ac133 Remove tracking-wider in search breadcrumbs (#4009) 2026-02-16 11:34:14 +00:00
Nolann B. e31a8f4275 Hide cookie banner for AI user agents (#3999) 2026-02-16 11:49:03 +01:00
Greg Bergé cc38ec2098 Fix regression showing page actions on page without title + description (#4004) 2026-02-13 10:53:25 +01:00
Nolann B. eab11e2d6f Increase timeout for recursive parse test (#4003) 2026-02-13 09:10:25 +00:00
Zeno Kapitein 152490cd67 Normalize undefined site-space language to English in variant categorization (fix language selector) (#4002) 2026-02-13 09:43:45 +01:00
conico974 833b07b284 Bump Vercel version to 50.15.1 (#4001) 2026-02-13 09:43:27 +01:00
Nolann B. 5f668605c2 Use isCookiesTrackingDisabled for cookie consent integrations (#3997)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-12 20:40:18 +01:00
conico974 9c762065f2 Bump Vercel CLI version (#4000) 2026-02-12 18:52:40 +01:00
Viktor Renkema 4af7df8902 Updates to style and interactions of toolbar (#3998) 2026-02-12 16:08:05 +01:00
Zeno Kapitein a3f854e1a8 Better localise French ask label (#3996) 2026-02-12 15:02:43 +01:00
Zeno Kapitein 4c54820055 Improve Docs Embed layout (#3994) 2026-02-12 14:06:37 +01:00
Zeno Kapitein 22f574da9c Fix followup & suggested questions overflow (#3995) 2026-02-12 14:06:28 +01:00
Zeno Kapitein 426aabe39a Fix sidebar on bold themes (#3993) 2026-02-12 11:48:43 +01:00
Taran Vohra 64de8d2cf6 Add e2e test for language variant navigation (#3992) 2026-02-12 15:07:27 +05:30
Greg Bergé b4b471fab6 Fix unexpected error thrown when using the embed in standalone mode (#3985) 2026-02-11 15:15:00 +01:00
Samy Pessé 17b32994f6 Version Packages (#3990)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-11 14:37:35 +01:00
Greg Bergé bcc0ef8666 Fix links handling in embedded Assistant (#3987) 2026-02-11 13:57:48 +01:00
Greg Bergé 6ede6ee7c6 Add Korean translations (#3986) 2026-02-10 15:41:28 +01:00
Nolann B. 7a11861d36 Add support for custom cookie banner registration (#3925) 2026-02-10 11:28:24 +01:00
Greg Bergé 10754fbaf4 Fix GitBook icon not displayed in trademark (#3984) 2026-02-10 10:58:30 +01:00
Zeno Kapitein 8f32f4fb1e Better handle huge section groups (#3978) 2026-02-09 15:13:01 +01:00
Nolann B. 717ad0b300 Fix OpenAPI try it button justify (#3975) 2026-02-05 11:16:58 +01:00
Nolann B. a3eed52f24 Refine OpenAPI schema alternatives/allOf handling (#3973) 2026-02-05 11:00:59 +01:00
Greg Bergé 27a34b7ee2 Support defaultExpanded in Expandable block (#3974) 2026-02-04 11:49:14 +01:00
Zeno Kapitein bf3ad28c7f Add margin to TOC items after page group (#3972) 2026-02-03 16:10:32 +01:00
Zeno Kapitein 70c2e90e4b Handle empty page section search results (#3971) 2026-02-03 16:10:13 +01:00
Greg Bergé d0a378cdb6 Disallow Markdown pages crawling (#3970) 2026-02-03 09:40:17 +01:00
Viktor Renkema 81f8ddcf27 Fix flaky Updates blocks in visual tests (#3969) 2026-02-02 17:32:46 +01:00
Zeno Kapitein 9d6457f814 OpenAPI styling tweaks (#3968) 2026-01-30 19:02:30 +01:00
Zeno Kapitein 07c02ebcd2 Header & TOC styling tweaks (#3967) 2026-01-30 15:23:16 +01:00
Samy Pessé 4d3172d13f Rollout to 100% the dereferencing of reusable content at the API level (#3966) 2026-01-30 09:01:12 +01:00
Zeno Kapitein 0aa34cc9a8 Fix Embed options in React (#3964) 2026-01-29 17:32:56 +01:00
Greg Bergé 551d30cfa4 Fix loading of images in PDF export (#3958)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2026-01-29 15:55:42 +01:00
Samy Pessé 844a0a8ea7 Fix CI for visual testing 2026-01-29 15:26:41 +01:00
Samy Pessé eb8ba66a66 Ensure deployments to preview start when approval job is skipped 2026-01-29 14:06:48 +01:00
Hirotaka Miyagi d738e118a0 Fix: Prevent form submission during IME composition (#3942)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Addison <42930383+addisonschultz@users.noreply.github.com>
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2026-01-29 12:46:52 +00:00
Samy Pessé c16d08f5d5 Add approval step for external contributors' PRs (#3963) 2026-01-29 11:33:13 +01:00
Samy Pessé 61ec9b25b9 Enable approved PR deployments for preview workflows (#3954) 2026-01-29 11:16:00 +01:00
Samy Pessé b57561d395 Fix rendering of images in reusable content from an external space (#3960) 2026-01-29 10:42:25 +01:00
Zeno Kapitein e00d1c5bbd Support code themes customization option, refactor built-in code themes (#3949) 2026-01-28 13:48:56 +01:00
conico974 9063b56c89 Fix for the assistant (#3948) 2026-01-28 11:16:50 +01:00
Greg Bergé 020d3668d7 Make the button icon validation safer (#3955) 2026-01-28 10:44:52 +01:00
Greg Bergé 4d30cbf058 Fix social links not appearing on desktop (#3950) 2026-01-28 10:44:40 +01:00
Samy Pessé d3f6c38f05 Update scalar dependencies (#3956) 2026-01-28 10:29:56 +01:00
Samy Pessé 6f1db32682 Simplify local development and contributions (#3952) 2026-01-28 09:32:56 +01:00
Samy Pessé 4ad6b2c067 Increase rollout of reusable content deref in the API to 80% (#3951) 2026-01-28 09:22:19 +01:00
Viktor Renkema 07449443a0 Fix rendering of Expandables within Update blocks (#3947) 2026-01-27 20:26:33 +01:00
Greg Bergé b09cc64fec Fix GIF rendering (#3946) 2026-01-27 17:34:02 +01:00
Greg Bergé 1eed2004f6 Avoid keyboard-shortcut flicker by adding OS class and using CSS (#3944) 2026-01-27 17:02:49 +01:00
Greg Bergé 91539b8187 Fix navigation loader (#3945) 2026-01-27 17:01:06 +01:00
Samy Pessé 16cfde87ee Increase rollout of reusable content deref in the API to 50% (#3943) 2026-01-27 12:22:09 +01:00
conico974 58b4a978ff Update Next.js dependency to version 15.4.11 (#3940) 2026-01-27 09:26:11 +01:00
Viktor Renkema e34be0e89b Fix logo sizing bug during navigation (#3928) 2026-01-27 08:46:37 +01:00
Samy Pessé e60c754343 Increase rollout of reusable content deref in the API to 30% (#3939) 2026-01-26 16:22:13 +01:00
Nolann B. 48ca21e471 Add OpenAPI 3.1 nullable array support (#3938) 2026-01-26 14:05:14 +01:00
Nolann B. 3ba9e46f2b Improve OpenAPI server URL validation (#3937) 2026-01-26 13:45:03 +01:00
Greg Bergé 58f0cc8287 Fix search field clear button (#3936) 2026-01-24 16:26:03 +01:00
Nolann B. 29e2b22b63 Fix ordered list item index calculation (#3935) 2026-01-23 16:01:33 +01:00
Greg Bergé 3c7cb09c1b Respect DPR to ensure a good image quality (#3931) 2026-01-23 11:45:00 +01:00
Greg Bergé 0f1cd6745d Refactor search (#3932) 2026-01-23 11:13:20 +01:00
Samy Pessé e90f57b138 Deference reusable content at the API level (#3847) 2026-01-23 10:34:44 +01:00
conico974 7bd5939247 Bust data cache (#3927) 2026-01-22 13:59:35 +01:00
Zeno Kapitein 89de966e7a Support social accounts customization option (#3916) 2026-01-22 13:57:40 +01:00
Greg Bergé 4e2d86318e Fix OpenAPI example not showing (#3930)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 12:11:55 +01:00
Nolann B. f647af2003 Prevent duplicate Content-Type headers in OpenAPI code samples (#3924) 2026-01-21 14:51:46 +01:00
Greg Bergé 96e24a1f84 Fix JWT token usage in GitBook embed SDK (#3922) 2026-01-21 12:05:14 +01:00
Viktor Renkema 2a0f37f4b3 Always display page actions even if there is no toc/title/description (#3921) 2026-01-21 10:54:42 +01:00
Zeno Kapitein 17d935dd4c Upgrade API to 0.158.0 (#3918) 2026-01-20 17:55:47 +01:00
Greg Bergé df07fbe242 Remove development from package.exports before publishing (#3917) 2026-01-20 17:22:24 +01:00
Zeno Kapitein 1e53376151 Add SideSheet component, refactor TOC and AIChat to use it (#3835) 2026-01-19 21:19:54 +01:00
Samy Pessé f294818775 Serve markdown version of the page depending on the accept type (#3914) 2026-01-19 15:11:02 -05:00
Greg Bergé eed386a133 Fix custom tools usage with GitBook Embed SDK (#3913) 2026-01-19 13:14:23 +01:00
Zeno Kapitein e172b25ba8 Grow InlineActionButton to available space on line (#3912) 2026-01-19 12:55:03 +01:00
Zeno Kapitein f7f6f9d06d Set <html lang> based on translated language (#3909) 2026-01-16 12:00:39 +01:00
Claire Chabas 9e01676fe9 Remove grid from framed images (#3907) 2026-01-15 14:06:26 +01:00
Zeno Kapitein f6cf62a686 Standardise toggle chevrons across the app (#3904) 2026-01-14 13:46:47 +01:00
Zeno Kapitein a5ef44ab2a Better conditional display of page actions (#3900)
Co-authored-by: Claire Chabas <claire.chabas@gmail.com>
2026-01-14 13:21:47 +01:00
Zeno Kapitein b53cb1d2b3 Align styling of PageLinkItem and PageDocumentItem (#3903) 2026-01-14 13:07:45 +01:00
Samy Pessé 7eeff73080 Version Packages (#3902)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-13 16:13:41 +00:00
Greg Bergé bf63045319 Fix OpenAPI JSX compilation (#3901) 2026-01-13 16:56:51 +01:00
Samy Pessé 71c2e7f8b4 Version Packages (#3886)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-13 13:40:37 +00:00
Samy Pessé 985c1b45b4 Fix OpenAPI dereferencing for $ref to itself (#3899) 2026-01-13 13:34:08 +00:00
Zeno Kapitein 35efc9d6d8 Fix TOC icon backgrounds on gradient and bold themes (#3898) 2026-01-12 15:45:41 +01:00
Nolann B. 6b1fd43179 Lazy load OpenAPI styles and remove Tailwind Typography (#3883) 2026-01-12 15:09:02 +01:00
Zeno Kapitein 7517e987a5 Clean up button and input across the app (#3897) 2026-01-12 12:41:57 +01:00
Zeno Kapitein 21851d3340 Round page cover corners on extrawide screens (#3892) 2026-01-12 11:58:17 +01:00
Samy Pessé ca682362dd Fix 404 when accessing rss.xml for the root page (#3896) 2026-01-09 16:05:31 +01:00
David Hughes 56fb754e22 Update published-site.png (#3894) 2026-01-08 09:47:37 +08:00
Zeno Kapitein 99d788ed9e Update page cover styling (#3890) 2026-01-07 11:55:17 +01:00
Taran Vohra ebf3b2beb7 Fix an issue where content refs to spaces outside of site was resolving with share links (#3889) 2026-01-07 11:59:02 +05:30
Zeno Kapitein 7b7231602f Add e2e tests for embed frame (#3830) 2026-01-06 17:30:14 +01:00
Addison adfb118285 Merge branch 'main' of github.com:GitbookIO/gitbook 2026-01-06 13:22:44 +08:00
Zeno Kapitein a7f58ab829 Fix embed tab button size regression (#3887) 2026-01-05 14:01:02 +01:00
Zeno Kapitein 360b525b4b Fix FontAwesome 7 icon overflows (#3885) 2026-01-05 13:56:08 +01:00
Zeno Kapitein 695e99f466 Update button sizing (#3868) 2026-01-05 11:18:00 +01:00
Zeno Kapitein aa87699915 Fix block alignment (#3880) 2025-12-18 21:26:10 +01:00
Zeno Kapitein d2eb3d9f5a Fix disabled button from not showing up (#3879) 2025-12-18 21:13:14 +01:00
Samy Pessé 77bc339fca Version Packages (#3878)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-18 14:47:36 +01:00
Zeno Kapitein e7fb77116e Fix @gitbook/icons icon rendering (#3877) 2025-12-18 14:27:34 +01:00
Samy Pessé 050ca23ac4 Version Packages (#3818)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-18 11:10:53 +00:00
Zeno Kapitein 9063f97dba Switch main DocumentView to flex layout (#3875) 2025-12-18 09:43:42 +01:00
Zeno Kapitein ed9e8fe93a Support button actions (#3866) 2025-12-18 09:29:08 +01:00
Zeno Kapitein f478ddc2ed Create <Input> component (#3857) 2025-12-16 19:43:43 +01:00
Zeno Kapitein bd11c0d21b Fix trademark rounded corners in embed (#3874) 2025-12-16 15:10:47 +01:00
Zeno Kapitein 381728970e Fix overflowing section groups (#3873) 2025-12-15 20:41:00 +01:00
Taran Vohra 4690e2087a Roll out meta links resolution to all sites (#3872) 2025-12-15 21:19:34 +05:30
John Cashman 224a44daf8 Fix priority assignment in sitemap generation (#3854) 2025-12-15 15:41:29 +00:00
Nolann B. 4766092ff9 Fix missing properties in allOf/oneOf (#3871) 2025-12-13 12:38:09 +01:00
conico974 3e4e0b0bf0 bump next again (#3870) 2025-12-12 20:01:11 +01:00
Samy Pessé 6b985051f3 Use response.id instead of responseId (#3867) 2025-12-12 13:52:05 +01:00
Johan Preynat 2d08a6e6de Return not found when decodeURIComponent from params fail (#3863) 2025-12-12 13:27:19 +01:00
conico974 d539409fc7 Bump Next.js version to 15.4.9 (#3864) 2025-12-12 08:46:04 +01:00
Taran Vohra c30ed46021 Bump meta links resolution percentage to 50% (#3807) 2025-12-11 21:58:16 +05:30
Zeno Kapitein fc98a4a881 Fix rounded corners on list-line TOC style (#3861) 2025-12-11 10:51:03 +00:00
Zeno Kapitein 6278a1844e Support greeting and fix suggested questions if there are no custom questions (#3856) 2025-12-11 11:45:42 +01:00
Zeno Kapitein 86e7aa6c5e Fix search results cursor not updating (#3858) 2025-12-11 11:45:33 +01:00
Nolann B. 8761cee1bb Enhance discriminator handling in OpenAPISchema (#3855) 2025-12-11 08:11:14 +01:00
Zeno Kapitein 44feb3b140 Rework dynamic icons (AIChatIcon, AISearchIcon) (#3853) 2025-12-09 12:15:05 +01:00
conico974 6b43773752 chore: update dependencies and remove Suspense on ISR requests (#3852) 2025-12-08 09:26:15 +01:00
Zeno Kapitein 62438f447e Update FontAwesome to 7.1.0 (#3851) 2025-12-06 12:54:04 +01:00
Nolann B. 87d68ea59e Fix OpenAPI oneOf/allOf merge (#3844) 2025-12-05 17:26:18 +01:00
Claire Chabas 5f9c80e4f2 Use link title for footer links and page cards (#3849) 2025-12-05 14:25:43 +01:00
Zeno Kapitein 9c69cc413f Add customizable suggested questions (#3789) 2025-12-05 13:25:26 +01:00
Claire Chabas b77623b9f3 Fix link title fallback in ToC item (#3845) 2025-12-04 12:58:20 +01:00
Zeno Kapitein 1430ceebea Update Docs Embed with new styling and tabs (#3823) 2025-12-04 10:01:25 +01:00
Zeno Kapitein 3fbd6f0d50 Hide empty Footer correctly (#3842) 2025-12-03 12:21:38 +00:00
Claire Chabas 8f0b97906b Support new "link title" page option (#3798) 2025-12-03 13:20:22 +01:00
Zeno Kapitein d460dc4b39 Update Stepper styling for theme-muted (#3841) 2025-12-03 12:18:44 +00:00
Zeno Kapitein 27b9f7817b Remove corner radius of hint block with heading (#3839) 2025-12-03 10:43:02 +00:00
Zeno Kapitein 9e062b10d3 Make semantic text colors more vibrant (#3838) 2025-12-03 10:39:41 +01:00
Nolann B. 74f89316d9 Fix hidden section not found (#3836) 2025-12-02 11:37:55 +01:00
Zeno Kapitein 9e1d7b296b Update AIChat context card open delay (#3834) 2025-12-02 10:24:18 +01:00
Nolann B. b49caafa9c Support for hidden sections (#3831) 2025-12-01 14:48:48 +01:00
Zeno Kapitein f372eec393 Make annotations visible on hover, fix accessibility and outro animations (#3822) 2025-11-28 17:15:29 +01:00
Zeno Kapitein 65ae941687 Replace AI Chat generated content in e2e (#3833) 2025-11-28 16:46:04 +01:00
Viktor Renkema 75b8efa155 Remove sticky prop on Updates block (#3832) 2025-11-28 10:08:21 +01:00
Utku Ufuk 34b0c67c91 Stop filtering by language when site space language is unspecified (#3825) 2025-11-27 17:57:57 +03:00
Zeno Kapitein e0ace99707 Fix flaky tests in AI Chat greeting (#3828) 2025-11-27 09:50:03 +01:00
Viktor Renkema 3553db0989 Add RSS feed for pages with update blocks (#3824)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-11-26 20:34:25 +01:00
Zeno Kapitein 5c14948cf3 Prevent flaky tests in AI Chat followup suggestions (#3827) 2025-11-26 18:39:51 +01:00
conico974 8e4e2a2333 Update wranglerVersion to 4.43.0 (#3826) 2025-11-26 18:35:18 +01:00
Zeno Kapitein 0f4c53cfbd Update Assistant e2e tests (#3814) 2025-11-26 17:09:48 +00:00
Zeno Kapitein c156e946b0 Update Assistant styling (#3797) 2025-11-24 12:41:00 +01:00
Nolann B. 461e15fd00 Add x-gitbook-prefix and x-gitbook-token-placeholder for OpenAPI security scheme (#3820) 2025-11-22 10:44:05 +01:00
conico974 2989f79708 Add 'use server' directive to markdown renderer (#3815) 2025-11-20 09:28:00 +01:00
Nolann B. 344842f5ce Improve OpenAPI circular references (#3821) 2025-11-19 22:44:32 +01:00
Samy Pessé c51076efc4 Fix links to other spaces in embeddable view (#3819) 2025-11-19 10:38:53 +00:00
Zeno Kapitein 38e34dcce3 Optimise page outline (#3813) 2025-11-19 11:26:16 +01:00
Nolann B. 04a4175678 Remove hidden site spaces from navigation (#3795) 2025-11-18 22:49:41 +01:00
Nolann B. 3e40b4db50 Fix OpenAPI basic auth placeholder (#3816) 2025-11-18 20:18:59 +01:00
Greg Bergé 854e612e5d Fix handling of broken links in buttons (#3812) 2025-11-18 11:12:27 +01:00
Samy Pessé 27a461be9d Fix NPM publishing once and for all (#3811) 2025-11-16 02:16:20 +01:00
Samy Pessé 3d83dadb12 New attempt at fixing NPM publishing with trusted publisher (#3810) 2025-11-16 01:34:57 +01:00
Samy Pessé 8dc5c18b9f Pass proper GITHUB_TOKEN to the publish script 2025-11-16 00:35:16 +01:00
Samy Pessé e4756c23a7 Use dedicated step to publish to NPM 2025-11-16 00:28:37 +01:00
Samy Pessé 79ad7bcb59 Fix publishing to NPM (#3809) 2025-11-16 00:25:26 +01:00
Samy Pessé 5b0917a13a Remove --dry-run in publish script 2025-11-15 23:36:35 +01:00
Samy Pessé 6d6f4ef6dd Fix the script to publish npm packages and make it more robust 2025-11-15 23:31:14 +01:00
Samy Pessé fa69a64a2b Merge branch 'main' of https://github.com/GitbookIO/gitbook 2025-11-15 19:47:52 +01:00
Samy Pessé d1a85d1f91 Try fixing the publishing workflow 2025-11-15 19:47:36 +01:00
Samy Pessé 05bf9d7a59 Version Packages (#3733)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-15 19:34:29 +01:00
Samy Pessé 2e0b3370c8 Fix permissions for publish workflow 2025-11-15 19:32:41 +01:00
Samy Pessé 10995e09ca Use trusted publishing for publishing NPM packages (#3808) 2025-11-15 19:26:26 +01:00
Samy Pessé e7cc3ff61f Revert "Use NPM dependency for eval-estree-expression"
This reverts commit 59d259074b.
2025-11-15 18:13:10 +01:00
Samy Pessé 59d259074b Use NPM dependency for eval-estree-expression 2025-11-15 17:58:54 +01:00
Zeno Kapitein 96c4ae4f2d Fix Heading in hint blocks are huge in published content (#3806) 2025-11-14 15:26:46 +01:00
Taran Vohra 64f3596df2 Output generic alternates for page metadata (#3804) 2025-11-14 16:32:19 +05:30
Taran Vohra 27d6c3af39 Use an available alternate when switching variants in a page (#3796) 2025-11-14 15:19:40 +05:30
Nolann B. 1de4eb7973 Fix @modelcontextprotocol/sdk version (#3805) 2025-11-13 15:38:33 +01:00
Nolann B. df1966df25 Bump Scalar (#3803) 2025-11-13 15:29:25 +01:00
conico974 1001aaf94e Refactor DynamicTabs to use NavigationStatusContext (#3800) 2025-11-13 14:37:09 +01:00
Zeno Kapitein 155e448c8f Support primary link in logo (#3802) 2025-11-13 12:59:17 +01:00
Viktor Renkema ca2a94ee1a Support sticky date + new third date format (#3801) 2025-11-13 12:09:53 +01:00
Nolann B. a512c90bda Re-arrange OpenAPI Scopes for OAuth2 (#3799) 2025-11-12 19:40:21 +01:00
conico974 9cbefa89c7 Filter invalid events sent to the API (#3792) 2025-11-12 11:54:13 +01:00
Taran Vohra c8bba7bfe1 Resolve page meta links for metadata (#3793) 2025-11-12 16:12:27 +05:30
Viktor Renkema 4e04db8a1b Support rendering upcoming Update blocks (#3794) 2025-11-12 10:16:02 +01:00
Nolann B. b45feafeff Disable OpenAPI "Try it" when no servers are defined (#3791) 2025-11-10 17:25:22 +01:00
Zeno Kapitein 3876f7081e Edgeless Assistant window on depth: flat sites (#3788) 2025-11-10 14:37:28 +01:00
Nolann B. b4a021a192 Add heredoc support for cURL JSON body (#3790) 2025-11-10 11:20:53 +01:00
Zeno Kapitein 1d92dc1350 Update site layout and blocks with container queries (#3780) 2025-11-10 10:55:03 +01:00
Nolann B. e4ba35fdae Rework OpenAPI Authorizations + scopes (#3786) 2025-11-06 21:20:09 +01:00
Viktor Renkema 35dcaca971 Support custom cover heights (#3779) 2025-11-05 14:21:47 +01:00
Zeno Kapitein 2aa4f2e0b8 Update DropdownMenu styling (#3787) 2025-11-05 08:40:53 +01:00
Zeno Kapitein 6d56df7c10 Treat pages without visible TOC items as no-toc (#3783) 2025-11-05 08:40:15 +01:00
Zeno Kapitein 4534ea83c3 Implement unique title tags for sections & variants (#3768) 2025-11-04 11:36:28 +01:00
Zeno Kapitein 9022d87ec7 Add page breadcrumbs to search results (#3784) 2025-11-04 00:31:32 +01:00
Brett Jephson a49781e200 Expand code block when printing (#3782) 2025-11-03 15:26:18 +00:00
Steven H 065a5e6e55 Disable Workers Observability (#3781) 2025-11-03 11:17:33 +00:00
Zeno Kapitein e27986b17a Reword "Everywhere" search scope to "All docs" (#3776) 2025-11-03 09:57:21 +01:00
Zeno Kapitein 8606f222c9 Add padding to TOC page group items (#3777) 2025-11-03 09:56:39 +01:00
Nolann B. 2c3066e3ae Improve OAuth2 scopes handling in OpenAPI (#3775) 2025-10-30 15:46:29 +01:00
Zeno Kapitein 55c0b03a41 Support translations and generic variants together (#3772) 2025-10-30 15:03:50 +01:00
conico974 3e548e4186 Initialize hash on mount and improve navigation handling (#3774) 2025-10-30 14:33:26 +01:00
Viktor Renkema 3aea302648 Revert "Support cover image heights and positioning changes" (#3771) 2025-10-30 10:24:22 +01:00
Zeno Kapitein 1d1f0a7c6a Fix search container shadow (#3770) 2025-10-29 11:14:31 +01:00
conico974 590729cce5 Redirect to space base path on fallback true (#3745) 2025-10-29 10:55:27 +01:00
conico974 b554d7d13f Add isRoot prop to PagesList for root header offset (#3759) 2025-10-29 10:50:53 +01:00
conico974 9568b8a7c8 Add keep_names option to wrangler configurations (#3769) 2025-10-29 10:00:19 +01:00
Zeno Kapitein 683bb8f634 Add confirmation to "Copy MCP URL" page action (#3763) 2025-10-28 12:42:02 +01:00
Zeno Kapitein 7d6a31627f Revert contentref anchor title (#3767) 2025-10-28 12:27:59 +01:00
Brett Jephson c31e5648f5 RND-8495: page group emoji and icon resolution (#3766) 2025-10-28 10:23:08 +00:00
Zeno Kapitein 0c01832684 SearchInput fixes (#3762) 2025-10-28 09:50:31 +01:00
Zeno Kapitein 6b1c9fef86 Fix opacity of ScrollContainer buttons on bold themes (#3764) 2025-10-28 09:37:23 +01:00
Claire Chabas cc976995cc Fix image frame width (#3765) 2025-10-27 20:01:12 +01:00
Nolann B. ebbb476589 Improve ancestor label in references (#3761) 2025-10-27 16:49:48 +01:00
Greg Bergé 89655cb428 Fix reset scroll issue (#3760) 2025-10-27 16:41:39 +01:00
Zeno Kapitein 3676d64bdc Update search scope UI (#3750) 2025-10-27 15:57:47 +01:00
Viktor Renkema f682df1e01 Support cover image heights and positioning changes (#3758) 2025-10-27 15:55:50 +01:00
Greg Bergé 6c9893945d Fix scrolling hash into tabs / expandable (#3757) 2025-10-25 15:30:52 +02:00
Greg Bergé a2fb627ae1 Fix scroll jump and scroll reset (#3756) 2025-10-25 13:04:34 +02:00
Nolann B. f9f80117f0 Add alt text support to card covers (#3715) 2025-10-24 16:27:23 +02:00
conico974 63bf4dbb16 Fix cross-space content reference resolution and enhance anchor text rendering (#3754) 2025-10-24 15:26:59 +02:00
Brett Jephson b3fa503aff Expandable code block (#3753) 2025-10-24 14:01:31 +01:00
Zeno Kapitein 0ae7cfd790 Improve AI Search branding (#3752) 2025-10-24 14:36:31 +02:00
Brett Jephson 8fdc5c89dc Fix: height issues with webframe (#3749) 2025-10-24 10:29:46 +01:00
Zeno Kapitein 758b34d46b Move the "Ask <query>" to the bottom of search results if query is not a question (#3751) 2025-10-24 10:58:51 +02:00
Zeno Kapitein 1b0613e482 Tweak language dropdown styling (#3748) 2025-10-22 22:00:52 +02:00
conico974 3164f1a99d Fix preloading images (#3747) 2025-10-22 15:09:42 +02:00
Greg Bergé 6b588af07d Clarify CONTRIBUTING.md (#3742) 2025-10-22 12:43:28 +00:00
Greg Bergé d436b04ff4 Fix jump when clicking tabs (#3746) 2025-10-21 18:30:33 +02:00
conico974 d159788d0b Add end-to-end tests for table of contents navigation (#3741) 2025-10-21 12:39:23 +02:00
Nolann B. 3355960d95 Fix ToggeableLinkItem display (#3744)
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2025-10-21 10:32:17 +02:00
Nolann B. c32d4494f2 Fix OpenAPI path display (#3743) 2025-10-19 21:44:26 +02:00
conico974 1dcf4df51f Fix table of contents display issue (#3740) 2025-10-17 19:20:33 +00:00
conico974 3c0e49d56b Fix cache race condition (#3738) 2025-10-17 19:42:16 +02:00
Greg Bergé c35d0e8954 Run tests automatically when "Version packages" PR is opened (#3737) 2025-10-17 14:28:38 +02:00
Greg Bergé c0d73d2f76 Improve tabs (#3735) 2025-10-17 12:13:09 +02:00
Viktor Renkema 454175ab86 Change copy of toolbar button (#3736) 2025-10-17 07:17:39 +00:00
Greg Bergé 6feb8fcd6a Support emojis in outline (#3734) 2025-10-16 23:49:12 +02:00
conico974 b8d3cb7ee5 Improve search error handling and token redirection (#3732) 2025-10-16 15:39:32 +02:00
Nolann B. 8ce73229c3 Add OpenAPI servers selection (#3726) 2025-10-16 14:27:42 +02:00
Greg Bergé 8eae6e970b Upgrade to bun 1.3 (#3731) 2025-10-16 10:22:52 +02:00
Greg Bergé a48b886dfd Add button to download file (#3730) 2025-10-15 17:46:47 +02:00
Greg Bergé 17de0838c5 Allow to split pages between list items (#3729) 2025-10-15 14:02:41 +02:00
Greg Bergé baf3496980 Improve inline tooltip performances (#3728) 2025-10-15 13:30:38 +02:00
Samy Pessé ae33c09aa7 Version Packages (#3724)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-15 13:30:24 +02:00
Greg Bergé 7ff69c3b72 Fix bun token (#3727) 2025-10-15 10:43:44 +02:00
Greg Bergé 6142d6bf80 Build all packages using tsdown (#3722) 2025-10-14 14:39:19 +02:00
Brett Jephson 75543b140f Update Node version to work with globalThis.process.getBuiltinModule (#3725) 2025-10-14 11:13:26 +01:00
Nolann B. 511f1cfbe5 Add scroll margin to Expandable (#3723) 2025-10-13 15:54:32 +02:00
Samy Pessé 46fa9d954c Version Packages (#3720)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-13 08:42:09 +02:00
Greg Bergé 5e1ed5358e Add npm token to publish with bun (#3721) 2025-10-13 11:38:57 +05:30
Greg Bergé 295f03db49 Force republishing packages (#3719) 2025-10-11 07:36:58 +00:00
Claire Chabas bdde3929fc Fix web frame height (#3709) 2025-10-10 17:41:54 +02:00
conico974 4f4210d24a Update @opennextjs/cloudflare to version 1.9.2 (#3710)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-10-10 17:20:11 +02:00
Greg Bergé c038ec8f81 Fix package publishing (#3718) 2025-10-10 13:39:39 +00:00
Samy Pessé 75b52eac28 Version Packages (#3651)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-10 15:11:33 +02:00
Greg Bergé ad4d07dc06 Support inline icon colors (#3717) 2025-10-10 14:51:40 +02:00
Greg Bergé 75086740f8 Fix bundling of embed package (#3716) 2025-10-10 10:57:06 +02:00
Nolann B. eea8f1e00f Enhance OpenAPI security scopes handling (#3712) 2025-10-09 10:16:12 +02:00
Greg Bergé 4f9abfb6f2 Display headings from reusable content in TOC (#3708) 2025-10-08 12:17:05 +02:00
conico974 252afceab0 Store customization in a cookie for preview route (#3707)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-10-08 10:37:52 +02:00
Viktor Renkema bef191660a Allow hiding toolbar for one session or persistent (#3698) 2025-10-08 09:48:38 +02:00
Zeno Kapitein 145625142d Unify section display condition (#3706) 2025-10-07 15:12:08 +02:00
Greg Bergé f70a2aa310 Disable flashing when navigating between sections (#3705) 2025-10-07 12:00:42 +02:00
Samy Pessé 56e46ce8ab Add Italian translation (#3704) 2025-10-06 17:07:30 +02:00
Nolann B. 754cc11e0b Fix merge logic for allOf alternatives (#3515) 2025-10-06 14:37:51 +02:00
Nolann B. 3551d147e9 Move to urls for page actions (#3703) 2025-10-06 09:42:37 +02:00
Zeno Kapitein 162cfe4163 Hide sections if only one is present (#3701) 2025-10-04 20:16:02 +02:00
Nolann B. 6815dd197e Fix markdownPageURL for PageActions (#3702) 2025-10-04 19:02:30 +02:00
Zeno Kapitein d74f8a7098 Improve wide page width (#3692) 2025-10-03 18:52:05 +02:00
Zeno Kapitein 1e53f46521 Fix button content truncate (#3693) 2025-10-03 18:51:32 +02:00
Claire Chabas 1a6216df4f Fix URL used for Ask to LLM (#3697) 2025-10-03 14:24:24 +02:00
Nolann B. a629900324 Add dev script for @gitbook/expr (#3700) 2025-10-03 13:23:15 +02:00
Nolann B. a4c33997fe Fix OpenAPI alternatives not showing (#3699) 2025-10-03 13:06:10 +02:00
Brett Jephson badf3a92f6 RND-8165: make sure we get alt text coming through on InlineImage (#3696) 2025-10-03 09:40:34 +01:00
Claire Chabas e0fb9ac3ca Support image frame (#3695) 2025-10-02 10:17:48 +02:00
spastorelli f3e40410c4 Adapt OpenAPI code samples to prefill API key using visitor data (#3666) 2025-09-30 11:09:05 +02:00
spastorelli 8e99871004 Highlight discriminator properties in oneOf, allOf, anyOf objects (#3691) 2025-09-29 17:09:21 +02:00
Zeno Kapitein 1e4e54e752 Fix rounded outline items (#3694) 2025-09-29 13:33:40 +02:00
spastorelli 50c1be3447 Move inline expression evaluation from API to GBO (#3687) 2025-09-26 10:55:09 +02:00
spastorelli 319a1e536c Ensure operation security reqs inherits from spec-level when not defined (#3690) 2025-09-26 09:11:52 +02:00
conico974 50cb0c6e15 Fix NavigationLoader on search and cmd/click (#3689)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-26 08:34:41 +02:00
Zeno Kapitein ceb32b1a2e Redo search scopes (#3688)
Co-authored-by: Claire Chabas <claire.chabas@gmail.com>
2025-09-25 16:49:53 +00:00
Zeno Kapitein f8c54b4d34 Improve assistant demo (#3686) 2025-09-25 17:07:07 +02:00
Greg Bergé c06ad70f8a Improve fonts loading (#3682) 2025-09-25 13:17:21 +02:00
Brett Jephson 0ef5dc8538 Update column width sizing (#3676) 2025-09-25 08:14:15 +01:00
Zeno Kapitein 28adc9b5f1 Add graphic to embed assistant demo page (#3685) 2025-09-24 14:37:41 +00:00
Zeno Kapitein 912d1693f5 Fix section group alignment (#3684) 2025-09-24 13:58:34 +00:00
Samy Pessé c23d4ef331 Fix crash for card cover defined without objectFit (#3683) 2025-09-24 11:01:06 +02:00
Zeno Kapitein c21693fe50 Improve screen reader accessibility for hints (#3680) 2025-09-24 10:23:34 +02:00
Zeno Kapitein c06b3dd60d Improve default site icon (#3679) 2025-09-24 10:22:49 +02:00
conico974 f173e014db Use cloudflare regional cache instead of our own implementation. (#3673)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-24 10:11:11 +02:00
conico974 1b458c436e Fix NavigationLoader not visible with bold header (#3681)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-24 09:54:57 +02:00
conico974 b42b0b3910 Add navigation loader and rename context provider (#3642)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2025-09-23 15:00:27 +02:00
Brett Jephson 2fc21272ff RND-7985: accessible search (#3637) 2025-09-23 11:36:50 +01:00
Zeno Kapitein 379d486488 Expose "Best match" when site has translations (#3670) 2025-09-22 22:31:17 +02:00
Zeno Kapitein cba583a31c Use siteSpace title instead of space title in search (#3677) 2025-09-22 18:03:50 +02:00
Zeno Kapitein 7a504b7cc5 Fix OpenAPI response dropdown & vertical overflow (#3678) 2025-09-22 18:03:43 +02:00
Addison 8ec5fd0986 Add blob: to style source for integrations (#3675) 2025-09-22 18:41:16 +09:00
Addison b9ad997fcb Add blob to style source for integrations 2025-09-22 17:01:16 +09:00
spastorelli 529f940a45 Fix OpenAPISecurities and code sample not using operation security requirements (#3671) 2025-09-22 10:00:46 +02:00
Viktor Renkema 8a3c15911f Update tooltip labels in docs sites toolbars (#3674) 2025-09-22 09:28:42 +02:00
spastorelli d7948e34b2 Fix OpenAPI response showing as JSON instead of YAML (#3669) 2025-09-18 18:40:24 +02:00
conico974 9f42211993 Enhance tracking with geolocation headers and continent info in analytics proxy (#3667)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-18 18:31:21 +02:00
conico974 65e62307bf Update @opennextjs/cloudflare to version 1.8.3 (#3668)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-18 18:04:38 +02:00
spastorelli 469b332da2 Add support for inline expressions rendering with visitor data on GBO side (#3660) 2025-09-18 16:30:52 +02:00
Zeno Kapitein e434442f1e Support site section groups (#3664) 2025-09-18 15:28:39 +02:00
Samy Pessé 9344431bfc Fix API URL we proxy insights events to (#3665) 2025-09-18 13:14:25 +00:00
conico974 622ecbec1d Handle broken links in header and footer (#3663)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-18 11:22:57 +02:00
conico974 fa1382b289 Add responsive image sizes for RecordCard component (#3654)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-18 08:55:11 +02:00
Zeno Kapitein b8388e5a2a Add ScrollContainer component, use for section tabs (#3661) 2025-09-17 22:20:32 +02:00
Zeno Kapitein 2bce12798f Fix section group dropdown offset on big screens (#3662) 2025-09-17 19:10:52 +02:00
Greg Bergé f89e889d18 Convert relative URL to absolute in Markdown (page and copy) (#3659) 2025-09-17 18:12:01 +02:00
Zeno Kapitein 2b8a2d24b6 Add stable scroll gutter to search container (#3656) 2025-09-17 17:27:47 +02:00
Zeno Kapitein b85eccdff5 Refactor site section tabs (#3655) 2025-09-17 17:27:34 +02:00
Viktor Renkema d924e655c0 Add visibility label and smoothen magnification (#3657) 2025-09-16 16:34:21 +02:00
Zeno Kapitein b932e4e2e9 Scope search across sections and variants (#3640)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-16 12:07:29 +02:00
Samy Pessé ca4a3e8c9a Track utm on links from the toolbar (#3653) 2025-09-15 16:51:31 +02:00
John Cashman b498521196 Refactor robots.txt generation logic (#3621)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-09-15 14:37:02 +02:00
Samy Pessé 8a8cf2025d Skip using __session cookie if it's in the old format (#3652) 2025-09-15 12:10:51 +02:00
Samy Pessé ed7d47d9b8 Toolbar for authenticated GitBook users (#3650)
Co-authored-by: Viktor Renkema <vrenkema@gmail.com>
2025-09-15 10:32:23 +02:00
Viktor Renkema b5494cc04b Restyle the toolbar in CR and revision preview links (#3631)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-09-15 08:33:23 +02:00
Samy Pessé 229f2ba9d5 Version Packages (#3646)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-12 16:40:28 +02:00
spastorelli 6da36558f6 Fix exports in gitbook/expr package.json after removing src (#3648) 2025-09-12 16:20:29 +02:00
Samy Pessé ca71c1fad6 Fix MCP in production (#3647) 2025-09-12 14:59:47 +02:00
Samy Pessé 262afa357b Implement an MCP server on each docs site (#3641) 2025-09-12 13:19:16 +02:00
Samy Pessé 7375d3c597 Version Packages (#3645)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-12 11:37:22 +02:00
spastorelli 3548fa6dff Fix eval estree expr cjs named import when gitbook/expr is imported in playwright tests (#3643) 2025-09-12 11:20:33 +02:00
conico974 cb73040e0f Bump Next.js to version 15.4.0 (#3644)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-12 11:17:43 +02:00
Samy Pessé 872d36b64f Version Packages (#3629)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-11 18:06:31 +02:00
spastorelli e1ff17e655 Fix bundling of @gitbook/expr package (#3639) 2025-09-11 17:24:06 +02:00
Utku Ufuk f3010bd28a Stop using deprecated search API param (#3638) 2025-09-11 15:26:35 +03:00
spastorelli 8ff1e3b619 Port latest changes from gbx util-expr (#3636) 2025-09-09 18:55:41 +02:00
Viktor Renkema d7596bf454 Don't show toolbar when rendering within GitBook app preview (#3635) 2025-09-09 15:09:52 +02:00
Zeno Kapitein aea5eb10ae Persist language choice across sections (#3633) 2025-09-09 15:06:28 +02:00
Zeno Kapitein 1165a81cf5 Language selector fixes (#3632) 2025-09-08 17:22:42 +02:00
conico974 61d1a0192e Fix null contentRef handling in RecordColumnValue (#3566)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-08 13:54:58 +02:00
Zeno Kapitein f9a2977621 Better handling for external link "mailto:" in Hovered Card in GBO (#3630) 2025-09-08 10:31:30 +00:00
spastorelli 24f601d594 Small optim in resolveTryItPrefillForOperation and remove uneeded dep (#3628) 2025-09-08 10:29:59 +02:00
Samy Pessé 7b34537bbe Version Packages (#3599)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-06 11:40:05 +02:00
conico974 145f385fdb Add pagination for llms-full.txt file (#3619)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-05 17:20:25 +02:00
Greg Bergé 360aa1c6d6 Upgrade Scalar dependencies (#3626) 2025-09-05 16:57:21 +02:00
conico974 ccc0975240 Fix error on parameters being an object instead of an array (#3623)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-05 16:17:50 +02:00
Zeno Kapitein 791135040a Add language selector to site header (#3622) 2025-09-05 12:15:07 +00:00
spastorelli 12c9d765ff Adapt OpenAPI to eval adaptive exprs & prefill TryIt config (#3607) 2025-09-05 13:39:12 +02:00
Greg Bergé 45207288a1 Support bold in headings (#3609) 2025-09-05 11:47:39 +02:00
Viktor Renkema 177afa5828 Handle non-defined objectFit on card cover images (#3625) 2025-09-05 10:42:57 +02:00
Zeno Kapitein d51b79edbb Fix "Search bar is broken on site with sections when header is disabled" (#3624) 2025-09-04 13:48:08 +02:00
conico974 a2720ac49d Update @opennextjs/cloudflare to version 1.7.1 (#3620)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-03 16:09:27 +02:00
Viktor Renkema 5f6853d812 Support new object-fit property on individual card cover image (#3594) 2025-09-03 09:11:17 +02:00
Zeno Kapitein 17dd382dc3 Add "original" color step and apply to bg-primary-solid elements (#3618) 2025-09-02 16:08:21 +02:00
Nolann B. 98e42cfe79 Handle OpenAPI ignored responses (#3614) 2025-09-01 14:25:35 +02:00
Samy Pessé c890e01004 Fix order in robots.txt preventing indexation of images by Google (#3617) 2025-09-01 11:21:11 +00:00
Samy Pessé 659be551bb Track insight event when embedded assistant is displayed (#3616) 2025-09-01 12:59:40 +02:00
Zeno Kapitein 78a632b7fe Fix Edit on GitHub button doesn't take you to GitHub (#3615) 2025-09-01 12:01:36 +02:00
spastorelli 4927e964b7 Add support for YAML content type in request body/example (#3612) 2025-09-01 10:56:09 +02:00
Samy Pessé 262a9b198b Fix embed script caching (#3611) 2025-08-29 22:35:14 +02:00
Samy Pessé 4f3588240c Unify layout and fix tracking of ask_question (#3610) 2025-08-29 16:10:28 +02:00
Nolann B. abbae3ec4d Remove scroll-nojump utility and update layout structure (#3494)
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2025-08-29 11:22:55 +02:00
conico974 1b8609ac60 fix next start not working (#3608)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-28 16:36:06 +02:00
Samy Pessé 193d591e9f Use space.language as source of truth (#3606) 2025-08-27 18:27:29 +02:00
Zeno Kapitein 1edc5d622a Assistant hold message (#3603) 2025-08-27 13:48:57 +00:00
Zeno Kapitein 61b8507996 OpenAPI: Make empty responses clickable (#3605) 2025-08-27 13:38:24 +00:00
Zeno Kapitein 6f368b5cf3 Fix embed assistant window width on small screens (#3604) 2025-08-27 15:33:37 +02:00
Brett Jephson 9201e2cf52 Add vertical align property to column block (#3596)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-08-27 13:58:26 +01:00
Zeno Kapitein 2e0d706d43 Fix corner radius of active section on "Line" sidebar style (#3601) 2025-08-27 13:15:07 +02:00
Nolann B. 7fefe4919c Catch OpenAPI validation errors (#3602) 2025-08-27 12:29:50 +02:00
Zeno Kapitein 729921f338 Move "Edit on Git" and "Export PDF" actions into page actions dropdown (#3574) 2025-08-27 11:32:55 +02:00
Zeno Kapitein 1839ea2678 Fix content min-height with sections (#3597) 2025-08-27 11:30:22 +02:00
Zeno Kapitein f08dd29364 Fix Search results are not clickable (#3595) 2025-08-26 20:33:37 +00:00
conico974 ca2dfa3065 Rollback to Next.js 15.3 (#3600)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-26 18:26:32 +02:00
Utku Ufuk 10ded437f0 Turn on the "noUncheckedIndexedAccess" flag in all TS config files (#3587) 2025-08-26 19:25:54 +03:00
Zeno Kapitein d1fdc13441 Remove ligatures from Lato font (#3592) 2025-08-26 15:04:22 +00:00
Zeno Kapitein a8fca0e033 Fix custom assistant keyboard shortcut (#3598) 2025-08-26 14:57:08 +00:00
Samy Pessé 9251b33959 Version Packages (#3535)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-26 14:21:26 +02:00
Samy Pessé 81a6bd756a Embeddable GitBook API (#3593) 2025-08-26 14:08:24 +02:00
spastorelli ada195d329 Add gitbook/expr package (#3586) 2025-08-25 13:10:19 +02:00
conico974 1211ebea9b Proxy analytics events (#3570)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-25 11:34:54 +02:00
Zeno Kapitein 2e6e28eb54 Fix: Long strings overflow out of message bubble in docs assistant (#3588) 2025-08-22 19:23:05 +02:00
Zeno Kapitein 854c448bad Custom assistants followup (#3584) 2025-08-22 17:32:10 +02:00
conico974 64a0b0f169 Add logging for request duration and response details in 2c (#3583)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-22 17:15:00 +02:00
Zeno Kapitein 13ff22b6a8 Fix AI Search follow-up question closing search (#3585) 2025-08-22 14:51:21 +02:00
Samy Pessé 8927e8fbbb Embeddable Docs Assistant (#3572)
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2025-08-22 09:44:51 +02:00
Samy Pessé 25e2b40d47 Update icons to latest version (#3582) 2025-08-21 15:31:13 +02:00
Zeno Kapitein a60af8ff9c Simplify tooltip implementation, add HoverCard (#3580) 2025-08-21 11:14:08 +02:00
Zeno Kapitein 27b328cfd7 Fix pageaside height on pages with hero cover (#3581)
Co-authored-by: Claire Chabas <claire.chabas@gmail.com>
2025-08-21 10:58:13 +02:00
conico974 fa6ef0c01c Remove motion animations from ScrollSectionList (#3573)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-20 19:14:01 +02:00
Zeno Kapitein 6217a2ed3b Page outline: scroll to active item (#3578) 2025-08-20 15:55:16 +02:00
Zeno Kapitein ffa866cb99 Fixes to search modal (#3577) 2025-08-20 10:50:58 +00:00
Zeno Kapitein 011aa931cf Integration tools styling (#3575)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-08-20 09:48:55 +00:00
Taran Vohra 68308154b5 Register custom assistant (#3569)
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2025-08-19 12:07:47 +02:00
Zeno Kapitein 36af03f4d5 Pageaside fixes (#3567) 2025-08-18 18:50:00 +02:00
Samy Pessé cbc71a56b6 API to let integrations define custom tools for Docs Assistant (#3568) 2025-08-17 10:03:54 +02:00
Zeno Kapitein 1420180220 Assistant: Prevent sending queries multiple times (#3565) 2025-08-15 08:44:31 +02:00
Zeno Kapitein e1b2cf6d81 Fix page outline scroll (#3563) 2025-08-14 17:52:56 +02:00
conico974 c0e9c49679 Optimize cache retrieval performance and reduce costs on 2C (#3542)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-14 09:14:40 +02:00
conico974 afc4bd4415 Improve image resizing logic in fetchImage function (#3553)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-13 15:35:14 +02:00
Zeno Kapitein bcfa8d8b19 Improve vertical alignment of site items and fix floating page aside (#3560) 2025-08-13 14:36:07 +02:00
Zeno Kapitein 43766d6519 Fix custom logo not rendering on the published site (#3557) 2025-08-13 14:29:49 +02:00
Greg Bergé 42c17f5c74 Improve OpenAPI parsing errors (#3555) 2025-08-13 13:38:14 +02:00
Greg Bergé d655d3eece Support "objectivec" as alias for Objective C syntax (#3556) 2025-08-13 13:37:06 +02:00
conico974 3500de95e8 Never follow redirect in the middleware (#3552)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-13 12:58:43 +02:00
Taran Vohra 7f03b94949 Fix an issue where recommended questions werent using the current siteSpace context (#3559) 2025-08-13 11:06:05 +05:30
Zeno Kapitein f0cf060191 Disable AI Search tests for now (#3561) 2025-08-12 22:12:51 +02:00
Zeno Kapitein 5a137e7b14 Always show placeholder while generating (#3554) 2025-08-12 15:43:29 +02:00
conico974 28f7fbaa8a Fix image format handling and enhance PageCoverImage component (#3527)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-08 18:30:00 +02:00
Nolann B. ff96bb5787 Support new coverDefinitionDark for cards & image type (#3547) 2025-08-08 18:08:11 +02:00
conico974 87841124e0 Add HashContext and HashProvider for managing URL hash state (#3541)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-08 17:01:21 +02:00
conico974 a650b58276 feat: add tracing to server actions (#3543)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-08 16:10:06 +02:00
Zeno Kapitein 388b20d44f Clear AI chat stream properly (#3544) 2025-08-07 19:07:25 +02:00
Claire Chabas ea7e94fc51 Fix search bar layout shit caused by the ToC (#3545) 2025-08-07 15:19:20 +02:00
Zeno Kapitein d30bcbabdb Improve Button and ButtonGroup styling (#3540) 2025-08-06 16:10:55 +02:00
Zeno Kapitein fb858a1941 Tweaks to AIChatButton and AIChatInput (#3539) 2025-08-06 16:10:05 +02:00
Samy Pessé cc2e615b9a Emit a <link rel="alternate"> for the markdown version of a page (#3528) 2025-08-06 08:55:02 +02:00
Samy Pessé a862cde6b4 Use new API to fetch the reusable content document (#3536) 2025-08-06 08:54:43 +02:00
Zeno Kapitein e2b7bec162 Add linewrap to AI actions dropdown (#3534) 2025-08-05 12:17:00 +00:00
Zeno Kapitein c1b577ecc9 Updates to RU translation (#3532) 2025-08-05 14:10:45 +02:00
Zeno Kapitein ba7ec14f76 Fix z-index of search highlight (#3533) 2025-08-05 14:08:46 +02:00
Zeno Kapitein 9f0117723c Add russian translation (#3531) 2025-08-05 12:52:30 +02:00
conico974 f72b6a20db Validate 'next-action' header format in server action requests (#3523)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-05 11:31:43 +02:00
conico974 2f798209bb Bump OpenNext to 1.6.4 (#3520)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-05 11:18:56 +02:00
conico974 eec8f16424 Increase timeout for search answer visibility check (#3530)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-05 11:18:12 +02:00
Samy Pessé 885264f132 Version Packages (#3506)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-05 11:03:50 +02:00
Greg Bergé f1a6dec951 Less strict OpenAPI parser (#3529)
Co-authored-by: Nolann Biron <biron.nolann@gmail.com>
2025-08-05 08:45:45 +00:00
Claire Chabas 44f41510fa Fix close buttons tooltips (#3525) 2025-08-04 15:22:59 +02:00
spastorelli f220229e5e Fix stepper title misalignment (#3524) 2025-08-04 09:34:22 +02:00
Greg Bergé 61b4cb8d7c Upgrade Argos (#3522) 2025-08-01 16:07:35 +02:00
Greg Bergé 185cdb4883 Fix scrolling to anchor positionning (#3521) 2025-08-01 15:44:27 +02:00
Zeno Kapitein 2cdba53451 Upgrade to Tailwind v4 (#3206) 2025-08-01 13:03:44 +02:00
Greg Bergé 950f6c36cf greg/rnd 6997 fix long openapi responses label (#3518) 2025-07-31 12:08:57 +00:00
Zeno Kapitein ea37977b1e Fix inconsistent state between q= and ask= and keyboard navigation (#3519) 2025-07-31 13:20:27 +02:00
Greg Bergé 81da82aed7 Increase tests max duration (#3517) 2025-07-30 18:33:56 +00:00
Greg Bergé 39a3518651 Show a HoverCard when an internal link is broken (#3516) 2025-07-30 18:09:57 +02:00
spastorelli d903273759 Strip visitor.* param when normalizing URL (#3512) 2025-07-30 17:22:09 +02:00
Taran Vohra d130532f69 Suggest questions in the current space context (#3514) 2025-07-30 20:36:40 +05:30
Zeno Kapitein 94fcd21f34 Improve ?ask= init handling (#3513) 2025-07-30 13:17:15 +02:00
conico974 1d2db4ae2b Fix cookie handling for AI assistant server actions (#3509)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-30 12:29:51 +02:00
Steven H d270b4af9b Don't track events if a special middleware header is set. (#3511) 2025-07-30 09:26:01 +01:00
Zeno Kapitein ccfbd3c1a8 Fix AI Search Query (#3510) 2025-07-30 07:44:37 +00:00
Claire Chabas 5c4923557f Temporarily remove highlighting for PowerShell and C++ in Safari to avoid page crash (#3508) 2025-07-29 20:39:26 +02:00
Zeno Kapitein b1f608b865 Implement basic URL scheme for Assistant & AI Search (#3503) 2025-07-29 16:13:44 +02:00
conico974 c7993756dd Fix regional caching of tag cache (#3504)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-29 13:27:56 +02:00
Nolann B. d02465823f Prevent page breaks inside blocks when printing (#3507) 2025-07-28 18:08:08 +02:00
Greg Bergé 6b9f6b5b79 Improve french translations (#3501) 2025-07-28 14:05:04 +02:00
Taran Vohra df7de8fb0c Fix an issue where links were not rendering properly in Ask AI answers (#3505) 2025-07-28 16:43:18 +05:30
Samy Pessé 72f5ce3985 Version Packages (#3480)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-24 13:20:46 +02:00
conico974 04c80262e2 Make sure to use the correct version for adaptive content (#3487)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-24 11:40:12 +02:00
Valentino Hudhra 56d1b8fe2d Fix overflowing recommended follow up questions in assistant (#3502) 2025-07-24 11:03:04 +02:00
Zeno Kapitein 054a1634ab Improve AI Actions dropdown (#3499) 2025-07-24 10:40:13 +02:00
conico974 6ff63f2db6 Don't throw on >500 API error (#3500)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-24 09:41:47 +02:00
Greg Bergé b5ad0ce1e5 Fix OpenAPI v2 parsing (#3498) 2025-07-23 17:21:05 +02:00
Claire Chabas 2ba7e54c81 Fix headings styles in hint block (#3497) 2025-07-23 15:46:44 +02:00
Zeno Kapitein 0a858e74ad Fix React warning (#3496) 2025-07-23 10:56:12 +02:00
Zeno Kapitein b0c534fc2c Tweaks to AI Chat and AI Actions Dropdown (#3495) 2025-07-22 19:18:35 +02:00
Steven H 250c194a02 Add nosnippet to announcement banner. (#3490) 2025-07-22 15:03:35 +01:00
Zeno Kapitein 5b10cde936 Fix background corners showing through Powered through GitBook badge (#3493) 2025-07-22 15:15:38 +02:00
Zeno Kapitein 4aeb81bfa5 Fix search input styling & text cutoff (#3491) 2025-07-22 14:58:20 +02:00
conico974 955cebf944 Add cache tags for fetchFileSystem (#3489)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-22 13:41:31 +02:00
Greg Bergé 7c951ef437 Support new colors in text formatting (#3492) 2025-07-22 13:40:03 +02:00
Brett Jephson 68f0dbcc35 RND-7464: confirm in content kit button (#3488) 2025-07-22 10:51:25 +01:00
Zeno Kapitein 39c4f76aea Add fullwidth page option (#3344) 2025-07-21 11:20:17 +02:00
Zeno Kapitein 2fe58e8cbb Page actions customisation (#3485)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-07-18 21:10:38 +02:00
Zeno Kapitein 765141a4d8 Make "last updated" section optional (#3486) 2025-07-18 18:46:11 +00:00
Zeno Kapitein eb1bd3aa01 AI response feedback buttons (#3482) 2025-07-18 15:40:04 +00:00
Zeno Kapitein b7284b0223 Small tweaks to assistant styling (#3481) 2025-07-17 14:31:53 +00:00
conico974 5bbd254742 Support webp and avif formats for og image (#3478)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-17 11:36:07 +02:00
Nolann B. 611e28626a Remove padding on first Heading in Columns (#3479) 2025-07-16 12:51:37 +02:00
Samy Pessé 2a3bb0eb22 Version Packages (#3453)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-15 17:40:24 +02:00
conico974 6816f0fa5a Add browserslist and improve CSS masks for compatibility (#3471)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-15 17:15:56 +02:00
Greg Bergé 17386776ec Add missing changeset to release @gitbook/openapi-parser (#3477) 2025-07-15 12:01:40 +00:00
Nolann B. 9ee9082614 Improvement for CopyMarkdown action (#3465) 2025-07-15 13:41:25 +02:00
Nolann B. 334cfdd522 Fix responsive class for SearchInput button (#3476) 2025-07-15 12:50:57 +02:00
Greg Bergé 8c961a5dbd Fix OpenAPI spec validation (#3473) 2025-07-15 08:56:08 +02:00
conico974 4708956e77 Ensure schema.security is always an array (#3472)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-14 22:01:06 +02:00
Zeno Kapitein 813dd03dbb Fix AIChatInput focus & styling (#3470)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-07-14 11:48:34 +01:00
conico974 0522dbc61e Handle markdown errors gracefully (#3468)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-11 19:11:09 +02:00
Zeno Kapitein acb9f53156 Inline search modal (#3416) 2025-07-11 16:29:03 +01:00
conico974 6016846156 Add cache life inference from API response headers (#3467)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-11 17:10:00 +02:00
Greg Bergé cf1fae5837 Fix using CMD or CTRL to open in a new tab (#3466) 2025-07-11 14:25:37 +00:00
Nolann B. 7212345466 Fix AI Actions dropdown and LLM integration (#3464) 2025-07-11 12:14:06 +02:00
conico974 e38caf78ff Implement consistent logging across the application (#3368)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-11 10:36:03 +02:00
Greg Bergé 0a8dd67541 Better aria for breadcrumbs (#3461) 2025-07-11 09:45:14 +02:00
Nolann B. ed684c19a9 Move AI Actions markdown fetching to client-side (#3459) 2025-07-10 16:05:53 +02:00
Zeno Kapitein 52ab3681e3 Reposition AI Actions dropdown (#3460) 2025-07-09 14:38:23 +00:00
Zeno Kapitein 9cc5a787e9 Generalise keyboard shortcuts, add Cmd+J to AI Chat (#3457) 2025-07-09 12:13:36 +00:00
Nolann B. 0003030bf5 Implement AI actions dropdown (#3431) 2025-07-08 23:33:36 +02:00
Zeno Kapitein 9ad8f45d38 Rebrand assistant (#3458) 2025-07-08 21:31:14 +02:00
conico974 a3855257c8 Gracefully handle OpenAPI fetch errors (#3455)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-08 17:35:23 +02:00
conico974 9169c2f053 Fix site redirect resolution with length check for source paths (#3454)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-08 17:15:57 +02:00
Greg Bergé abe028bd38 Fix Link behaviour reloading the page (#3456) 2025-07-08 14:30:12 +00:00
conico974 12c2451f4f Bump OpenNext to latest versions (#3451)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-08 15:06:23 +02:00
Samy Pessé 8daede5427 Ensure all content links are resolved relatively to preview (#3450) 2025-07-08 13:05:31 +02:00
Samy Pessé 4c89aa60eb Adapt cache header parsing to support s-maxage (#3452) 2025-07-08 12:55:00 +02:00
Greg Bergé d0a3f64ae4 Always open header logo link in the current tab (#3445) 2025-07-08 09:02:49 +00:00
Samy Pessé 433d66482d Version Packages (#3446)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-08 11:01:57 +02:00
Greg Bergé bd553bcf9e Expose isJson and isYaml in @gitbook/openapi-parser (#3447) 2025-07-08 10:41:46 +02:00
Nolann B. f5894bcf74 Fix incorrect revision ID in Markdown fetch logic (#3448) 2025-07-08 09:41:12 +02:00
Zeno Kapitein 1b59e7ce36 Styling improvements to AI Chat Followup questions (#3444) 2025-07-07 19:02:01 +02:00
Samy Pessé 32d6f5845d Version Packages (#3247)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-07 12:49:53 +00:00
Greg Bergé 4f5cbfe732 Upgrade @scalar/openapi-parser (#3442) 2025-07-07 14:33:40 +02:00
Steven H afba8fcb34 Rating columns in tables now use a solid icons instead of unfilled. (#3434) 2025-07-07 10:05:45 +01:00
conico974 377a4899e4 Adjust cache life for error responses (#3423)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-07 09:38:48 +02:00
Greg Bergé 938bdeb34b Fix visual tests (#3440) 2025-07-06 09:02:31 +02:00
Greg Bergé d216eaa3f6 Upgrade Argos & disable bad plugin (#3438) 2025-07-05 21:37:20 +02:00
conico974 5ca5da0f21 Fix handling of bad requests to prevent unnecessary 500 errors (#3432)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-04 17:06:37 +02:00
Greg Bergé ebe6eb3b39 Support custom monospace fonts (#3430) 2025-07-04 13:13:36 +00:00
Zeno Kapitein 3bfe347096 Show section tabs when there is a single section group present (#3387) 2025-07-04 14:55:40 +02:00
Samy Pessé 7b38f89078 Update API client and condition for AI assistant (#3429) 2025-07-03 20:34:26 +02:00
Greg Bergé 09b689b0ea Fix table display in print (#3428) 2025-07-03 17:44:55 +02:00
Nolann B. 2350baa75f Support for OpenAPI Array request body (#3420) 2025-07-03 17:18:26 +02:00
Nolann B. bc1eca815e Error handling for AI chat (#3411) 2025-07-03 14:26:40 +02:00
conico974 78c10340e7 Increase max line length for code highlighting (#3415)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-03 13:05:42 +02:00
Valentino Hudhra c16890a3a6 Align README with CONTRIBUTING guides regarding bun version (#3427) 2025-07-03 12:40:08 +02:00
Addison 3b4fe2827a Update community ad on sponsored/community sites (#3421) 2025-07-03 11:03:16 +02:00
Samy Pessé 7e807aabb5 Don't prevent sending message in AI chat when loading followup suggestions (#3426) 2025-07-03 10:16:42 +02:00
Samy Pessé 973c74ee69 Adapt AI assistant to upcoming API changes (#3422) 2025-07-02 20:58:23 +02:00
Zeno Kapitein efed0b0617 Turn on Docs Assistant for another site (#3425) 2025-07-02 18:02:12 +02:00
Zeno Kapitein ca3b9aca5d Improve AI Chat context popup (#3424) 2025-07-02 18:01:24 +02:00
Taran Vohra e2afc07ab2 Fix resolution of page by resolving site redirects before space redirects (#3414) 2025-07-02 17:30:18 +05:30
Brett Jephson e8fb84d362 Remove flip hash from columns (#3413) 2025-07-02 09:41:53 +01:00
Samy Pessé 57f951a7d9 Remove AI page link summaries (#3418) 2025-07-02 08:40:55 +02:00
conico974 28008667ed Make Emoji component synchronous (#3417)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-01 17:10:16 +02:00
Samy Pessé f3affc3034 Add support for MCP tools (#3410) 2025-07-01 09:58:59 +02:00
conico974 59da30f3c6 Add support for cover image repositioning (#3404)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-30 19:07:48 +02:00
Greg Bergé 2db721112a Fix OpenAPI error (#3408) 2025-06-30 15:56:22 +00:00
Samy Pessé b60039b7d5 Fix links to other spaces in a section (#3409)
Co-authored-by: Steven H <steven@gitbook.io>
2025-06-30 17:47:23 +02:00
Steven H 8fe9c9afea Fix an issue where same-site references across spaces created invalid links. (#3407) 2025-06-30 16:03:27 +01:00
Zeno Kapitein 216ba7a556 Add analytics on AI chat (#3405) 2025-06-30 08:41:12 +00:00
Samy Pessé a2ff57081b Fix changesets 2025-06-29 10:41:30 +02:00
Zeno Kapitein 40dbd9a14b AI Chat followup (#3403) 2025-06-28 17:55:56 +02:00
Nolann B. e5bac69c5c Fix markdown page generation for groups (#3402) 2025-06-27 23:32:35 +02:00
Zeno Kapitein 8d6598393f AI Chat UI (#3397)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-06-27 17:43:14 +02:00
Greg Bergé 59b86eb9dc Correctly handle OpenAPI errors (#3401) 2025-06-27 11:15:21 +02:00
Steven H 52c9f6da2f Fix an issue with link resolution in cross-space reusable content. (#3392)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-06-27 11:14:38 +02:00
Nolann B. dd65987c94 Include page group children under the .md route (#3400) 2025-06-27 09:53:18 +02:00
conico974 ace61901a4 Fix ScrollSectionsList with suspended component (#3390)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-26 15:37:51 +02:00
conico974 e6c3c7635d refactor toc components to client (#3394)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-06-26 11:35:02 +02:00
conico974 5134d9e7f8 Avoid unnecessary proxy URL lookups (#3377)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-25 17:21:09 +02:00
conico974 6821fb2937 Downgrade esbuild to fix wrangler dev issues (#3396)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-25 16:50:09 +02:00
Taran Vohra cfe4812045 Replace all usage of getPublishedContentByUrl (#3395) 2025-06-25 18:55:02 +05:30
Samy Pessé 14843ac343 Fix changesets (#3393) 2025-06-25 14:42:49 +02:00
Samy Pessé 8fb6d7465f Remove v1 and replace by v2 (#3391)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-25 13:28:04 +02:00
Samy Pessé 0ef647586f Hooks and APIs for AI v2 (#3385) 2025-06-25 09:48:28 +02:00
Nolann B. b4039627d9 Handle nullish OpenAPI mediaTypeObject (#3389) 2025-06-24 17:25:38 +00:00
Addison 4a295e6db4 Update contributing guide (#3381) 2025-06-24 17:55:24 +02:00
Greg Bergé 5726999a5a Fix viewing a page from a revision (#3388) 2025-06-24 15:31:06 +00:00
conico974 58d7f3c140 Fix revision ID handling for computed content (#3386)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-24 15:00:33 +02:00
Samy Pessé 07eab986d5 Optimize fetching of revisions (#3382) 2025-06-23 19:15:17 +02:00
Samy Pessé 711cf38f9b Use getRevision in getRevisionFile (#3379) 2025-06-23 18:43:41 +02:00
Zeno Kapitein 4f7c0eea9f Clicking an active TOC item toggles its descendants (#3378) 2025-06-23 17:27:34 +02:00
conico974 f3448de2da Improve efficiency of the queue (#3373)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-23 15:09:29 +02:00
Viktor Renkema ec92075ef8 Set tw classes correctly for horizontal alignment in tables (#3375) 2025-06-23 14:35:32 +02:00
Nolann B. 1e013cd87c Optional label in OpenAPI x-codeSamples (#3376) 2025-06-23 13:54:15 +02:00
Samy Pessé f033734308 Support for site customization option to change how external links open (#3372) 2025-06-22 15:49:15 +02:00
Nolann B. 50c05164f7 Bump @scalar/api-client-react (#3371) 2025-06-22 12:03:10 +02:00
conico974 7fb9004b43 Properly redirect to the root page (#3369)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-20 19:15:21 +02:00
Greg Bergé 4721403986 Hide scrollbar on sections (#3367) 2025-06-20 12:31:18 +00:00
Samy Pessé df848ef64e Icon in buttons and text alignment (#3365) 2025-06-20 12:08:28 +02:00
conico974 0b40ccd286 Remove buildId from composable cache key generation (#3366)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-20 12:06:44 +02:00
Greg Bergé caaa692f0b Allow to zoom image on mobile if relevant (#3364) 2025-06-20 10:25:58 +02:00
Greg Bergé 42d43e09c4 Always show scrollbars (#3363) 2025-06-20 08:41:21 +02:00
Samy Pessé 7ccbf55ba7 Set myself as codeowner for data cache (#3355) 2025-06-20 08:41:02 +02:00
conico974 81f5bfc2f9 Filter out malicious request (#3357)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-19 17:00:37 +02:00
Greg Bergé a7a713bca2 Scroll to active TOC when clicking a link (#3362) 2025-06-19 16:50:13 +02:00
conico974 d6613c787f Make skeletons client component (#3361)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-19 16:46:31 +02:00
Greg Bergé 427f748e1c Add metadata for adding site to Apple devices home (#3360) 2025-06-19 16:35:07 +02:00
Greg Bergé 4f5fec7213 Fix CodeBlock rendering (#3359) 2025-06-19 15:55:19 +02:00
Greg Bergé 8c0a53ab40 Fix page group not expanded by default (#3358) 2025-06-19 15:08:59 +02:00
conico974 2dfd5aab20 Update SpacesDropdown to manage active state based on current space (#3356)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-19 13:39:59 +02:00
conico974 8d3c6562a2 Implement a StylesProvider (#3352)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-18 19:02:32 +02:00
Zeno Kapitein dae019c115 Consistently show variant selector in section bar if site has sections (#3349) 2025-06-18 17:48:49 +02:00
Steven H 73e0cbb2d6 Fix an issue where PDF export URLs were not bringing their query params. (#3351) 2025-06-18 13:26:20 +01:00
Steven H 392f59450c Improve performance of InlineLinkTooltip (#3339) 2025-06-18 12:15:45 +01:00
conico974 b4918f60ce fix variant space change when pathname equals space-path (#3343)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-17 22:31:32 +02:00
Samy Pessé 8f7c304d58 Fix crash when rendering ogimage with invalid icon (#3348) 2025-06-17 20:25:21 +02:00
Samy Pessé a3a944d7dd Fix crash during rendering of ogimage for VA sites with default icon (#3347) 2025-06-17 19:38:47 +02:00
Samy Pessé 88a35ed057 Fix crash when integration is triggering invalid requests (#3346) 2025-06-17 19:23:52 +02:00
Samy Pessé 67998b6f15 Fix ogimage generation failing with some JPEG images (#3345) 2025-06-17 19:10:15 +02:00
Samy Pessé ff6d1150a5 Use getRevisionPageDocument on v2 (#3342) 2025-06-17 16:12:58 +02:00
Samy Pessé 500c8cb649 Don't crash ogimage generation on RTL text (#3341) 2025-06-17 13:04:50 +02:00
Samy Pessé 6859f7d239 Fix rendering of ogimage when logo or icon are AVIF images (#3336)
Co-authored-by: Steven H <steven@gitbook.io>
2025-06-17 00:31:07 +02:00
Samy Pessé dfa8a37be1 Don't cache unexpected API errors for more than a few minutes (#3334) 2025-06-17 00:16:52 +02:00
Samy Pessé 11a6511b7a Fix crash when integration script fails to render block (#3332) 2025-06-16 20:06:33 +02:00
Samy Pessé af98402655 Add support for inline icons (#3329) 2025-06-16 19:15:22 +02:00
Samy Pessé b7a0db3339 Fix rendering of ogimage with SVG logos (#3328) 2025-06-16 16:59:54 +02:00
Greg Bergé 382a19885b Hide empty section groups (#3327) 2025-06-16 13:52:42 +00:00
Utku Ufuk d99da6a3ae Ignore case while pattern-matching to highlight search results (#3326) 2025-06-16 12:33:00 +00:00
Samy Pessé 72cd0e59e6 Replace withoutConcurrency by a smarter React.cache (#3325) 2025-06-16 13:37:45 +02:00
Utku Ufuk 42d88da73c Fix UX issue about highlighting the search term in search result sections (#3323) 2025-06-16 13:31:15 +03:00
Taran Vohra 2863fe0dc1 Use resolvePublishedContentByUrl instead of the deprecated resolution endpoint (#3310) 2025-06-16 09:59:55 +05:30
conico974 87218062be Use Promise.all whenever possible (#3319)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-14 11:10:26 +02:00
Samy Pessé 8a3910e208 Experiment with hiding sections without a matching site space (#3318) 2025-06-13 15:55:04 +02:00
conico974 6294bbb53c Fix error propagating (#3317)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-13 15:46:37 +02:00
Zeno Kapitein a28a997851 Add margin to adjacent buttons (#3316) 2025-06-13 15:42:00 +02:00
spastorelli 28a9ee701e Add paypal.gitbook.com to ADAPTIVE_CONTENT_HOSTS list (#3315) 2025-06-13 10:10:10 +00:00
Samy Pessé 50e8b2ec52 Don't use next/server after (#3314) 2025-06-13 11:30:18 +02:00
Samy Pessé 315717fa2d Disable the background fetch of getRevisionPageDocument (#3313) 2025-06-13 11:17:33 +02:00
Samy Pessé da67711c90 Fix crash on Vercel (#3311) 2025-06-12 23:37:38 +02:00
Samy Pessé fecdbe8dde Support dynamic expressions (#3295) 2025-06-12 12:25:28 +02:00
Nolann B. 9316ccd1f4 Update Models page styling (#3307) 2025-06-12 10:57:37 +02:00
Samy Pessé c3b620e975 Preserve current variant when navigating between sections (#3306) 2025-06-12 09:30:08 +02:00
Nolann B. 7a00880e5b Improve support for OAuth2 security type (#3304) 2025-06-11 15:34:52 +02:00
Zeno Kapitein b6b597564d Reverse order of feedback smileys (#3300) 2025-06-11 14:44:07 +02:00
spastorelli d410381957 Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list (#3303) 2025-06-11 14:25:05 +02:00
conico974 902c3c6c1a apply customization for dynamic site context in v2 (#3302)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-11 14:05:30 +02:00
Zeno Kapitein 4fb2a4a6d7 Rework full-width layout, add support for full-width page option (#3293)
Co-authored-by: conico974 <nicodorseuil@yahoo.fr>
2025-06-11 14:03:45 +02:00
conico974 2d64c78787 return null for 404 cached page in the incremental cache (#3296)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-11 13:47:34 +02:00
Samy Pessé fbfcca5dae Fix rendering of font in ogimage (#3299) 2025-06-11 13:41:26 +02:00
Greg Bergé 664ae8bff6 Make heading from stepper visible in outline (#3301) 2025-06-11 13:40:59 +02:00
Samy Pessé fa3d2aae08 Fix inline expression crashing pages (#3297) 2025-06-11 12:27:24 +02:00
Zeno Kapitein e7a591d9f6 Fix border being added to cards (#3294) 2025-06-10 17:25:29 +00:00
conico974 2860beacdc revert to use withoutConcurrentExecution (#3292)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-10 19:05:07 +02:00
Zeno Kapitein 231167d3e1 Make icons for page groups more contrasting (#3286) 2025-06-10 17:48:06 +02:00
Brett Jephson c0ee60ec13 RND-6843: columns block (#3257)
Co-authored-by: Steven Hall <steven@gitbook.io>
2025-06-09 11:37:56 +01:00
conico974 f58b904ba6 Encode customization header (#3289)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-09 11:37:29 +02:00
conico974 d5992f1a5c Bump OpenNext to latest (#3288)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-09 10:40:44 +02:00
Samy Pessé fa12f9eeb6 Page cover dark-mode specific (#3287) 2025-06-08 14:57:50 +02:00
Samy Pessé 33726c88a0 Generate a llms-full version of the docs (#3285) 2025-06-08 12:13:14 +02:00
Zeno Kapitein 6aa3ff9c5e Fix various styling issues (#3283) 2025-06-08 00:38:41 +01:00
Zeno Kapitein 7d3fe23195 Add circular corners and depth styles (#3280)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-06-07 13:48:16 +02:00
Samy Pessé 521052d84f Remove withoutConcurrentExecution and withCacheKey (#3281) 2025-06-05 20:43:15 +01:00
Samy Pessé c730845e17 Fix missing title for button to close announcement banner (#3279) 2025-06-04 16:33:01 +01:00
Addison 7b5a3616c4 Add UTM tags to the preview site badge (#3277) 2025-06-03 16:07:55 +01:00
Zeno Kapitein c9373efdb5 Fix bold header links hover color (#3255) 2025-06-03 15:18:04 +02:00
Brett Jephson 015615de8a fix: defaultWidth/fullWidth options in images (#3274) 2025-06-03 12:18:01 +01:00
conico974 0f867e533a bump OpenNext to latest (#3276)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-02 17:40:04 +02:00
conico974 7d5a6d2255 Fix hydration error on V2 (#3272)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-02 14:23:15 +01:00
conico974 a1da4f023d Move DO to a 3rd worker (#3270)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-30 16:19:11 +02:00
conico974 666e8426e1 External middleware in cloudflare (#3254)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-30 14:56:00 +02:00
conico974 df2fa42bda Chores: create server worker (#3265)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-30 14:31:21 +02:00
conico974 b3a7ad6d2c Fix href being empty in TOC (#3269)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-28 17:07:33 +02:00
conico974 2932077bf9 Remove trailing slash from linker (#3268)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-27 21:13:55 +02:00
Nolann B. 957afd967f Add authorization header for OAuth2 (#3266) 2025-05-26 20:54:07 +02:00
Nolann B. 4c9a9d0416 Handle nested deprecated properties in generateSchemaExample (#3267) 2025-05-26 20:28:26 +02:00
Nolann B. 40df91a363 Deduplicate path parameters from OpenAPI spec (#3264) 2025-05-26 18:55:11 +02:00
conico974 af66ff71ed Add a force-revalidate api route (#3263)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-25 16:55:45 +02:00
Samy Pessé 6f8ecd6111 Use more strict context ID for images during preview (#3261) 2025-05-23 13:16:43 +02:00
conico974 fa3eb07617 Cache fonts and static image in memory for OG Image (#3258)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-23 10:20:07 +02:00
Samy Pessé dc4268db64 Compute correct urls for variants/sections when previewing unpublished site (#3259) 2025-05-23 10:01:15 +02:00
Nolann B. a0c06a72cc Indent JSON python code sample (#3260) 2025-05-23 09:13:20 +02:00
conico974 19c20c0be3 Fix OGImage icon fetch on cloudflare (#3253)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-21 15:39:52 +02:00
Viktor Renkema fae34e56ac Calculate class more explicitly & separately to fix horizontal table cell alignment issue (#3252) 2025-05-21 14:20:39 +02:00
conico974 ba0094a862 fix ISR in preview in cloudflare (#3250)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-21 09:52:05 +02:00
conico974 b259009c9c Add temporary logging for Cloudflare debugging (#3249)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-20 19:33:33 +02:00
conico974 d88dd5ed8a Update OpenNext (#3248)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-20 19:04:07 +02:00
Zeno Kapitein 57bb146075 Dynamic TOC height (#3233) 2025-05-20 14:13:57 +02:00
Zeno Kapitein 4b67fe5317 V2: Add urlObject.hash to linker.toLinkForContent (#3246) 2025-05-20 10:25:35 +02:00
Samy Pessé 938125e772 Version Packages (#3180)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-19 19:10:57 +02:00
conico974 1c8d9febb3 Custom Incremental Cache (#3239)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-19 17:00:09 +02:00
conico974 223edae17d Add env variables for the DOQueue (#3243)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-19 13:47:33 +02:00
Nolann B. 12a455dab5 Fix OpenAPI layout issues (#3242) 2025-05-18 14:50:05 +02:00
Nolann B. d00dc8ca72 Pass spec validation errors through OpenAPIParseError (#3240) 2025-05-18 13:20:17 +02:00
Samy Pessé 2c85a302fc Update opennext and leverage "use cache" on CF (#3204)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-16 13:58:35 +01:00
Addison 3460b78b45 Update docs.gitbook.com to gitbook.com/docs (#3187)
Co-authored-by: Tal Gluck <talagluck@gmail.com>
Co-authored-by: Taran Vohra <taranvohra@outlook.com>
2025-05-16 17:59:53 +05:30
Zeno Kapitein 95a1f652f9 Better print layouts: wrap code blocks & force table column auto-sizing (#3236) 2025-05-15 13:24:10 +02:00
spastorelli cc37e2ae15 Persist visitor params values in public visitor session cookie (#3231) 2025-05-14 14:16:13 +02:00
Claire Chabas 08382ce62f Improve tabs link support (#3211) 2025-05-14 11:10:09 +02:00
Nolann B. 80cb52a237 Handle OpenAPI alternatives from schema.items (#3235) 2025-05-14 08:05:08 +02:00
Nolann B. c6637b09e2 Use default value if string/number/boolean in generateSchemaExample (#3234) 2025-05-13 22:07:41 +02:00
Samy Pessé 778624af00 Only resize images with supported extensions (#3229) 2025-05-13 10:52:58 +02:00
Samy Pessé e15757d01f Use next@15.3.2 instead of canary (#3227) 2025-05-09 15:17:49 +02:00
Zeno Kapitein aed79fd6c3 Remove header logo rounding (#3225) 2025-05-09 14:45:38 +02:00
Greg Bergé 0c973a3292 Always link main logo to the root of the site (#3226) 2025-05-09 10:31:52 +00:00
spastorelli 56c923c0ed Adapt site resolution/lookup to pass visitor unsigned claims in addition to token (#3202)
Co-authored-by: taranvohra <taranvohra@outlook.com>
2025-05-09 15:34:20 +05:30
Samy Pessé 7d7806df30 Passthrough SVG images in image resizing (#3224) 2025-05-08 18:57:50 +02:00
Nolann B. cb5598dc19 Handle invalid OpenAPI Responses (#3223) 2025-05-08 11:54:46 +02:00
Greg Bergé 47f01edcb8 Fix E2E icons (#3222) 2025-05-08 13:04:04 +05:30
Greg Bergé e6ddc0f07f Fix sitemap URLs (#3220) 2025-05-08 08:54:28 +02:00
Zeno Kapitein 373f18f451 Prevent section group popovers from opening on click (#3213) 2025-05-07 13:57:58 +00:00
Taran Vohra 3f292065cd Update site redirect regex for validation (#3216) 2025-05-07 12:42:28 +05:30
Greg Bergé 5e975ab95b Fix code highlighting for HTTP (#3214) 2025-05-06 13:41:15 +00:00
Greg Bergé a3ec264764 Fix Python code sample "null vs None" (#3215) 2025-05-06 15:40:56 +02:00
Samy Pessé 5d504ffa4c Fix resolution of links in reusable content (#3212) 2025-05-06 10:30:55 +02:00
Samy Pessé 04999662db Use absolute URLs in sitemap.xml when pointing to sitemap-pages.xml (#3210) 2025-05-05 09:03:28 +02:00
Zeno Kapitein f7a34706c7 Allow high-contrast input colors to be used as-is (#3209) 2025-05-02 19:41:05 +02:00
Taran Vohra f328a41982 Revert getPublishedContentByUrl timeout back to 10s (#3208) 2025-05-02 11:16:31 +00:00
Nolann B. 20ebecb114 Missing top-level required OpenAPI alternatives (#3207) 2025-05-02 11:48:15 +02:00
Taran Vohra 5a69692f54 Temporarily bump getPublishedContentByUrl timeout to 30s on v1 (#3199) 2025-04-30 19:14:09 +05:30
Nolann B. 42ca7e1580 Fix openapi CR preview (#3197) 2025-04-30 11:44:39 +02:00
Nolann B. c4ebb3fecd Fix openapi-select hover in responses (#3196) 2025-04-30 11:27:03 +02:00
Zeno Kapitein 2a805cc082 Change OpenAPI schema-optional from info to tint color (#3194) 2025-04-29 19:30:11 +02:00
Zeno Kapitein 89a5816ee4 Fix OpenAPI disclosure label ("Show properties") misalignment on mobile (#3193) 2025-04-29 19:29:56 +02:00
Zeno Kapitein c3f6b8c003 Reduce chroma of text layers (#3188) 2025-04-29 19:29:42 +02:00
Zeno Kapitein 0e201d5899 Add border to filled sidebar on gradient theme (#3185) 2025-04-29 19:29:32 +02:00
Zeno Kapitein ae5f1abc58 Change Dropdowns to use Radix's DropdownMenu (#3189) 2025-04-29 18:30:47 +02:00
Nolann B. 580101d04e Schemas disclosure label causing client error (#3192) 2025-04-29 16:16:58 +02:00
Johan Preynat 81b4a4db53 Allow /~gitbook/image and /~gitbook/icon despite querystring parameters for SEO in robots.txt (#3191) 2025-04-28 12:07:34 +02:00
Steven H 8339e91e2b Fix images and other content refs in reusable content across spaces. (#3190) 2025-04-25 15:28:09 +01:00
Steven H 3119066728 Support for reusable content across spaces. (#3173) 2025-04-24 16:54:11 +01:00
Zeno Kapitein 326e28e9b0 Restyle openapi blocks (#3169) 2025-04-24 16:53:29 +02:00
Johan Preynat 90f0127ada Use react@19 on v2 and v1 (#3184) 2025-04-24 16:53:23 +02:00
Viktor Renkema 0353a8186c Have all nodes within a text-cells adhere to alignments (#3175) 2025-04-24 11:43:20 +02:00
Steven H dd043df00f Revert investigation work around URL caches. (#3183) 2025-04-23 16:53:33 +01:00
Steven H 97b7c79bc6 Increase logging around caching behaviour causing page crashes. (#3182) 2025-04-23 15:04:00 +01:00
Steven H 634e0b4302 Improve error messages around undefined site sections. (#3179) 2025-04-23 11:47:28 +00:00
Greg Bergé 2a23f1f003 Improve performances of TOC (#3176) 2025-04-23 09:31:22 +02:00
Samy Pessé 88ffdcc303 Version Packages (#3172)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-22 11:20:21 +02:00
Nolann B. b6b09d42af Fix OpenAPI responses select placement and icon (#3174) 2025-04-22 10:59:42 +02:00
Nolann B. ebc39e9db9 Missing select icon (#3171) 2025-04-18 16:26:59 +02:00
Samy Pessé 0df3849ea2 Version Packages (#3157)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-18 13:40:34 +02:00
Greg Bergé 8ed1bda2f2 Translate OpenAPI blocks (#3166) 2025-04-18 13:29:55 +02:00
Nolann B. d67699a1f1 Add OpenAPI Webhook block (#3160) 2025-04-18 08:35:44 +02:00
Greg Bergé 3363a18856 Merge simple alternatives (#3165) 2025-04-16 09:14:12 +00:00
Amrit Kahlon ad1dc0b914 Bump scalar version (#3161)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-04-15 18:00:07 -07:00
Samy Pessé e4c7307c99 Fix CORS error when loading fonts in 2c (#3163) 2025-04-14 17:05:31 -07:00
Samy Pessé f9c942c8ff Fix CORS error when loading icons in 2c (#3162) 2025-04-14 16:52:39 -07:00
Greg Bergé eeb977fc03 Fix Python JSON code example (#3159) 2025-04-14 19:50:40 +00:00
Greg Bergé 4b8a62122a Show sections tabs only if there is at least two sections (#3158) 2025-04-14 14:09:34 +02:00
Addison e44e947fc1 Update cookie banner to use American English (#3154) 2025-04-14 18:43:01 +09:00
Nolann B. 7588cfe1c5 Improve OpenAPIResponses examples and schemas (#3123) 2025-04-14 11:16:35 +02:00
Greg Bergé a9a643f222 Improve icon stabilization (#3147) 2025-04-13 05:41:47 +00:00
Samy Pessé 77d6cb75be Improve error message when image resizing failed (#3148) 2025-04-12 17:44:23 -07:00
Samy Pessé 1942d221df Bump the cache of getPublishedContentByUrl in v1 (#3146) 2025-04-12 07:18:26 -07:00
Greg Bergé 041601ed65 Upgrade Argos (#3145) 2025-04-12 15:48:51 +02:00
Steven H 5567281a85 Bump getPublishedContentByUrl cache. (#3144) 2025-04-11 17:13:01 +01:00
Samy Pessé 4c3753046b Cloudflare: fix image resizing (#3143) 2025-04-11 16:48:43 +02:00
Samy Pessé 89f8e16184 Cloudflare: resize images in v2 using direct fetch (#3141) 2025-04-11 15:18:44 +02:00
Samy Pessé a3e1125ced Hotfix for v1 crashes (#3139) 2025-04-10 22:00:41 +02:00
Samy Pessé 4dae8acfed Cloudflare: fix image resizing (#3138) 2025-04-10 19:30:15 +02:00
Samy Pessé bc081c4344 Fix cache revalidation for computed revisions (#3134) 2025-04-10 16:36:02 +02:00
Samy Pessé e4275b235d Version Packages (#3137)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 13:31:27 +02:00
Samy Pessé 77397cac82 Fix version of @gitbook/api referenced in cache-tags (#3136) 2025-04-10 13:30:28 +02:00
Samy Pessé b6a08bc679 Version Packages (#3084)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 13:18:22 +02:00
Samy Pessé 116575c811 Changeset for cache-tags (#3135) 2025-04-10 13:16:01 +02:00
Samy Pessé 4ba7bd5ce1 CF Preview URL: Extract worker URL and run visual tests (#3132) 2025-04-09 18:25:37 +02:00
Samy Pessé 223ab5377f Test cloudflare preview URL (#3130) 2025-04-09 17:24:38 +02:00
Samy Pessé ae43ca146c Fix preview URLs on Cloudflare env (#3129) 2025-04-09 16:00:21 +02:00
Taran Vohra 42342893d6 Fix incoming URL for requests that were proxied (#3127) 2025-04-09 19:10:14 +05:30
Nolann B. eedefdd5c9 Handle optional security headers (#3128) 2025-04-09 15:07:33 +02:00
Samy Pessé f5e152dd74 Fix hovering links in docs.gitbook.com (#3125) 2025-04-09 11:43:12 +02:00
Zeno Kapitein 95ea22d9c5 Cache AI Page Link summary (#3106)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-04-09 09:59:35 +02:00
Taran Vohra f23023ddfa Use the proxy request identifier for storing the VA cookie in proxy sites (#3091) 2025-04-09 12:49:22 +05:30
Samy Pessé da74077954 Remove proxy package (#3124) 2025-04-08 23:58:19 +02:00
Nolann B. 29aaba5949 Override Scalar's overscroll-behavior (#3120) 2025-04-08 19:16:46 +02:00
Zeno Kapitein daf41fc728 Footer design tweaks (#3109) 2025-04-08 18:44:57 +02:00
Zeno Kapitein b08eb1af59 Fix list of variants overflow (#3114) 2025-04-08 18:44:44 +02:00
Zeno Kapitein 8322cd3e35 Fix sections being displayed when there is only 1 (#3113) 2025-04-08 18:44:08 +02:00
Samy Pessé 444039c142 Fix opening of links when v1 is embedded in an iframe (#3122) 2025-04-08 18:43:06 +02:00
Samy Pessé 3b0eff7f71 Fix getRevisionPageByPath in v2 not encoding the path correctly (#3121) 2025-04-08 16:29:08 +00:00
Samy Pessé c765463e38 Fix ogimage generation crashing when site is using a custom WOFF2 font (#3119) 2025-04-08 16:44:28 +02:00
Samy Pessé e8ca25b67e Fix caching in Cloudflare version using R2 as a backend (#3118) 2025-04-08 13:37:07 +02:00
Brett Jephson 528eee3fc6 RND-548: add subscript and superscript support (#3110) 2025-04-08 11:32:12 +01:00
Steven H 168a4fa998 Add support for buttons to GitBook. (#3117) 2025-04-08 09:18:29 +01:00
Samy Pessé 3ea75adba4 Update turbo to 2.5.0 and fix cache config (#3116) 2025-04-08 09:56:05 +02:00
Nolann B. cbd768a095 Improve OpenAPI codesample (#3090) 2025-04-08 09:35:22 +02:00
Samy Pessé 2d01653a78 Fix ogimage on Cloudflare and resizing of ads image (#3107) 2025-04-08 01:29:34 +02:00
Nolann B. aa3357a57d Fix OpenAPISchemas description padding (#3115) 2025-04-07 21:54:33 +02:00
Samy Pessé de53946896 Sanitize the back URL used when rendering PDF (#3111) 2025-04-07 17:03:09 +02:00
Samy Pessé d51204148e Revert debugging of ogimage errors (#3105) 2025-04-06 13:33:48 +01:00
Samy Pessé 319761a39e Fix revalidation not working because of memoization (#3104) 2025-04-06 01:27:22 +02:00
Samy Pessé 7fbf01a348 Update opennext to 1.0.0-beta.0 (#3102) 2025-04-05 23:51:12 +02:00
Steven Hall c22490bd80 Prefer @vercel/og for icon generation. 2025-04-05 09:22:13 +01:00
Steven Hall 860fd6d02e Use system-ui font in icons. 2025-04-05 09:12:20 +01:00
Steven Hall c43c524804 Fix a typing issue using ArrayBuffer.detached 2025-04-05 09:06:17 +01:00
Steven H 66bb938d85 Use Inter font when generating icons to prevent ArrayBuffer re-use. (#3101) 2025-04-05 09:01:41 +01:00
Steven H 0e6d928496 Use Inter font when generating icons to prevent ArrayBuffer re-use. (#3100) 2025-04-05 08:47:56 +01:00
Steven H f07982d9f8 Investigate issue where icons and ogimage would occasionally fail. (#3099) 2025-04-05 08:15:02 +01:00
Steven H b92ecfad58 Implement retry logic for the DO cache to prevent when revalidating content. (#3098) 2025-04-04 23:44:32 +01:00
Zeno Kapitein 416bde70bc Link tooltips + AI link summaries (#3088)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-04-04 20:56:42 +02:00
Steven H 580f7ade15 Better debugging of GitBook v1s revalidate endpoint. (#3097) 2025-04-04 17:36:24 +01:00
Zeno Kapitein cdffd7c47b Desaturate text colours by decreasing chroma for the last steps of the color scale (#3096) 2025-04-04 18:00:01 +02:00
Greg Bergé 5a959cf041 Fix a regression on OpenAPI example bg (#3095) 2025-04-04 15:05:53 +00:00
Zeno Kapitein 31466546a2 Change overlay close icon to close (#3089) 2025-04-04 16:42:47 +02:00
Steven H 90ead98eaa Better error handling in revalidate v1 (#3094) 2025-04-04 14:59:14 +01:00
Samy Pessé 65e9cb9d5c Update next to "canary" for v2 (#3093) 2025-04-04 14:39:43 +02:00
Greg Bergé e59076a074 Improve OpenAPI schemas block ungrouped style (#3092) 2025-04-04 14:28:44 +02:00
Nolann B. 653920dec9 Add tests for generateSchemaExample (#3086) 2025-04-03 19:42:40 +02:00
Greg Bergé 23cedd2d8b Hide deprecated properties in examples (#3085) 2025-04-03 19:40:52 +02:00
Nolann B. 70c4182fd8 Improve OpenAPI schema style (#3079) 2025-04-03 16:10:20 +00:00
Samy Pessé b62b101065 Do not use insights cookie if disabled (#3087) 2025-04-03 17:30:43 +02:00
Zeno Kapitein b9c929beeb Update page breadcrumb color (for links-accent crumbs) (#3082) 2025-04-03 16:34:14 +02:00
Nolann B. 2b6c593289 Remove stable from x-stability (#3083) 2025-04-03 11:33:39 +02:00
Samy Pessé c0b339e406 Version Packages (#3075)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-02 13:36:04 +02:00
Nolann B. da485f5144 Fix read-only in generateSchemaExample (#3069) 2025-04-02 13:03:51 +02:00
Nolann B. 139a8050a5 Fix OpenAPI enum display (#3077) 2025-04-02 10:15:19 +02:00
Nolann B. da7b369ffd Fix missing headers in OpenAPIResponses (#3074) 2025-04-01 21:57:36 +02:00
Samy Pessé 432da64919 Version Packages (#3073)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 18:13:52 +02:00
Nolann B. 7d0b422200 Handle grouped OpenAPISchemas (#3071) 2025-04-01 17:24:43 +02:00
Zeno Kapitein fb90eb0acc Limit tinted background on bold theme to sites with filled sidebar (#3072) 2025-04-01 16:43:08 +02:00
Samy Pessé b647538e41 Version Packages (#3033)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 16:35:09 +02:00
Samy Pessé e50797a5c1 Adapt new computed content and add tag for translation (#3070) 2025-04-01 15:10:55 +02:00
Nolann B. 5b1e01c786 Support for x-stability property (#3064) 2025-03-31 13:49:37 +02:00
Nolann B. 813b2af06b Support for x-enumDescriptions and x-gitbook-enum (#3066) 2025-03-31 12:18:57 +02:00
Nolann B. 6f71da88d8 Fix padding in OpenAPI schemas (#3067) 2025-03-31 12:03:53 +02:00
Samy Pessé fa91eb7eaf Fix dark mode PDF (#3065) 2025-03-28 18:00:33 +01:00
Zeno Kapitein c756761591 Add breadcrumbs to search results (#2772) 2025-03-28 13:29:05 +01:00
Samy Pessé 434af90366 Fix image resizing when using proxy by passing signing identifier in site URL data (#3063)
Co-authored-by: Taran Vohra <taranvohra@outlook.com>
2025-03-28 12:28:12 +01:00
Valentino Hudhra 0f41e194cb Fix dismissed annoucement banner ignored when switching sites (#3057) 2025-03-28 10:56:18 +01:00
Nolann B. bd353489e9 Fix missing alternative schemas (#3058) 2025-03-28 06:53:28 +01:00
Samy Pessé 77fd393f15 Fix click on search items when embedded in an iframe and track announcement links (#3061) 2025-03-27 20:13:14 +01:00
Taran Vohra 8701b5e078 Use the site canonical URL when rewriting in the middleware (#3060) 2025-03-27 19:39:39 +01:00
Nolann B. 61db166f92 Add OpenAPI write-only indicator (#3059) 2025-03-27 18:21:38 +01:00
Zeno Kapitein 0d9b9cea0f Fix announcement banner margin (#3056) 2025-03-27 15:59:47 +00:00
Nolann B. 7bb37c7f0a Move filterSelectedOpenAPISchemas to @gitbook/openapi-parser (#3054) 2025-03-27 15:53:41 +01:00
Taran Vohra 5b2bf828e3 Use stable site URL data for route rewrite in the middleware (#3053) 2025-03-27 18:26:06 +05:30
Samy Pessé c85d3f9752 Serve a markdown version of the page when adding a .md extension (#3055)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-03-27 12:35:18 +00:00
Zeno Kapitein 4d7088bbf0 Stop variant selector from growing too large (#3043) 2025-03-27 10:42:18 +01:00
Samy Pessé dd6ab6044c Fix crash when using user mention or accessing invalid change requests (#3051) 2025-03-27 01:04:00 +01:00
Samy Pessé d236bf029c Fix flash for site with dark default mode (#3050) 2025-03-26 18:12:45 +01:00
Zeno Kapitein d70d566abe Support site announcement banner (#3008)
Co-authored-by: Valentino Hudhra <v.hudhra@gmail.com>
2025-03-26 16:37:05 +01:00
Nolann B. a25fdedea9 Replace $ref with $reference in json-decycle (#3049) 2025-03-26 14:16:03 +00:00
Nolann B. 373183a2cd Safe parse OpenAPI JSON schema (#3046) 2025-03-26 13:55:22 +01:00
Samy Pessé e9fa50dfb5 Trim search query before doing search (#3042) 2025-03-26 10:18:18 +00:00
Samy Pessé 57ca4e0e88 Fix crash when integration is not installed on the space (#3040) 2025-03-26 10:19:57 +01:00
Samy Pessé bc90adb73a Allow indexation of images to ensure favicons display on Google (#3041) 2025-03-26 10:17:57 +01:00
Nolann B. 1505ddbd5a Fix multiple request examples selector not showing (#3039) 2025-03-25 20:48:57 +01:00
Greg Bergé cd99ed57fb Fix OpenAPI spec rendering (#3037) 2025-03-25 15:13:18 +00:00
Samy Pessé 27de1cd01d Default to the first ID as being active in scroll detection (#3036) 2025-03-25 15:39:45 +01:00
Nolann B. ae78fc5394 Fix XML in code sample (#3030) 2025-03-25 15:11:45 +01:00
Samy Pessé 40e8e69c6d Disallow crawling by web-robots of search/ask URLs (#3035) 2025-03-25 13:51:09 +00:00
Nolann B. e84a46a558 Fix OpenAPI tabs indicator overflow (#3032) 2025-03-25 10:03:58 +01:00
Samy Pessé 18a72d4466 v2: improve caching (#3031) 2025-03-25 09:40:54 +01:00
Zeno Kapitein d7aa5fa003 Remove loading state for recommended questions (#3029) 2025-03-24 19:19:12 +01:00
Samy Pessé 27532a5f41 Version Packages (#2988)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-24 17:43:16 +01:00
Nolann B. 48c18c03b9 Add option to pass scalar plugins (#3027) 2025-03-24 14:11:14 +01:00
Samy Pessé bba2e52e24 Fix support for site redirects that include section/variant paths (#3024) 2025-03-24 10:21:26 +01:00
Valentino Hudhra 54ee0149e1 Add support for custom fonts (#2981) 2025-03-24 08:20:58 +01:00
Greg Bergé 8fda9d2fab Update Argos (#3025) 2025-03-22 23:00:25 +01:00
Samy Pessé 77a72d79ce Update next.js to latest v14 and v15 (#3026) 2025-03-22 20:02:15 +01:00
Nolann B. 73e2b472ef Fix write only properties in request example (#3023) 2025-03-21 19:09:25 +01:00
Samy Pessé 2f28cc1323 v2: improve link and navigation between content in a same site (#3020) 2025-03-21 18:18:34 +01:00
Greg Bergé f5516d4e21 Improve OpenAPI TOC display (#3022) 2025-03-21 14:29:12 +00:00
Greg Bergé d2facb2db1 Mark OpenAPI properties as optional if not required (#3021) 2025-03-21 14:28:53 +00:00
Greg Bergé 6eae764b7c Add support for multiple body content types / examples (#2991) 2025-03-21 14:23:42 +01:00
Greg Bergé 970ef8f886 Add missing token (#3019) 2025-03-21 11:04:57 +01:00
Nolann B. ed0720699e Fix OpenAPI path overflow on mobile (#3018) 2025-03-21 11:02:32 +01:00
Greg Bergé ad94d00d5f Improve markdown parsing (#3017) 2025-03-21 10:53:01 +01:00
Greg Bergé c7843f7f52 Remove bun cache (#3016) 2025-03-21 10:42:57 +01:00
Greg Bergé 1fe3286bdb Fix OpenAPI overflow issue (#3015) 2025-03-21 10:42:09 +01:00
Samy Pessé ce030fdf76 v2: Fix redirect to strip jwt_token in url-host mode (#3014) 2025-03-21 00:46:39 +01:00
Samy Pessé 9ba8783952 v2: Fix the redirection when stripping the jwt_token from url (#3013) 2025-03-21 00:25:34 +01:00
Greg Bergé 2f3966cc53 Fix icons flaky (#3012) 2025-03-20 21:08:46 +00:00
Samy Pessé 8f65a22e13 Accept API token in query parameters for normal v2 routes (#3011) 2025-03-20 19:54:55 +01:00
Greg Bergé 2787d884cd Stabilize images & icons (#3009) 2025-03-20 19:40:42 +01:00
Nolann B. fc00b51683 Remove default value in generateSchemaExample (#3007) 2025-03-20 17:24:23 +01:00
Samy Pessé f80c3a7b37 v2: Fix display of integration blocks (#3010) 2025-03-20 15:56:31 +00:00
Samy Pessé 4e5d6c7487 v2: use static route for VA / adaptive content (#2998) 2025-03-20 15:41:17 +01:00
Greg Bergé 96f84b5917 Fix visual tests v2 (#3006) 2025-03-20 11:34:04 +01:00
Greg Bergé 6b8a5fdd1b Trigger tests (#3005) 2025-03-20 10:00:09 +00:00
Greg Bergé 89083ac790 Stabilize visual tests (#3001) 2025-03-20 09:22:09 +01:00
Nolann B. 70be2c6b2b Stringify default value (#3003) 2025-03-19 21:28:40 +01:00
Samy Pessé bcf877841b Update to latest version of the GitBook API (#3002) 2025-03-19 20:11:44 +01:00
Nolann B. a84b06bb19 Update resolveDescription and add minItems/maxItems (#3000) 2025-03-19 17:21:35 +01:00
Zeno Kapitein 24b7808d9a Fix prominent search bar width on md screens (#2996) 2025-03-19 17:14:07 +01:00
Greg Bergé 8364ee988e Fix Argos flakies (#2999) 2025-03-19 16:04:45 +01:00
Samy Pessé 07f6e2d233 Pass page.computedSeed to getComputedDocument (#2995) 2025-03-19 15:02:49 +01:00
Greg Bergé 7212973f34 Update Scalar client (#2993) 2025-03-18 16:02:40 +01:00
Greg Bergé af44e0436b fix typo (#2992) 2025-03-18 14:20:23 +01:00
Greg Bergé 3f9c35d505 Fix emoji display on other platforms than Mac OS (#2990) 2025-03-17 17:49:13 +01:00
Zeno Kapitein 2c13f8b95c Fix horizontal overscroll issue on large screens (#2989) 2025-03-17 15:38:44 +01:00
Zeno Kapitein 4dab1c56e5 Fix alignment of prominent search bar on full-width pages (#2987) 2025-03-17 15:02:23 +01:00
Zeno Kapitein 4723f03ef1 Restyle section group dropdown (#2985) 2025-03-17 13:22:54 +01:00
1143 changed files with 106075 additions and 23758 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "🚀 Dev server",
"runtimeExecutable": "sh",
"runtimeArgs": ["-c", "bun install && bun dev"],
"port": 3000,
"autoPort": false
}
]
}
+10
View File
@@ -0,0 +1,10 @@
# Stop editors from looking for .editorconfig files in parent directories.
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
max_line_length = 100
+31 -6
View File
@@ -53,19 +53,44 @@ After forking this repository, you'll want to [create a branch](https://docs.git
#### 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:
You can easily do this by running the command `nvm use`.
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
To start your local version of GitBook, run the command `bun dev`.
##### Setup steps:
1. Ensure you are using the project's version of Node:
```bash
nvm use
```
2. Install dependencies using Bun:
```bash
bun install
```
3. Start the development server:
```bash
bun dev
```
Additional development commands:
- `bun lint`: Lint the code using Oxlint
- `bun lint:fix`: Automatically fix lint issues using Oxlint
- `bun format`: Format the code using Oxfmt
- `bun format:check`: Check formatting without changing files
- `bun typecheck`: Run TypeScript type checking
- `bun unit`: Run unit tests
- `bun e2e`: Run end-to-end tests
#### 4. Preview your changes
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/`.
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
@@ -0,0 +1,83 @@
name: Gradual Deploy to Cloudflare
description: Use gradual deployment to deploy to Cloudflare. This action will upload the middleware and server versions to Cloudflare and kept them bound together
inputs:
apiToken:
description: 'Cloudflare API token'
required: true
accountId:
description: 'Cloudflare account ID'
required: true
environment:
description: 'Cloudflare environment to deploy to (staging, production, preview)'
required: true
middlewareVersionId:
description: 'Middleware version ID to deploy'
required: true
serverVersionId:
description: 'Server version ID to deploy'
required: true
outputs:
deployment-url:
description: 'Deployment URL'
value: ${{ steps.deploy_middleware.outputs.deployment-url }}
runs:
using: 'composite'
steps:
- id: wrangler_status
name: Check wrangler deployment status
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
# This step is used to get the version ID that is currently deployed to Cloudflare.
- id: extract_current_version
name: Extract current version
shell: bash
run: |
version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
echo "version_id=$version_id" >> $GITHUB_OUTPUT
- id: deploy_server
name: Deploy server to Cloudflare at 0%
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
- id: deploy_middleware
name: Deploy middleware to Cloudflare at 100%
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
- name: Deploy server to Cloudflare at 100%
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
- name: Outputs
shell: bash
env:
DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
run: |
echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"
@@ -1,37 +0,0 @@
name: 'Setup Playwright'
description: 'Install Playwright and dependencies'
runs:
using: 'composite'
steps:
# Run npm ci and get Playwright version
- name: 🏗 Prepare Playwright env
shell: bash
run: |
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["gitbook"].dependencies["@playwright/test"].version')
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
# Cache browser binaries, cache key is based on Playwright version and OS
- name: 🧰 Cache Playwright browser binaries
uses: actions/cache@v4
id: playwright-cache
with:
path: '~/.cache/ms-playwright'
key: '${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}'
restore-keys: |
${{ runner.os }}-playwright-
# Install browser binaries & OS dependencies if cache missed
- name: 🏗 Install Playwright browser binaries & OS dependencies
if: steps.playwright-cache.outputs.cache-hit != 'true'
shell: bash
working-directory: packages/gitbook
run: |
bun x playwright install --with-deps chromium
# Install only the OS dependencies if cache hit
- name: 🏗 Install Playwright OS dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
shell: bash
working-directory: packages/gitbook
run: |
bun x playwright install-deps
+115 -42
View File
@@ -1,28 +1,34 @@
name: 'Deploy cloudflare'
description: 'Deploy GitBook to Cloudflare'
inputs:
opItem:
description: '1Password item to load secrets from'
required: true
opServiceAccount:
description: '1Password service account token'
required: true
apiToken:
description: 'Cloudflare API token'
required: true
accountId:
description: 'Cloudflare account ID'
required: true
environment:
description: 'Cloudflare environment to deploy to (staging, production, preview)'
required: true
deploy:
description: 'Deploy as main version for all traffic instead of uploading versions'
required: true
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.deploy.outputs.deployment-url }}
deployment-url:
description: 'Deployment URL'
value: ${{ steps.upload_middleware.outputs.deployment-url }}
runs:
using: 'composite'
steps:
@@ -36,36 +42,103 @@ runs:
- name: Load secret
uses: 1password/load-secrets-action@v2
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ inputs.opItem }}/NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ inputs.opItem }}/NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
GITBOOK_OAUTH_SERVER_URL: ${{ inputs.opItem }}/GITBOOK_OAUTH_SERVER_URL
GITBOOK_SITE_OAUTH_SIGNING_SECRET: ${{ inputs.opItem }}/GITBOOK_SITE_OAUTH_SIGNING_SECRET
GITBOOK_PREVIEW_BASE_URL: ${{ inputs.opItem }}/GITBOOK_PREVIEW_BASE_URL
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
GITBOOK_INTEGRATIONS_CONTENT_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_CONTENT_HOST
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL_2
GITBOOK_IMAGE_RESIZE_SALT: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SALT
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
- name: Build worker
run: bun run turbo build:v2:cloudflare
run: bun run turbo build:cloudflare
env:
GITBOOK_RUNTIME: cloudflare
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
shell: bash
- id: deploy
name: Deploy to Cloudflare
- name: Upload the DO worker
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '3.112.0'
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: ${{ fromJSON(inputs.deploy) == true && 'deploy' || 'versions upload' }} --config ./packages/gitbook-v2/wrangler.toml
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
- id: upload_server
name: Upload server to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
- name: Extract server version worker ID
shell: bash
id: extract_server_version_id
run: |
version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
echo "version_id=$version_id" >> $GITHUB_OUTPUT
- name: Run updateWrangler scripts
shell: bash
run: |
bun run ./packages/gitbook/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
- id: upload_middleware
name: Upload middleware to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
- name: Extract middleware version worker ID
shell: bash
id: extract_middleware_version_id
run: |
version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
echo "version_id=$version_id" >> $GITHUB_OUTPUT
- name: Deploy server and middleware to Cloudflare
if: ${{ inputs.deploy == 'true' }}
uses: ./.github/actions/gradual-deploy-cloudflare
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
opServiceAccount: ${{ inputs.opServiceAccount }}
opItem: ${{ inputs.opItem }}
environment: ${{ inputs.environment }}
serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
deploy: ${{ inputs.deploy }}
- name: Outputs
shell: bash
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
+64 -40
View File
@@ -1,28 +1,31 @@
name: 'Deploy vercel'
description: 'Deploy GitBook to Vercel'
inputs:
vercelOrg:
description: 'Vercel organization'
required: true
vercelProject:
description: 'Vercel project'
required: true
vercelToken:
description: 'Vercel token'
required: true
opItem:
description: '1Password item to load secrets from'
required: true
opServiceAccount:
description: '1Password service account token'
required: true
environment:
description: 'Environment to deploy to'
required: true
vercelOrg:
description: 'Vercel organization'
required: true
vercelProject:
description: 'Vercel project'
required: true
vercelToken:
description: 'Vercel token'
required: true
opItem:
description: '1Password item to load secrets from'
required: true
opServiceAccount:
description: '1Password service account token'
required: true
environment:
description: 'Environment to deploy to'
required: true
headSha:
description: 'Git ref to deploy, used for the deploymentId'
required: false
outputs:
deployment-url:
description: "Deployment URL"
value: ${{ steps.deploy.outputs.deployment-url }}
deployment-url:
description: 'Deployment URL'
value: ${{ steps.deploy.outputs.deployment-url }}
runs:
using: 'composite'
steps:
@@ -37,30 +40,51 @@ runs:
run: bun run vercel pull --yes --environment=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
shell: bash
env:
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
- name: Load secret
uses: 1password/load-secrets-action@v2
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
GITBOOK_OAUTH_SERVER_URL: ${{ inputs.opItem }}/GITBOOK_OAUTH_SERVER_URL
GITBOOK_SITE_OAUTH_SIGNING_SECRET: ${{ inputs.opItem }}/GITBOOK_SITE_OAUTH_SIGNING_SECRET
GITBOOK_PREVIEW_BASE_URL: ${{ inputs.opItem }}/GITBOOK_PREVIEW_BASE_URL
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
GITBOOK_INTEGRATIONS_CONTENT_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_CONTENT_HOST
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL_2
GITBOOK_IMAGE_RESIZE_SALT: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SALT
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
- name: Inject build env vars
if: ${{ inputs.environment == 'preview' }}
shell: bash
run: |
HEAD_SHA=$(git rev-parse HEAD)
echo "resolved HEAD_SHA: $HEAD_SHA"
echo "GITBOOK_HEAD_SHA=$HEAD_SHA" >> .vercel/.env.${{ inputs.environment }}.local
echo "GITBOOK_RUNTIME=vercel" >> .vercel/.env.${{ inputs.environment }}.local
echo "GITBOOK_BLOCK_SEARCH_INDEXATION=true" >> .vercel/.env.${{ inputs.environment }}.local
echo "GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE=true" >> .vercel/.env.${{ inputs.environment }}.local
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
cat .vercel/.env.${{ inputs.environment }}.local
- name: Build Project Artifacts
run: bun run vercel build --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
shell: bash
env:
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
GITBOOK_RUNTIME: vercel
GITBOOK_HEAD_SHA: ${{ inputs.headSha }}
- name: Deploy Project Artifacts to Vercel
id: deploy
shell: bash
@@ -68,8 +92,8 @@ runs:
DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }})
echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"
env:
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
- name: Outputs
shell: bash
run: |
-9
View File
@@ -3,15 +3,6 @@ description: 'Install Bun and cache dependencies'
runs:
using: 'composite'
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
uses: oven-sh/setup-bun@v2
with:
+17 -7
View File
@@ -4,8 +4,7 @@ on:
push:
branches:
- main
env:
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
- changeset-release/main
jobs:
format:
runs-on: ubuntu-latest
@@ -21,6 +20,20 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun format:check
lint:
runs-on: ubuntu-latest
name: Lint
timeout-minutes: 6
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun lint
test:
runs-on: ubuntu-latest
name: Test
@@ -35,13 +48,10 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun unit
build-oss:
# CI to check that the repository builds correctly on a machine without the credentials
build:
runs-on: ubuntu-latest
name: Build (Open Source)
name: Build
timeout-minutes: 6
env:
NPM_TOKEN_READONLY: ''
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -0,0 +1,34 @@
name: CSS browser compatibility
on:
pull_request_target:
types: [opened, reopened, synchronize]
permissions:
contents: read
pull-requests: read
concurrency:
group: css-browser-compatibility-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
check:
name: Check newly added CSS declarations
runs-on: ubuntu-latest
timeout-minutes: 6
steps:
# pull_request_target checks out the trusted base branch by default. Never use the PR ref here.
- name: Checkout trusted checker
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Check CSS browser compatibility
working-directory: packages/gitbook
run: bun run check:css-browser-compatibility
env:
GITHUB_TOKEN: ${{ github.token }}
+211 -156
View File
@@ -1,69 +1,35 @@
name: Preview
on:
pull_request:
pull_request_target:
push:
branches:
- main
env:
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
branches:
- main
jobs:
deploy-v1-cloudflare:
name: Deploy v1 to Cloudflare Pages
approval:
# Ensure that external contributors' PRs are manually approved before deployment
name: Approve Preview Deployment
runs-on: ubuntu-latest
environment:
name: ${{ github.ref == 'refs/heads/main' && '1c-production' || '1c-preview' }}
url: ${{ steps.deploy.outputs.deployment-url }}
permissions:
contents: read
deployments: write
issues: write
pull-requests: write
checks: write
statuses: write
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
deployment-alias-url: ${{ steps.deploy.outputs.deployment-alias-url }}
if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.fork }}
environment:
name: preview-approval
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Sets env vars for production
if: github.ref == 'refs/heads/main'
run: |
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
- name: Build Next.js with next-on-pages
run: bun run turbo gitbook#build:cloudflare
env:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
- id: deploy
name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: ./
wranglerVersion: '3.112.0'
command: pages deploy ./packages/gitbook/.vercel/output/static --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
echo "Alias URL: ${{ steps.deploy.outputs.deployment-alias-url }}"
- name: Approval gate
run: echo "Preview deployment approved."
deploy-v2-vercel:
name: Deploy v2 to Vercel (preview)
runs-on: ubuntu-latest
environment:
name: 2v-preview
url: ${{ steps.deploy.outputs.deployment-url }}
needs: approval
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
environment:
name: 2v-preview
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy to Vercel
id: deploy
uses: ./.github/composite/deploy-vercel
@@ -74,17 +40,22 @@ jobs:
vercelToken: ${{ secrets.VERCEL_TOKEN }}
opItem: op://gitbook-open/2v-preview
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
headSha: ${{ github.event.pull_request.head.sha || github.sha }}
deploy-v2-cloudflare:
name: Deploy v2 to Cloudflare Worker (preview)
runs-on: ubuntu-latest
environment:
name: 2c-preview
url: ${{ steps.deploy.outputs.deployment-url }}
needs: approval
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
environment:
name: 2c-preview
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
deployment-url: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy to Cloudflare
id: deploy
uses: ./.github/composite/deploy-cloudflare
@@ -95,15 +66,24 @@ jobs:
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 }}"
echo "URL: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}"
comment-deployments:
runs-on: ubuntu-latest
name: Comment Deployments (preview)
if: always() && !startsWith(github.ref, 'refs/heads/main')
if: always() && github.event_name != 'push'
needs:
- deploy-v1-cloudflare
- deploy-v2-vercel
- deploy-v2-cloudflare
steps:
@@ -123,15 +103,6 @@ jobs:
body: |
Summary of the deployments:
### Version 1 (production)
| Version | URL | Status |
| --- | --- | --- |
| Latest commit | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} |
| PR | [${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} |
### Version 2 (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' && '✅' || '❌' }} |
@@ -139,107 +110,191 @@ jobs:
### Test content
| Site | v1 | v2 |
| Site | `2v` | `2c` |
| --- | --- | --- |
| GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy-v1-cloudflare.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-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
| 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-v1:
runs-on: ubuntu-latest
name: Visual Testing v1
needs: deploy-v1-cloudflare
timeout-minutes: 8
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
visual-testing-v2-vercel:
runs-on: ubuntu-latest
name: Visual Testing v2
needs: deploy-v2-vercel
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'v2-vercel'
visual-testing-customers-v1:
runs-on: ubuntu-latest
name: Visual Testing Customers v1
needs: deploy-v1-cloudflare
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-v1-cloudflare.outputs.deployment-url }}
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'customers-v1'
visual-testing-customers-v2:
runs-on: ubuntu-latest
name: Visual Testing Customers v2
needs: deploy-v2-vercel
timeout-minutes: 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 }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'customers-v2'
pagespeed-testing-v1:
runs-on: ubuntu-latest
name: PageSpeed Testing v1
needs: deploy-v1-cloudflare
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run pagespeed tests
run: bun ./packages/gitbook/tests/pagespeed-testing.ts
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{needs.deploy-v1-cloudflare.outputs.deployment-url}}
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: v2-vercel
visual-testing-v2-cloudflare:
runs-on: ubuntu-latest
name: Visual Testing v2 (Cloudflare)
needs: deploy-v2-cloudflare
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: v2-cloudflare
visual-testing-customers-v2:
runs-on: ubuntu-latest
name: Visual Testing Customers v2
needs: deploy-v2-vercel
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run Playwright tests
run: bun e2e-customers
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: customers-v2-vercel
# Style recalc depends on the client bundle, not the host, so this runs here only and
# not in the Cloudflare job. Kept out of `e2e-customers`, which is a visual suite.
- name: Run style invalidation tests
if: always()
working-directory: packages/gitbook
run: bun run e2e-style-perf
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
# Runs on failure too: a blown style budget is exactly when the numbers are worth seeing.
- name: Build style invalidation report
if: always() && github.event_name != 'push'
id: style-perf
working-directory: packages/gitbook
run: |
rows=$(cat test-results/style-perf.md 2>/dev/null || true)
if [ -z "$rows" ]; then exit 0; fi
{
echo 'body<<STYLE_PERF_EOF'
echo '### Style invalidation on a large API reference'
echo
echo 'Elements restyled by opening one popup on [the Snyk API reference](https://docs.snyk.io/snyk-api/reference/apps). A share near or above 100% means the insertion restyles the whole document.'
echo
echo '| interaction | restyled | page | share | budget | |'
echo '| --- | ---: | ---: | ---: | ---: | :-: |'
echo "$rows"
echo 'STYLE_PERF_EOF'
} >> "$GITHUB_OUTPUT"
- name: Find style invalidation comment
if: always() && github.event_name != 'push' && steps.style-perf.outputs.body != ''
uses: peter-evans/find-comment@v3
id: fc-style-perf
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: 'Style invalidation on a large API reference'
- name: Create or update style invalidation comment
if: always() && github.event_name != 'push' && steps.style-perf.outputs.body != ''
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc-style-perf.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: ${{ steps.style-perf.outputs.body }}
edit-mode: replace
visual-testing-customers-v2-cloudflare:
runs-on: ubuntu-latest
name: Visual Testing Customers v2 (Cloudflare)
needs: deploy-v2-cloudflare
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run Playwright tests
run: bun e2e-customers
env:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: customers-v2-cloudflare
browserless-testing-v2-vercel:
runs-on: ubuntu-latest
name: Browserless Testing v2 (Vercel)
needs: deploy-v2-vercel
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run tests
run: cd ./packages/gitbook && bun e2e-browserless
env:
BASE_URL: ${{needs.deploy-v2-vercel.outputs.deployment-url}}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
# browserless-testing-v2-cloudflare:
# runs-on: ubuntu-latest
# name: Browserless Testing v2 (Cloudflare)
# needs: deploy-v2-cloudflare
# steps:
# - name: Checkout
# uses: actions/checkout@v4
# - name: Setup Bun
# uses: ./.github/composite/setup-bun
# - name: Install dependencies
# run: bun install --frozen-lockfile
# env:
# PUPPETEER_SKIP_DOWNLOAD: 1
# - name: Run tests
# run: cd ./packages/gitbook && bun e2e-browserless
# env:
# BASE_URL: ${{needs.deploy-v2-cloudflare.outputs.deployment-url}}
# SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
+12 -12
View File
@@ -3,13 +3,11 @@ on:
push:
branches:
- main
env:
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
deploy-v2-vercel:
name: Deploy v2 to Vercel (production)
runs-on: ubuntu-latest
environment:
environment:
name: 2v-production
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
@@ -17,20 +15,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy staging
- 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 }}
environment: production
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
vercelToken: ${{ secrets.VERCEL_TOKEN }}
opItem: op://gitbook-open/2v-production
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
deploy-v2-cloudflare:
name: Deploy v2 to Cloudflare Worker (production)
runs-on: ubuntu-latest
environment:
environment:
name: 2c-production
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
@@ -48,6 +46,8 @@ jobs:
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 }}"
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
+12 -12
View File
@@ -3,13 +3,11 @@ on:
push:
branches:
- main
env:
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
deploy-v2-vercel:
name: Deploy v2 to Vercel (staging)
runs-on: ubuntu-latest
environment:
environment:
name: 2v-staging
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
@@ -17,20 +15,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy staging
- 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 }}
environment: staging
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
vercelToken: ${{ secrets.VERCEL_TOKEN }}
opItem: op://gitbook-open/2v-staging
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
deploy-v2-cloudflare:
name: Deploy v2 to Cloudflare Worker (staging)
runs-on: ubuntu-latest
environment:
environment:
name: 2c-staging
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
@@ -48,6 +46,8 @@ jobs:
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 }}"
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
+20 -25
View File
@@ -11,47 +11,42 @@ jobs:
publish:
name: Publish
runs-on: ubuntu-latest
permissions:
id-token: write
pull-requests: write
contents: write
env:
# The repo is managed by Bun, but publishing uses npm for provenance.
# Keep Corepack from rejecting packageManager: bun@... when npm runs.
COREPACK_ENABLE_PROJECT_SPEC: 0
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Update npm
run: |
corepack enable npm
corepack install -g npm@latest
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Create Release Pull Request or Publish to npm
- name: Create Release Pull Request
id: changesets
uses: changesets/action@v1
with:
publish: npm run release
version: npm run changeset-version
publish: bun run publish-all-packages
version: bun run changeset-version
env:
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
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 }}
+4
View File
@@ -25,3 +25,7 @@ yarn-error.log*
# TypeScript
*.tsbuildinfo
.npmrc
# Bun pack artifacts
packages/*/*.tgz
+1 -1
View File
@@ -1 +1 @@
v20.6
v22.3
+70
View File
@@ -0,0 +1,70 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 100,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf",
"sortImports": {
"customGroups": [
{
"groupName": "workspace",
"elementNamePattern": ["@gitbook/**"]
}
],
"groups": [
["value-builtin", "value-external", "type-builtin", "type-external"],
{ "newlinesBetween": true },
"workspace",
{ "newlinesBetween": true },
[
"value-internal",
"type-internal",
"value-parent",
"type-parent",
"value-sibling",
"type-sibling",
"value-index",
"type-index"
],
"type-import",
"unknown"
]
},
"sortPackageJson": false,
"sortTailwindcss": {
"config": "./packages/gitbook/tailwind.config.ts",
"attributes": ["class", "className", "style"],
"functions": ["clsx", "tw"]
},
"ignorePatterns": [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/public/**",
"**/.next/**",
"**/.open-next/**",
"**/.turbo/**",
"**/.vercel/**",
"**/.cache/**",
"**/.wrangler/**",
"**/*.log",
"**/*.MD",
"**/*.md",
"**/*.mdx",
"**/*.html",
"**/*.css",
"packages/embed/standalone/**",
"packages/openapi-parser/src/fixtures/**",
"packages/emoji-codepoints/index.ts",
"packages/icons/src/data/*.json",
"packages/gitbook/worker-configuration.d.ts"
]
}
+87
View File
@@ -0,0 +1,87 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"categories": {
"correctness": "warn",
"suspicious": "warn",
"perf": "warn"
},
"plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "jsx-a11y", "vitest"],
"env": {
"builtin": true,
"browser": true,
"node": true,
"shared-node-browser": true,
"worker": true
},
"globals": {
"Bun": "readonly",
"GitBookIntegrationEvent": "readonly",
"React": "readonly"
},
"rules": {
"eslint/no-console": [
"warn",
{
"allow": ["assert", "error", "warn"]
}
],
"eslint/no-control-regex": "warn",
"eslint/no-prototype-builtins": "warn",
"eslint/no-cond-assign": "warn",
"eslint/no-undef": "error",
"eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"ignoreRestSiblings": true,
"varsIgnorePattern": "^_"
}
],
"react/button-has-type": "warn",
"react/exhaustive-deps": "warn",
"react/jsx-key": "warn",
"react/jsx-no-useless-fragment": "warn",
"react/no-array-index-key": "warn",
"react/void-dom-elements-no-children": "warn",
"react/rules-of-hooks": "error",
"jsx-a11y/alt-text": "warn",
"jsx-a11y/anchor-is-valid": "warn",
"jsx-a11y/click-events-have-key-events": "warn",
"jsx-a11y/iframe-has-title": "warn",
"jsx-a11y/interactive-supports-focus": "warn",
"jsx-a11y/label-has-associated-control": "warn",
"jsx-a11y/no-noninteractive-tabindex": "warn",
"jsx-a11y/role-has-required-aria-props": "warn",
"jsx-a11y/tabindex-no-positive": "warn",
"typescript/array-type": "error",
"typescript/no-explicit-any": "warn",
"typescript/no-non-null-assertion": "warn",
"typescript/no-confusing-void-expression": "warn",
"typescript/only-throw-error": "error",
"vitest/no-focused-tests": "error",
"no-delete-var": "warn",
"no-debugger": "error",
"no-throw-literal": "error",
"use-isnan": "error",
"valid-typeof": "error"
},
"ignorePatterns": [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/public/**",
"**/.next/**",
"**/.open-next/**",
"**/.turbo/**",
"**/.vercel/**",
"**/.cache/**",
"**/.wrangler/**",
"packages/embed/standalone/**",
"packages/openapi-parser/src/fixtures/**",
"packages/emoji-codepoints/index.ts",
"packages/icons/src/data/*.json",
"packages/gitbook/worker-configuration.d.ts",
"**/*.css"
]
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"recommendations": ["biomejs.biome"]
"recommendations": ["oxc.oxc-vscode"]
}
+6 -4
View File
@@ -8,10 +8,12 @@
["style \\=([^;]*);", "\\`([^\\`]*)\\`"]
],
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
"prettier.enable": false,
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
"source.fixAll.oxc": "always"
},
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
}
}
+82
View File
@@ -0,0 +1,82 @@
# AGENTS Instructions
## Commands
```bash
bun install # Install dependencies
bun dev # Start dev server (all packages)
bun run build # Build all packages
bun run lint # Lint with Oxlint
bun run format # Format with Oxfmt (run after every change)
bun run typecheck # Type-check all packages
bun run unit # Run unit tests
```
## Development
The dev server proxies published GitBook sites locally. After `bun dev`, access any published site at:
```
http://localhost:3000/url/<published-gitbook-url>
```
Examples:
- `http://localhost:3000/url/gitbook.com/docs`
- `http://localhost:3000/url/open-source.gitbook.io/midjourney`
## Architecture
```
packages/
gitbook/ # Main Next.js app
src/
app/ # Next.js App Router (sites/)
components/ # React components
lib/ # Server utilities, data fetching
intl/ # Internationalization (translations/)
openapi-parser/ # OpenAPI 3.0/3.1/Swagger parser
react-openapi/ # OpenAPI rendering components
react-contentkit/ # ContentKit component rendering
embed/ # Embeddable GitBook components
shared/ # Shared utilities
icons/ # Icon assets
fonts/ # Font assets
colors/ # Color tokens
expr/ # GitBook expression evaluator
cache-do/ # Cloudflare DO cache
cache-tags/ # Cache tag utilities
```
## Testing
```bash
bun run unit # Unit tests via bun test (not vitest)
bun run e2e # Playwright e2e tests (requires built app)
```
Run a specific test file:
```bash
cd packages/gitbook && bun test src/lib/cache.test.ts
```
## Changesets
After committing code changes, create a changeset for the affected package:
```md
---
"gitbook": patch
---
Provide a short description of the change.
```
Save as `.changeset/<name>.md`, then commit it separately with message: `changeset`
## Formatting
Linting uses Oxlint and formatting uses Oxfmt. Always run `bun run format` before committing.
## Comments
Comment to explain *why*, not *what* — the code already shows what it does. Keep comments short, ideally a single line. Avoid multi-line block comments that narrate mechanics a reader can follow from the code; they add noise and go stale. Reserve longer comments for genuinely non-obvious rationale: a subtle invariant, or a workaround and the reason it exists.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+15 -32
View File
@@ -1,7 +1,11 @@
<p align="center">
<img src="./assets/gitbook_icon_dark.svg" alt="GitBook" width="48" />
</p>
<h1 align="center">GitBook</h1>
<p align="center">
<a href="https://docs.gitbook.com/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a>
<a href="https://gitbook.com/docs/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a> - <a href="https://github.com/orgs/GitbookIO/discussions/categories/feature-requests">Feature requests</a>
</p>
<p align="center">
@@ -35,10 +39,10 @@ To run a local version of this project, please follow these simple steps.
### Prerequisites
- Node.js (Version: >=20.6)
- Use nvm for easy Node management
- Bun (Version: >=1.2.1)
- We use a text-based lockfile which isn't supported below 1.2.1
- Node.js (Version: >= 22.3)
- Use nvm for easy Node management
- [Bun](https://bun.sh/) (Version: >=1.2.15)
- We use a text-based lockfile which isn't supported below 1.2.15
### Set up
@@ -62,43 +66,22 @@ bun install
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:
- http://localhost:3000/docs.gitbook.com
- http://localhost:3000/open-source.gitbook.io/midjourney
- http://localhost:3000/url/gitbook.com/docs
- http://localhost:3000/url/open-source.gitbook.io/midjourney
Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser.
### Other development commands
- `bun format`: format the code
- `bun lint`: lint the code
### CI and testing
All pull-requests will be tested against both visual and performances testing to prevent regressions.
## Fonts and Icons
GitBook Open uses fontawesome. During development, your local environment will use the free version. However, only the pro version will be accepted by CI. If you see the following error:
```
The GitBook icon is missing. It indicates that the dependencies were installed without the correct font-awesome package. These changes have probably been persisted in the Bun lockfile. Read the README for more information.
```
It means that you've changed the GBO dependencies and bundled in the free version. Only GitBook staff can help with this - if you're not on the GitBook team, please ping us in the PR and we'll help get things moving.
If you are GitBook staff, you'll need our NPM token in your local environment.
```
.env.local
NPM_TOKEN_READONLY=xxx
```
and then reinstall dependencies.
GitBook Open uses fontawesome. For self-hosting and local development, for licensing reasons, only the icons from the free version should be used.
## Contributing
@@ -150,11 +133,11 @@ See `LICENSE` for more information.
</p>
```md
[![GitBook](https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1)](https://gitbook.com/)
[![GitBook](https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1)](https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link)
```
```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
src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"
/>
+3
View File
@@ -0,0 +1,3 @@
<svg width="65" height="65" viewBox="0 0 65 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.3963 34.2195C30.5255 36.0255 32.09 36.9285 33.8082 36.93C35.5265 36.9315 37.0926 36.0313 40.2249 34.2308L60.1913 22.7534C61.0927 22.2353 61.6484 21.2749 61.6484 20.2352C61.6484 19.1955 61.0927 18.2351 60.1913 17.717L40.2177 6.23554C37.0888 4.43695 35.5243 3.53766 33.8078 3.53833C32.0912 3.539 30.5275 4.43951 27.4 6.24053L10.2292 16.1286C10.102 16.2019 10.0383 16.2385 9.97905 16.2732C4.11368 19.7068 0.489862 25.9754 0.441408 32.7717C0.440918 32.8404 0.440918 32.9138 0.440918 33.0607C0.440918 33.2074 0.440918 33.2807 0.441407 33.3494C0.489754 40.138 4.10549 46.4008 9.96041 49.837C10.0196 49.8718 10.0831 49.9085 10.2101 49.9818L20.9658 56.1918C27.2331 59.8104 30.3668 61.6197 33.808 61.6208C37.2493 61.622 40.3842 59.8148 46.6539 56.2005L58.008 49.6551C61.1474 47.8454 62.7171 46.9405 63.579 45.4487C64.4409 43.957 64.4409 42.1451 64.4409 38.5215V31.5212C64.4409 30.5159 63.8965 29.5895 63.0182 29.1004C62.1683 28.627 61.1325 28.6341 60.2891 29.1189L37.0074 42.5019C35.4453 43.3998 34.6643 43.8487 33.8072 43.849C32.9501 43.8493 32.1688 43.4008 30.6062 42.5038L14.8487 33.4586C14.0593 33.0055 13.6647 32.7789 13.3477 32.738C12.625 32.6448 11.9301 33.0497 11.6548 33.7244C11.534 34.0203 11.5365 34.4753 11.5414 35.3855C11.545 36.0555 11.5468 36.3905 11.6094 36.6987C11.7496 37.3887 12.1127 38.0136 12.6428 38.4771C12.8795 38.6842 13.1696 38.8516 13.7499 39.1866L30.5974 48.9103C32.164 49.8145 32.9473 50.2666 33.8075 50.2668C34.6677 50.267 35.4512 49.8154 37.0184 48.912L57.6684 37.0085C58.2037 36.7 58.4713 36.5457 58.672 36.6616C58.8727 36.7776 58.8727 37.0865 58.8727 37.7044V40.8796C58.8727 41.7855 58.8727 42.2385 58.6572 42.6114C58.4417 42.9844 58.0493 43.2106 57.2644 43.663L40.2322 53.4811C37.0966 55.2885 35.5288 56.1923 33.8078 56.1915C32.0869 56.1907 30.5199 55.2855 27.386 53.4752L11.4509 44.2701C11.4003 44.2409 11.375 44.2262 11.3514 44.2125C8.0102 42.26 5.94856 38.6882 5.92922 34.8185C5.92909 34.7911 5.92909 34.7619 5.92909 34.7035V31.7889C5.92909 29.6526 7.06686 27.678 8.9151 26.6067C10.5483 25.66 12.5628 25.6582 14.1977 26.6018L27.3963 34.2195Z" fill="#181C1F"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 622 KiB

After

Width:  |  Height:  |  Size: 491 KiB

-175
View File
@@ -1,175 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": [
"**/node_modules/**/*",
"**/dist/**/*",
"**/build/**/*",
"**/public/**/*",
"**/.next/**/*",
"**/.open-next/**/*",
"**/.turbo/**/*",
"**/.vercel/**/*",
"**/.cache/**/*",
"**/.wrangler/**/*",
"packages/openapi-parser/src/fixtures/**/*",
"packages/emoji-codepoints/index.ts",
"packages/icons/src/data/*.json"
]
},
"formatter": {
"enabled": true,
"useEditorconfig": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 4,
"lineEnding": "lf",
"lineWidth": 100,
"attributePosition": "auto",
"bracketSpacing": true
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"performance": {
"noDelete": "warn"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"complexity": {
"noForEach": "off",
"noUselessFragments": "warn",
"noBannedTypes": "warn"
},
"correctness": {
"noUndeclaredVariables": "error",
"noUnusedVariables": "error",
"useArrayLiterals": "error",
"useHookAtTopLevel": "error",
"noUnusedImports": "error",
"noVoidElementsWithChildren": "warn",
"useJsxKeyInIterable": "warn",
"useExhaustiveDependencies": "warn",
"noUnknownFunction": "warn"
},
"style": {
"noNonNullAssertion": "warn",
"noParameterAssign": "off",
"useThrowOnlyError": "error"
},
"suspicious": {
"noConsole": {
"level": "warn",
"options": {
"allow": ["assert", "error", "warn"]
}
},
"noExplicitAny": "warn",
"noImplicitAnyLet": "warn",
"noConfusingVoidType": "warn",
"noControlCharactersInRegex": "warn",
"noPrototypeBuiltins": "warn",
"noAssignInExpressions": "warn",
"noArrayIndexKey": "warn"
},
"a11y": {
"useSemanticElements": "warn",
"useKeyWithClickEvents": "warn",
"noSvgWithoutTitle": "warn",
"useButtonType": "warn",
"useIframeTitle": "warn",
"useAltText": "warn",
"noPositiveTabindex": "warn",
"useFocusableInteractive": "warn",
"useAriaPropsForRole": "warn",
"useValidAnchor": "warn",
"noLabelWithoutControl": "warn",
"noNoninteractiveTabindex": "warn"
},
"nursery": {
"useSortedClasses": {
"level": "error",
"fix": "safe",
"options": {
"attributes": ["class", "className", "style"],
"functions": ["clsx", "tw"]
}
}
}
}
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSameLine": false,
"quoteStyle": "single",
"attributePosition": "auto",
"bracketSpacing": true
}
},
"overrides": [
{
"include": [
"packages/gitbook/**/*",
"packages/gitbook-v2/**/*",
"packages/react-openapi/**/*",
"packages/react-math/**/*",
"packages/react-contentkit/**/*",
"packages/icons/**/*"
],
"javascript": {
"globals": ["React"]
}
},
{
"include": ["packages/gitbook/**/*"],
"javascript": {
"globals": ["React", "GitBookIntegrationEvent"]
}
},
{
"include": ["*.css"],
"javascript": {
"globals": ["theme"]
}
},
{
"include": ["*.test.ts", "packages/gitbook/tests/**/*"],
"javascript": {
"globals": ["Bun"]
}
},
{
"include": [
"packages/cache-do/**/*",
"packages/gitbook/cf-env.d.ts",
"packages/gitbook/src/cloudflare-entrypoint.ts"
],
"javascript": {
"globals": [
"DurableObjectLocationHint",
"DurableObjectNamespace",
"DurableObjectStub",
"ContinentCode",
"Fetcher",
"ExportedHandler"
]
}
}
]
}
+2198 -2712
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -1,2 +0,0 @@
[install.scopes]
"gitbook" = { token = "$NPM_TOKEN_READONLY", url = "https://registry.npmjs.org" }
+49 -19
View File
@@ -1,42 +1,72 @@
{
"name": "gitbook",
"version": "0.1.0",
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.27.12",
"turbo": "^2.4.4",
"vercel": "^39.3.0"
"engines": {
"node": "^22.3.0"
},
"packageManager": "bun@1.2.4",
"devDependencies": {
"@changesets/cli": "^2.31.1",
"oxfmt": "^0.62.0",
"oxlint": "^1.82.0",
"turbo": "^2.10.12",
"vercel": "50.37.3"
},
"packageManager": "bun@1.3.7",
"overrides": {
"@codemirror/state": "6.4.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"@gitbook/api": "0.99.0"
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"esbuild": "0.27.3",
"axios": "1.8.4"
},
"private": true,
"scripts": {
"dev": "turbo run dev",
"dev:v2": "turbo run dev:v2",
"dev": "turbo run dev --concurrency 20",
"build": "turbo run build",
"build:v2": "turbo run build:v2",
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
"typecheck": "turbo run typecheck",
"format": "biome check --write ./",
"format:check": "biome check --diagnostic-level=error ./",
"lint": "oxlint --quiet",
"lint:fix": "oxlint --fix --quiet",
"format": "oxfmt",
"format:check": "oxfmt --check",
"unit": "turbo run unit",
"e2e": "turbo run e2e",
"e2e-customers": "turbo run e2e-customers",
"changeset": "changeset",
"changeset-version": "changeset version && bun run format",
"release": "turbo run release && changeset publish",
"release:preview": "turbo run release:preview",
"changeset-version": "changeset version && bun run format && bun update",
"publish-all-packages": "turbo run publish-to-npm --continue=dependencies-successful",
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
"clean": "turbo run clean"
},
"workspaces": ["packages/*"],
"workspaces": {
"packages": [
"packages/*"
],
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@base-ui/react": "^1.7.0",
"@gitbook/api": "0.201.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"assert-never": "^1.4.0",
"bidc": "^0.0.2",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"tsdown": "^0.15.6",
"typescript": "^5.5.3",
"usehooks-ts": "^3.1.1",
"vercel": "^50.26.1"
}
},
"patchedDependencies": {
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
"next@16.3.3": "patches/next@16.3.3.patch"
}
}
+58
View File
@@ -0,0 +1,58 @@
# @gitbook/browser-types
## 0.1.6
### Patch Changes
- bf674a4: Add an optional `context` property (string, up to 512 characters) to the `confirmation` of custom AI tools, shown above the confirmation dialog to help the user understand what they are approving or rejecting. The `confirmation` can now also be a function that receives the AI-provided input and returns the confirmation, so the context can be derived dynamically from the arguments the tool is about to run with. Available both to integrations (`GitBookIntegrationTool`) and to embed consumers (`GitBookToolDefinition`).
## 0.1.5
### Patch Changes
- 5f66860: Use isCookiesTrackingDisabled for cookie consent integrations
- 2e495cb: Add Global Privacy Control (GPC) support
## 0.1.4
### Patch Changes
- 7a11861: Add support for custom cookie banner registration
- Updated dependencies [6f1db32]
- @gitbook/icons@0.4.3
## 0.1.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- Updated dependencies [10995e0]
- @gitbook/icons@0.3.4
## 0.1.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
- Updated dependencies [6142d6b]
- @gitbook/icons@0.3.3
## 0.1.1
### Patch Changes
- 295f03d: Republish packages
- Updated dependencies [295f03d]
- @gitbook/icons@0.3.2
## 0.1.0
### Minor Changes
- cbc71a5: First version of the public package for typing script integrations.
### Patch Changes
- 854c448: Custom assistants followup
- Updated dependencies [25e2b40]
- @gitbook/icons@0.3.0
+3
View File
@@ -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.
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@gitbook/browser-types",
"description": "Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"sideEffects": false,
"version": "0.1.6",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:"
},
"devDependencies": {
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
},
"scripts": {
"build": "tsdown",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
},
"files": [
"dist",
"README.md",
"CHANGELOG.md"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
+122
View File
@@ -0,0 +1,122 @@
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
export type GitBookIntegrationEvent = 'load' | 'unload';
export type GitBookIntegrationEventCallback = (...args: any[]) => void;
export type GitBookIntegrationToolConfirmation = {
icon?: IconName;
label: string;
/**
* Supporting context displayed to the user above the confirmation dialog,
* to help them understand what they are approving or rejecting.
* Limited to 512 characters.
*/
context?: string;
};
export type GitBookIntegrationTool = AIToolDefinition & {
/**
* Confirmation action to be displayed to the user before executing the tool.
* Provide a static object, or a function that receives the input provided by
* the AI assistant and returns the confirmation — useful to display dynamic
* context based on the arguments the tool is about to be executed with.
*/
confirmation?:
| GitBookIntegrationToolConfirmation
| ((input: object) => GitBookIntegrationToolConfirmation);
/**
* Callback when the tool is executed.
* The input is provided by the AI assistant following the input schema of the tool.
*/
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
};
export type GitBookAssistant = {
/**
* Name of the assistant displayed in the UI.
*/
label: string;
/**
* Icon of the assistant displayed in the UI.
* Any FontAwesome icon name is supported.
* @example 'sparkle'
*/
icon: string;
/**
* Callback when the assistant is opened.
*/
open: (query?: string) => void;
/**
* Whether to display the triggers for this assistant in the UI.
* @default true
*/
ui?: boolean;
};
export type GitBookCookieBannerHandler = (options: {
onApprove: () => void;
onReject: () => void;
}) => void;
export type GitBookGlobal = {
/**
* Register an event listener.
*/
addEventListener: (
type: GitBookIntegrationEvent,
func: GitBookIntegrationEventCallback
) => void;
/**
* Remove an event listener.
*/
removeEventListener: (
type: GitBookIntegrationEvent,
func: GitBookIntegrationEventCallback
) => void;
/**
* Register a custom tool to be exposed to the AI assistant.
*/
registerTool: (tool: GitBookIntegrationTool) => void;
/**
* Register a custom assistant to be available on the site.
*/
registerAssistant: (assistant: GitBookAssistant) => () => void;
/**
* Register a custom cookie banner handler.
* When registered, the built-in cookie banner will not be displayed.
*/
registerCookieBanner: (handler: GitBookCookieBannerHandler) => void;
/**
* Indicates whether cookies tracking is disabled for the current user.
* Returns `true` when tracking is disabled, `false` when tracking is enabled,
* and `undefined` when the tracking preference is unknown or not yet determined.
*/
isCookiesTrackingDisabled: () => boolean | undefined;
/**
* Indicates whether global privacy control is enabled for the current user.
* Returns `true` when global privacy control is enabled, `false` when it is disabled.
*/
isGlobalPrivacyControlEnabled: () => boolean;
};
declare global {
interface Window {
/**
* Global `window.GitBook` object accessible by integrations.
*/
GitBook?: GitBookGlobal;
}
}
@@ -5,6 +5,7 @@
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
@@ -13,9 +14,12 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"types": ["./.wrangler/types/runtime.d.ts"]
"types": [
"bun-types" // add Bun global
]
},
"include": ["src/**/*.ts"],
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
-3
View File
@@ -1,3 +0,0 @@
.wrangler
worker-configuration.d.ts
dist/
-18
View File
@@ -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
-22
View File
@@ -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.
-35
View File
@@ -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.112.0"
},
"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"]
}
-292
View File
@@ -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;
}
-97
View File
@@ -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
View File
@@ -1 +0,0 @@
export * from './CacheObjectStub';
-9
View File
@@ -1,9 +0,0 @@
import { WorkerEntrypoint } from 'cloudflare:workers';
export * from './CacheObject';
export default class Worker extends WorkerEntrypoint {
fetch() {
return new Response('Hello, world!');
}
}
-27
View File
@@ -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
+30
View File
@@ -1,5 +1,35 @@
# @gitbook/cache-tags
## 0.3.4
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.3.3
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 0.3.2
### Patch Changes
- 295f03d: Republish packages
## 0.3.1
### Patch Changes
- 77397ca: Fix version of @gitbook/api referenced in package.json
## 0.3.0
### Minor Changes
- 116575c: Improve typing of getComputedContentSourceCacheTags to match latest API specification
## 0.2.0
### Minor Changes
+22 -8
View File
@@ -4,22 +4,36 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"version": "0.2.0",
"sideEffects": false,
"version": "0.3.4",
"dependencies": {
"@gitbook/api": "*",
"assert-never": "^1.2.1"
"@gitbook/api": "catalog:",
"assert-never": "catalog:"
},
"devDependencies": {
"typescript": "^5.5.3"
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
},
"scripts": {
"build": "tsc",
"build": "tsdown",
"typecheck": "tsc --noEmit",
"dev": "tsc -w"
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
},
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
"files": [
"dist",
"README.md",
"CHANGELOG.md"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
+41 -25
View File
@@ -1,14 +1,15 @@
import type { ComputedContentSource } from '@gitbook/api';
import assertNever from 'assert-never';
import type { ComputedContentSource } from '@gitbook/api';
/**
* Get a stringified cache tag for a given object.
*/
export function getCacheTag(
spec: /**
* All data related to a user
* @deprecated - in v2, no tag as this is an immutable data
*/
* All data related to a user
* @deprecated - in v2, no tag as this is an immutable data
*/
| {
tag: 'user';
user: string;
@@ -60,7 +61,7 @@ export function getCacheTag(
| {
tag: 'computed-document';
space: string;
integration: string;
sourceType: string;
}
/**
* All data related to the URL of a content
@@ -84,6 +85,14 @@ export function getCacheTag(
organization: string;
openAPISpec: string;
}
/**
* All data related to a translation
*/
| {
tag: 'translation';
organization: string;
translation: string;
}
): string {
switch (spec.tag) {
case 'user':
@@ -99,29 +108,20 @@ export function getCacheTag(
case 'document':
return `space:${spec.space}:document:${spec.document}`;
case 'computed-document':
return `space:${spec.space}:computed-document:${spec.integration}`;
return `space:${spec.space}:computed-document:${spec.sourceType}`;
case 'site':
return `site:${spec.site}`;
case 'integration':
return `integration:${spec.integration}`;
case 'openapi':
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
case 'translation':
return `organization:${spec.organization}:translation:${spec.translation}`;
default:
assertNever(spec);
}
}
/**
* Get the cache tag for a given URL.
*/
export function getCacheTagForURL(url: string | URL) {
const parsedURL = url instanceof URL ? url : new URL(url);
return getCacheTag({
tag: 'url',
hostname: parsedURL.hostname,
});
}
/**
* Get the tags for a computed content source.
*/
@@ -134,6 +134,10 @@ export function getComputedContentSourceCacheTags(
) {
const tags: string[] = [];
if (!('dependencies' in source)) {
return tags;
}
// We add the dependencies as tags, to ensure that the computed content is invalidated
// when the dependencies are updated.
const dependencies = Object.values(source.dependencies ?? {});
@@ -157,9 +161,18 @@ export function getComputedContentSourceCacheTags(
})
);
break;
case 'translation':
tags.push(
getCacheTag({
tag: 'translation',
organization: inContext.organizationId,
translation: dependency.ref.translation,
})
);
break;
default:
// Do not throw for unknown dependency types
// as it might mean we are lacking behind the API version
// as it might mean we are lagging behind the API version
break;
}
});
@@ -169,18 +182,21 @@ export function getComputedContentSourceCacheTags(
getCacheTag({
tag: 'computed-document',
space: inContext.spaceId,
integration: source.integration,
sourceType: source.type,
})
);
}
// We invalidate the computed content when a new version of the integration is deployed.
tags.push(
getCacheTag({
tag: 'integration',
integration: source.integration,
})
);
if (source.type.startsWith('integration:')) {
const integration = source.type.split(':')[1]!;
tags.push(
getCacheTag({
tag: 'integration',
integration,
})
);
}
return tags;
}
+1
View File
@@ -5,6 +5,7 @@
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
+54
View File
@@ -1,5 +1,59 @@
# @gitbook/colors
## 0.4.4
### Patch Changes
- a69a307: A near-white tint color (e.g. a warm `#F5F3EF`) is now taken as the exact page background, mirroring the existing behavior for near-black tints. The tint's exact lightness, hue and chroma are preserved, and the color is anchored to whichever scale step the active theme renders as the background — so it matches exactly on `muted` (which uses the second step) as well as `clean`. This applies only to near-neutral tints that are light enough to read as a background; saturated or merely light-ish colors keep their normal accent scale. The `bold` theme is unaffected: it already uses the tint for the header and stays intentionally two-tone.
## 0.4.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.4.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 0.4.1
### Patch Changes
- 295f03d: Republish packages
## 0.4.0
### Minor Changes
- 17dd382: Add `original` background color step
### Patch Changes
- 193d591: Fix return type for `colorContrast`
## 0.3.3
### Patch Changes
- c3f6b8c: Update chroma ratio per step
- 5e975ab: Fix code highlighting for HTTP
- f7a3470: Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
## 0.3.2
### Patch Changes
- cdffd7c: Desaturate text colors by decreasing chroma for the last steps of the color scale
## 0.3.1
### Patch Changes
- fb90eb0: Reduce chroma of first color scale step
## 0.3.0
### Minor Changes
+21 -6
View File
@@ -4,18 +4,33 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"version": "0.3.0",
"sideEffects": false,
"version": "0.4.4",
"devDependencies": {
"typescript": "^5.5.3"
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
},
"scripts": {
"build": "tsc",
"build": "tsdown",
"typecheck": "tsc --noEmit",
"dev": "tsc -w"
"unit": "bun test",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
},
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
"files": [
"dist",
"README.md",
"CHANGELOG.md"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'bun:test';
import { colorScale } from './transformations';
describe('colorScale exact base', () => {
it('takes a very light tint as the exact background on step 1 (clean/bold)', () => {
const scale = colorScale('#F5F3EF', { baseStep: 1 });
expect(scale[0]).toBe('#F5F3EF');
});
it('anchors a very light tint to step 2 when the theme uses the subtle step (muted)', () => {
const scale = colorScale('#F5F3EF', { baseStep: 2 });
expect(scale[1]).toBe('#F5F3EF');
// Step 1 sits just above the exact base, toward white.
expect(scale[0]).not.toBe('#F5F3EF');
expect(scale[0]).not.toBe('#ffffff');
});
it('takes a darker-than-dark tint as the exact background, preserving hue and chroma', () => {
const scale = colorScale('#0B0F19', { darkMode: true, baseStep: 1 });
expect(scale[0]).toBe('#0B0F19');
});
it('does not trigger for a normal mid-lightness tint', () => {
const scale = colorScale('#787878', { baseStep: 2 });
// The default white background is kept; the tint only colors the scale.
expect(scale[0]).toBe('#ffffff');
expect(scale[1]).not.toBe('#787878');
});
it('does not trigger for a saturated light color, keeping the normal accent ramp', () => {
// Light enough (L≈0.93) to pass the lightness bound, but too chromatic to read as a
// background — emitting it verbatim would leave a vivid step 1 above a near-gray scale.
const scale = colorScale('#FFEB3B', { baseStep: 1 });
expect(scale[0]).toBe('#ffffff');
expect(scale[0]).not.toBe('#FFEB3B');
});
it('respects a custom light background instead of overriding it with the tint', () => {
// The color is darker than the requested background, so it is not the extreme end and the
// supplied base must be preserved rather than overwritten.
const scale = colorScale('#eeeeee', { baseStep: 1, background: '#f8f8f8' });
expect(scale[0]).not.toBe('#eeeeee');
});
it('anchors an exact base even when a neutral mix is supplied (tint === primary)', () => {
// getTintMixColor blends neutral into the tint when it equals the primary color; that must
// not darken a near-white tint out of the exact-base path.
const scale = colorScale('#F5F3EF', {
baseStep: 1,
mix: { color: '#787878', ratio: 0.4 },
});
expect(scale[0]).toBe('#F5F3EF');
});
it('does not trigger for a light accent color below the near-white threshold', () => {
// #D8DEEC (light blue-gray, L≈0.90) is a UI accent, not a background, so it must not anchor.
const scale = colorScale('#D8DEEC', { baseStep: 1 });
expect(scale[0]).toBe('#ffffff');
expect(scale[0]).not.toBe('#D8DEEC');
});
it('never anchors when no baseStep is given (accent scales and the bold theme)', () => {
// A scale that does not define the page background opts out of the exact base entirely.
const scale = colorScale('#F5F3EF', {});
expect(scale[0]).toBe('#ffffff');
expect(scale[0]).not.toBe('#F5F3EF');
});
});
+93 -22
View File
@@ -8,7 +8,7 @@ type RGBColor = [number, number, number];
type OKLABColor = { L: number; A: number; B: number };
type OKLCHColor = { L: number; C: number; H: number };
const D65 = [95.047, 100.0, 108.883]; // Reference white (D65)
const D65 = [95.047, 100.0, 108.883] as const; // Reference white (D65)
export enum ColorCategory {
backgrounds = 'backgrounds',
@@ -19,7 +19,7 @@ export enum ColorCategory {
}
type ColorSubScale = {
[key: string]: number;
[key: string]: number | string;
};
/**
@@ -56,6 +56,8 @@ export const scale: Record<ColorCategory, ColorSubScale> = {
solid: 9,
/** Hovered solid backgrounds */
'solid-hover': 10,
/** Original color */
original: 'original',
},
[ColorCategory.text]: {
/** Very low-contrast text
@@ -80,6 +82,22 @@ export const colorMixMapping = {
dark: [0, 0.03, 0.08, 0.1, 0.13, 0.15, 0.2, 0.25, 0.5, 0.55, 0.75, 1],
};
/**
* Light mode has no equivalent to the dark base bound (nothing is lighter than white), so a tint
* at or above this lightness is treated as an explicit, near-white background (e.g. a warm `#F5F3EF`
* at L≈0.96). Kept high so light UI accent colors (around L≈0.90) aren't mistaken for backgrounds.
*/
const EXACT_BASE_LIGHT_THRESHOLD = 0.95;
/**
* Only a near-neutral tint reads as a background. A saturated color would keep the exact hue at the
* anchored step while the rest of the low scale stays ~gray, so those keep the normal accent ramp.
*/
const EXACT_BASE_NEUTRAL_CHROMA = 0.05;
/** Lightness of the default white light background (≈0.99999, not exactly 1). */
const LIGHT_BASE_L = rgbToOklch(hexToRgbArray(LIGHT_BASE)).L;
/**
* Convert a hex color to an RGB color.
*/
@@ -157,6 +175,14 @@ export type ColorScaleOptions = {
/** Define a custom foreground color to use. If left undefined, the global `light`/`dark` values (in `colors.ts`) will be used. */
foreground?: string;
/**
* The 1-indexed scale step this scale renders as the page background (1 = `tint-base` for
* `clean`, 2 = `tint-subtle` for `muted`). When set, an extreme near-neutral tint is taken as
* the exact background, anchored to this step so it matches exactly. Omit for scales that don't
* define the page background (accents, or the two-tone `bold` theme) — they never anchor.
*/
baseStep?: number;
mix?: {
/** If set to a hex code, this color will be additionally mixed into the generated scale according to `mix.ratio`. */
color: string;
@@ -177,6 +203,7 @@ export function colorScale(
darkMode = false,
background = darkMode ? DARK_BASE : LIGHT_BASE,
foreground = darkMode ? LIGHT_BASE : DARK_BASE,
baseStep,
mix,
}: ColorScaleOptions = {}
) {
@@ -186,41 +213,82 @@ export function colorScale(
const backgroundColor = rgbToOklch(hexToRgbArray(background));
let mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
if (mixColor && mix?.ratio && mix.ratio > 0) {
// If defined, we mix in a (tiny) bit of the mix color with the base color.
// A near-neutral tint at the extreme end of the scale is taken as the exact page background
// rather than tinting pure black/white with it — letting brands set an exact background such as
// a warm `#F5F3EF`. Only scales that define the page background opt in (via `baseStep`). In light
// mode the base is pure white by default, so a near-white tint also qualifies (nothing is lighter
// than white); a custom, lower background is respected instead. Decided on the raw color so a
// neutral mix (below) can't darken a tint out of the exact base.
const isExtremeBase = darkMode
? baseColor.L < backgroundColor.L
: backgroundColor.L >= LIGHT_BASE_L
? baseColor.L > EXACT_BASE_LIGHT_THRESHOLD
: baseColor.L > backgroundColor.L;
const isExactBase =
baseStep !== undefined && isExtremeBase && baseColor.C < EXACT_BASE_NEUTRAL_CHROMA;
const exactBaseIndex = (baseStep ?? 1) - 1;
if (mixColor && mix?.ratio && mix.ratio > 0 && !isExactBase) {
// Mix a little of the mix color into the base — but not when the tint is the exact base,
// where it must stay true to the supplied color (and match `--header-background`).
baseColor.L = mixColor.L * mix.ratio + baseColor.L * (1 - mix.ratio);
baseColor.C = mixColor.C * mix.ratio + baseColor.C * (1 - mix.ratio);
baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H;
}
if (
(darkMode && baseColor.L < backgroundColor.L) ||
(!darkMode && baseColor.L > backgroundColor.L)
) {
// If the supplied color is outside of our lightness bounds, use the supplied color's lightness.
// This is mostly used to allow darker-than-dark backgrounds for brands that specifically want that look.
if (isExactBase) {
const difference = (backgroundColor.L - baseColor.L) / backgroundColor.L;
backgroundColor.L = baseColor.L;
// At the edges of the scale, the subtle lightness changes stop being perceptible. We need to amp up our mapping to still stand out.
const amplifier = 1;
mapping = mapping.map((step, index) =>
index < 9 ? step + step * amplifier * difference : step
);
// Anchor the supplied color to the step the theme renders as the background, solving the
// background lightness so neighbouring steps stay continuous with it.
const baseMix = mapping[exactBaseIndex]!;
backgroundColor.L = (baseColor.L - foregroundColor.L * baseMix) / (1 - baseMix);
}
const result = [];
for (let index = 0; index < mapping.length; index++) {
const targetL =
foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
if (isExactBase && index === exactBaseIndex) {
result.push(hex);
continue;
}
if (index === 8 && !mix && Math.abs(baseColor.L - targetL) < 0.2) {
const step = mapping[index]!;
const targetL = foregroundColor.L * step + backgroundColor.L * (1 - step);
if (
index === 8 &&
!mix &&
(darkMode ? targetL - baseColor.L < 0.2 : baseColor.L - targetL < 0.2)
) {
// Original colour is close enough to target, so let's use the original colour as step 9.
result.push(hex);
continue;
}
const chromaRatio = 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:
// When the tint is the exact base, hold the steps from the base toward the
// accents at its chroma so the background stays tinted; steps lighter than the
// base (e.g. cards in `muted`) keep desaturating toward white.
return isExactBase && index >= exactBaseIndex ? 1 : index * 0.05;
}
})();
const shade = {
L: targetL, // Blend lightness
@@ -277,7 +345,7 @@ export function rgbArrayToHex(rgb: RGBColor): string {
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
const rgb = end.map((channel, index) => {
return Math.round(channel + percentage * (start[index] - channel));
return Math.round(channel + percentage * (start[index]! - channel));
});
return rgbArrayToHex(rgb as RGBColor);
@@ -374,14 +442,14 @@ export function xyzToLab65(xyz: [number, number, number]): {
B: number;
} {
const [x, y, z] = xyz.map((v, i) => {
const scaled = v / D65[i];
const scaled = v / D65[i]!;
return scaled > 0.008856 ? Math.cbrt(scaled) : 7.787 * scaled + 16 / 116;
});
return {
L: 116 * y - 16,
A: 500 * (x - y),
B: 200 * (y - z),
L: 116 * y! - 16,
A: 500 * (x! - y!),
B: 200 * (y! - z!),
};
}
@@ -401,7 +469,10 @@ export function dpsContrast(a: RGBColor, b: RGBColor) {
return contrast < 7.5 ? 0 : contrast;
}
export function colorContrast(background: string, foreground: string[] = [LIGHT_BASE, DARK_BASE]) {
export function colorContrast(
background: string,
foreground: string[] = [LIGHT_BASE, DARK_BASE]
): string {
const bg = hexToRgbArray(background);
const best: { color?: RGBColor; contrast: number } = {
@@ -418,5 +489,5 @@ export function colorContrast(background: string, foreground: string[] = [LIGHT_
}
}
return best.color ? rgbArrayToHex(best.color) : foreground[0];
return best.color ? rgbArrayToHex(best.color) : foreground[0] || LIGHT_BASE;
}
+1
View File
@@ -5,6 +5,7 @@
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
+2
View File
@@ -0,0 +1,2 @@
dist/
standalone/
+112
View File
@@ -0,0 +1,112 @@
# @gitbook/embed
## 0.5.2
### Patch Changes
- 94a496c: Make the Docs Embed widget match the page it is embedded in rather than the visitor's OS: a widget on a light page stays light even when the visitor's system is in dark mode, and the widget's own chrome and the docs inside it always render in the same scheme. Sites published with a single theme impose it on the widget too, since they render in it regardless. The standalone script takes `?theme=light` on its URL, and calling `init` twice now updates the options instead of throwing.
- bf674a4: Add an optional `context` property (string, up to 512 characters) to the `confirmation` of custom AI tools, shown above the confirmation dialog to help the user understand what they are approving or rejecting. The `confirmation` can now also be a function that receives the AI-provided input and returns the confirmation, so the context can be derived dynamically from the arguments the tool is about to run with. Available both to integrations (`GitBookIntegrationTool`) and to embed consumers (`GitBookToolDefinition`).
## 0.5.1
### Patch Changes
- 37a9453: Allow copying in embed
- Updated dependencies [47c8ccb]
- @gitbook/icons@0.5.0
## 0.5.0
### Minor Changes
- 88c38fa: Add Search tab to Docs Embed, refactor search
### Patch Changes
- 8d2a95b: Docs Embed: Better support light/dark mode overrides
## 0.4.0
### Minor Changes
- b3875a1: Support `assistantName` property to override Assistant name
## 0.3.0
### Minor Changes
- 370b731: Docs Embed: Make trademark optional
## 0.2.2
### Patch Changes
- b4b471f: Fix unexpected error thrown when using the embed in standalone mode
## 0.2.1
### Patch Changes
- 96e24a1: Fix signed JWT token usage
- 0aa34cc: Fix Embed options in React
- Updated dependencies [6f1db32]
- @gitbook/icons@0.4.3
## 0.2.0
### Minor Changes
- 1430cee: Improve Docs Embed with separate Assistant and Docs tabs
### Patch Changes
- Updated dependencies [62438f4]
- @gitbook/icons@0.4.0
## 0.1.5
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- Updated dependencies [10995e0]
- @gitbook/icons@0.3.4
## 0.1.4
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
- Updated dependencies [6142d6b]
- @gitbook/icons@0.3.3
## 0.1.3
### Patch Changes
- 295f03d: Republish packages
- Updated dependencies [295f03d]
- @gitbook/icons@0.3.2
## 0.1.2
### Patch Changes
- 7508674: Fix bundling of packages
## 0.1.1
### Patch Changes
- 6f368b5: Fix embed assistant window width on small screens
## 0.1.0
### Minor Changes
- 81a6bd7: Improve API to control the GitBook embed
### Patch Changes
- 8927e8f: Initial version of the embed SDK.
- Updated dependencies [25e2b40]
- @gitbook/icons@0.3.0
+514
View File
@@ -0,0 +1,514 @@
# GitBook Docs Embed (`@gitbook/embed`)
Embed your GitBook docs in your product or website.
The Docs Embed can contain three tabs:
- **Assistant**: The [GitBook Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) - an AI-powered chat interface to help users find answers
- **Search**: A search-focused surface for quickly finding pages and asking scoped questions
- **Docs**: A browser for navigating your documentation site
The embed is set up automatically based on your site's configuration. You can optionally customize and override the configuration with custom actions, tools, suggested questions, [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access), and more. See the [Configuration](#configuration) section for all available options.
# Usage
## As a standalone script from your docs site
All GitBook docs sites include a script to easily add the Docs Embed as a widget on your website.
The script is served at `https://docs.company.com/~gitbook/embed/script.js`.
You can find the embed script from your docs site settings, or you can copy the following and replace `docs.company.com` with your docs site hostname.
```html
<script src="https://docs.company.com/~gitbook/embed/script.js"></script>
```
The script initializes the widget itself, so there is nothing to call. To pin the embed to one color
scheme, put it on the script URL — it has to be known before the widget renders:
```html
<script src="https://docs.company.com/~gitbook/embed/script.js?theme=light"></script>
```
To authenticate the visitor, call `init` with their token. Keep tokens out of the script URL: it is
publicly cacheable and ends up in server logs.
```html
<script src="https://docs.company.com/~gitbook/embed/script.js"></script>
<script>
window.GitBook('init',
{ siteURL: 'https://docs.company.com' },
{ visitor: { token: 'your-jwt-token' } }
);
</script>
```
The standalone script provides a global `GitBook` function. See the [API Reference](#api-reference) section for all available methods.
### Example: Configuring the widget
```javascript
GitBook('configure', {
button: {
label: 'Ask',
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
},
tabs: ['assistant', 'search', 'docs'],
actions: [
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
],
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
assistantName: 'Support Assistant',
suggestions: ['What is GitBook?', 'How do I get started?'],
tools: [/* ... */],
closeButton: true,
});
```
See the [Configuration](#configuration) section for all available options.
## As a package from NPM
Install the package: `npm install @gitbook/embed` and import it in your web application:
```tsx
import { createGitBook } from '@gitbook/embed';
const gitbook = createGitBook({
siteURL: 'https://docs.company.com'
});
// Create an iframe and get its URL
const iframe = document.createElement('iframe');
iframe.src = gitbook.getFrameURL({
colorScheme: 'dark', // Optional: force the embed to render in dark mode
visitor: {
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
unsignedClaims: { // Optional: custom claims for dynamic expressions
userId: '123',
plan: 'premium'
}
}
});
// Create a frame client to communicate with the iframe
const frame = gitbook.createFrame(iframe);
// Use the frame client methods
frame.navigateToPage('/getting-started'); // Navigate to a page in the docs tab
frame.navigateToAssistant(); // Switch to the assistant tab
frame.postUserMessage('How do I get started?');
frame.clearChat();
// Configure the embed (see Configuration section for all options)
frame.configure({
tabs: ['assistant', 'search', 'docs'],
actions: [
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
],
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
assistantName: 'Support Assistant',
suggestions: ['What is GitBook?', 'How do I get started?'],
tools: [/* ... */],
closeButton: true
});
// Listen to events
frame.on('close', () => {
console.log('Frame closed');
});
```
## As React components
After installing the NPM package, you can import prebuilt React components:
```tsx
import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
<GitBookProvider siteURL="https://docs.company.com">
<GitBookFrame
colorScheme="dark"
visitor={{
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
}}
tabs={['assistant', 'search', 'docs']}
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
assistantName="Support Assistant"
suggestions={['What is GitBook?', 'How do I get started?']}
actions={[
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
]}
tools={[/* ... */]}
closeButton
/>
</GitBookProvider>
```
You can also use the `useGitBook` hook to access the client:
```tsx
import { useGitBook } from '@gitbook/embed/react';
function MyComponent() {
const gitbook = useGitBook();
const frameURL = gitbook.getFrameURL({ colorScheme: 'dark', visitor: { token: '...' } });
// ...
}
```
# API Reference
## Method Comparison
| Method | Standalone Script | NPM Package | React Components |
|--------|------------------|-------------|------------------|
| **Initialize** | `GitBook('init', options, frameOptions)` | `createGitBook(options)` | `<GitBookProvider siteURL="...">` |
| **Get frame URL** | ❌ (handled internally) | `client.getFrameURL(options)` | `useGitBook().getFrameURL(options)` |
| **Create frame client** | ❌ (handled internally) | `client.createFrame(iframe)` | `useGitBook().createFrame(iframe)` |
| **Show/Hide widget** | `GitBook('show')` / `GitBook('hide')` | ❌ | ❌ |
| **Open/Close window** | `GitBook('open')` / `GitBook('close')` / `GitBook('toggle')` | ❌ | ❌ |
| **Navigate to page** | `GitBook('navigateToPage', path)` | `frame.navigateToPage(path)` | Via frame client |
| **Navigate to assistant** | `GitBook('navigateToAssistant')` | `frame.navigateToAssistant()` | Via frame client |
| **Post message** | `GitBook('postUserMessage', message)` | `frame.postUserMessage(message)` | Via frame client |
| **Clear chat** | `GitBook('clearChat')` | `frame.clearChat()` | Via frame client |
| **Configure** | `GitBook('configure', settings)` | `frame.configure(settings)` | Props on `<GitBookFrame>` |
| **Event listeners** | ❌ | `frame.on(event, listener)` | Via frame client |
| **Unload** | `GitBook('unload')` | ❌ | ❌ |
## Method Signatures
### Standalone Script
- `GitBook('init', options: { siteURL: string }, frameOptions?: { colorScheme?: 'light' | 'dark', visitor?: {...} })` - Initialize widget
- `GitBook('show')` - Show widget button
- `GitBook('hide')` - Hide widget button
- `GitBook('open')` - Open widget window
- `GitBook('close')` - Close widget window
- `GitBook('toggle')` - Toggle widget window
- `GitBook('navigateToPage', path: string)` - Navigate to page (accepts the page path within the site, an absolute path, or the full published URL; resolves pages in any space/section)
- `GitBook('navigateToAssistant')` - Navigate to assistant tab
- `GitBook('postUserMessage', message: string)` - Post message to chat
- `GitBook('clearChat')` - Clear chat history
- `GitBook('configure', settings: {...})` - Configure widget
- `GitBook('unload')` - Unload widget
### NPM Package
**Client Factory:**
- `createGitBook(options: { siteURL: string })``GitBookClient`
- `client.getFrameURL(options?: { colorScheme?: 'light' | 'dark', visitor?: {...} })``string`
- `client.createFrame(iframe: HTMLIFrameElement)``GitBookFrameClient`
**Frame Client:**
- `frame.navigateToPage(path: string)``void` (accepts the page path within the site, an absolute path, or the full published URL; resolves pages in any space/section)
- `frame.navigateToAssistant()``void`
- `frame.postUserMessage(message: string)``void`
- `frame.clearChat()``void`
- `frame.configure(settings: Partial<GitBookEmbeddableConfiguration>)``void`
- `frame.on(event: string, listener: Function)``() => void` (unsubscribe)
### React Components
**Components:**
- `<GitBookProvider siteURL: string>` - Provider component
- `<GitBookFrame {...props}>` - Frame component (accepts all config options as props)
**Hooks:**
- `useGitBook()``GitBookClient` (must be used within `<GitBookProvider>`)
# Configuration
Configuration options are available across usage methods as follows:
- **Standalone script**: via `GitBook('configure', {...})`
- **NPM package**: via `frame.configure({...})`
- **React components**: via props on `<GitBookFrame>`
### `tabs`
Available in: Standalone script, NPM package, React components
Override which tabs are displayed. Defaults to your site's configuration.
- **Type**: `('assistant' | 'search' | 'docs')[]`
```javascript
tabs: ['assistant', 'search', 'docs']
```
### `closeButton`
Available in: Standalone script, NPM package, React components
Display a close (`x`) button in the embed sidebar.
- **Type**: `boolean`
- **Default**: `false`
```javascript
closeButton: true
```
Behavior:
- When clicked, the embed sends a `close` event to the parent page.
- In the standalone script, this event is handled automatically and the widget closes.
- In custom iframe integrations (NPM package), you must listen for the `close` event and decide how to hide/collapse your UI.
- In React, this works automatically when using the standard widget flow. If you build custom frame wiring, handle `close` the same way as the NPM package.
NPM package example:
```typescript
const frame = gitbook.createFrame(iframe);
frame.configure({
closeButton: true
});
const unsubscribe = frame.on('close', () => {
// Hide your modal/drawer/container
container.classList.add('hidden');
});
```
Notes:
- The close button is rendered in the same sidebar area as tabs/actions.
- If your UI hides that sidebar or doesn't render it, the button will not be visible.
### `trademark`
Available in: Standalone script, NPM package, React components
Display GitBook branding in the embed. Defaults to true.
- **Type**: `boolean`
- **Default**: `true`
```javascript
trademark: true
```
### `actions`
Available in: Standalone script, NPM package, React components
Custom action buttons rendered in the sidebar alongside tabs. Each action button triggers a callback when clicked.
**Note**: This prop was previously named `buttons`. Use `actions` instead, it has the same functionality.
- **Type**: `GitBookEmbeddableActionDefinition[]`
- **Properties**:
- `icon`: `string` - Icon name. Any [FontAwesome icon](https://fontawesome.com/search) is supported. (e.g., `'rocket'`, `'comments'`, `'user-circle'`, ...)
- `label`: `string` - Button label text
- `onClick`: `() => void | Promise<void>` - Callback function when clicked
```javascript
actions: [
{
icon: 'comments',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
},
{
icon: 'rocket',
label: 'Get started',
onClick: () => {
GitBook('navigateToPage', '/getting-started');
}
}
]
```
### `greeting`
Available in: Standalone script, NPM package, React components
Welcome message displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) tab.
- **Type**: `{ title: string, subtitle: string }`
```javascript
greeting: {
title: 'Welcome!',
subtitle: 'How can I help you today?'
}
```
### `assistantName`
Available in: Standalone script, NPM package, React components
Override the assistant name displayed in the chat header and assistant entry points (for example, sidebar tabs and action labels). This name will be limited to 32 characters to prevent text overflow.
- **Type**: `string`
```javascript
assistantName: 'Support Assistant'
```
### `suggestions`
Available in: Standalone script, NPM package, React components
Suggested questions displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) welcome screen.
- **Type**: `string[]`
```javascript
suggestions: [
'What is GitBook?',
'How do I get started?',
'What can you do?'
]
```
### `tools`
Available in: Standalone script, NPM package, React components
Custom AI tools to extend the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant). Tools allow the assistant to execute functions and integrate with your own systems.
**Note**: In addition to custom tools, the Assistant will always have access to any [MCP servers you define](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant#extend-gitbook-assistant-with-mcp-servers) in your site's AI settings.
- **Type**: `GitBookToolDefinition[]`
- **Properties**:
- `name`: `string` - Unique tool identifier
- `description`: `string` - Description of what the tool does (used by the AI to decide when and how to use it).
- `inputSchema`: `object` - JSON schema defining the tool's input parameters
- `execute`: `(input: object) => Promise<{ output: any, summary: string }>` - Async function that executes the tool.
- `output`: The result of the tool execution, provided to the AI to continue working with. Not shown to the user.
- `summary`: The visual summary of the tool execution, shown in the user's chat window.
- `confirmation`: `{ icon?: string, label: string }` (optional) - Confirmation button shown before execution, useful for actions that require the user's express approval.
```javascript
tools: [
{
name: 'get_user_info',
description: 'Get information about the current user',
inputSchema: {
type: 'object',
properties: {
userId: {
type: 'string',
description: 'The user ID to look up'
}
},
required: ['userId']
},
execute: async (input) => {
const user = await fetch(`/api/users/${input.userId}`).then(r => r.json());
return {
output: { name: user.name, plan: user.plan },
summary: `Retrieved info for user ${user.name}`
};
}
},
{
name: 'create_ticket',
description: 'Create a support ticket',
confirmation: {
icon: 'circle-question',
label: 'Create support ticket?'
},
inputSchema: {
type: 'object',
properties: {
subject: { type: 'string' },
description: { type: 'string' }
},
required: ['subject', 'description']
},
execute: async (input) => {
const ticket = await fetch('/api/tickets', {
method: 'POST',
body: JSON.stringify(input)
}).then(r => r.json());
return {
output: { ticketId: ticket.id },
summary: `Created ticket #${ticket.id}`
};
}
}
]
```
### `visitor` (Authenticated Access)
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
[Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access) options passed when creating the frame URL. Used for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) and [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access).
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)`
**NPM package**: Pass to `getFrameURL({ visitor: {...} })`
**React components**: Pass as the `visitor` prop on `<GitBookFrame>`
- **Type**: `{ token?: string, unsignedClaims?: Record<string, unknown> }`
- **Properties**:
- `token`: `string` (optional) - Signed JWT token for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) or [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access)
- `unsignedClaims`: `Record<string, unknown>` (optional) - Unsigned claims that can be used in dynamic expressions via `visitor.claims.unsigned.<claim-name>`
```javascript
visitor: {
token: 'your-jwt-token',
unsignedClaims: {
userId: '123',
plan: 'premium',
role: 'admin'
}
}
```
### `colorScheme`
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
Override the embed's color scheme.
When omitted, the standalone widget follows the page it is embedded in — its `color-scheme`, falling back to the visitor's OS preference only when that page declares support for both. With the NPM package or the React components you own the iframe, so the embed follows the visitor's OS preference unless you pass this.
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
**Standalone script**: `?theme=light` on the script URL
**NPM package**: Pass to `getFrameURL({ colorScheme: 'dark' })`
**React components**: Pass as the `colorScheme` prop on `<GitBookFrame>`
- **Type**: `'light' | 'dark'`
```html
<script src="https://docs.company.com/~gitbook/embed/script.js?theme=light"></script>
```
Sites published with a single theme always render in that theme, so `colorScheme` has no effect on
them — the widget follows the site instead, to keep its chrome and the docs inside it consistent.
### `button`
Available in: Standalone script only
Configure the widget button for the standalone script. This option is not available when using the NPM package or React components, since they can be customized completely.
- **Type**: `{ label: string, icon: 'assistant' | 'sparkle' | 'help' | 'book' }`
- **Properties**:
- `label`: `string` - Button label text
- `icon`: `'assistant' | 'sparkle' | 'help' | 'book'` - Icon displayed on the button. Choose from one of 4 presets.
```javascript
button: {
label: 'Ask',
icon: 'assistant'
}
```
+54
View File
@@ -0,0 +1,54 @@
{
"name": "@gitbook/embed",
"description": "Embeddable components for GitBook",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./react": {
"types": "./dist/react/index.d.ts",
"default": "./dist/react/index.js"
}
},
"sideEffects": false,
"version": "0.5.2",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
"bidc": "catalog:"
},
"peerDependencies": {
"react": "*"
},
"devDependencies": {
"bun-types": "catalog:",
"@types/react": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:",
"react": "catalog:"
},
"scripts": {
"build": "bun run build-lib && bun run build-standalone",
"build-lib": "tsdown",
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
"clean": "rm -rf ./dist",
"unit": "bun test",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
},
"files": [
"dist",
"README.md",
"CHANGELOG.md"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
@@ -0,0 +1,51 @@
import { describe, expect, it } from 'bun:test';
import { createGitBook } from './createGitBook';
describe('createGitBook.getFrameURL', () => {
it('builds the embed URL when the site URL has no trailing slash', () => {
const client = createGitBook({ siteURL: 'https://example.com/docs' });
const url = new URL(client.getFrameURL({}));
expect(url.origin).toBe('https://example.com');
expect(url.pathname).toBe('/docs/~gitbook/embed');
expect(url.searchParams.toString()).toBe('');
});
it('handles a trailing slash and adds visitor parameters', () => {
const client = createGitBook({ siteURL: 'https://example.com/docs/' });
const url = new URL(
client.getFrameURL({
visitor: {
token: 'signed-token',
unsignedClaims: {
role: 'editor',
count: 3,
enabled: false,
},
},
})
);
expect(url.pathname).toBe('/docs/~gitbook/embed');
expect(url.searchParams.get('jwt_token')).toBe('signed-token');
expect(url.searchParams.get('visitor.role')).toBe('editor');
expect(url.searchParams.get('visitor.count')).toBe('3');
expect(url.searchParams.get('visitor.enabled')).toBe('false');
});
it('adds an explicit color scheme override when requested', () => {
const client = createGitBook({ siteURL: 'https://example.com/docs/' });
const url = new URL(
client.getFrameURL({
colorScheme: 'dark',
})
);
expect(url.pathname).toBe('/docs/~gitbook/embed');
expect(url.searchParams.get('theme')).toBe('dark');
});
});
@@ -0,0 +1,73 @@
import { type GitBookFrameClient, createGitBookFrame } from './createGitBookFrame';
export type CreateGitBookOptions = {
/**
* URL of the GitBook site to embed.
*/
siteURL: string;
};
export type GetFrameURLOptions = {
/**
* Override the color scheme used by the embedded docs.
* When omitted, the standalone widget follows the page it is embedded in, and only falls back
* to the visitor's OS preference when that page supports both schemes. Building the iframe
* yourself, the embed follows the visitor's OS preference unless you pass this.
*/
colorScheme?: 'light' | 'dark';
/**
* Authentication to use for the frame.
*/
visitor?: {
/**
* Signed JWT token for Adaptive Content or Visitor Authentication to use.
*/
token?: string;
/**
* Unsigned claims to pass to the frame.
* You can use these claims in dynamic expressions using `visitor.claims.unsigned.<claim-name>`.
*/
unsignedClaims?: Record<string, unknown>;
};
};
export type GitBookClient = {
/**
* Get the URL for a GitBook frame.
*/
getFrameURL: (options: GetFrameURLOptions) => string;
/**
* Create a new GitBook frame.
*/
createFrame: (iframe: HTMLIFrameElement) => GitBookFrameClient;
};
export function createGitBook(options: CreateGitBookOptions) {
const client: GitBookClient = {
getFrameURL: (frameOptions) => {
const url = new URL(options.siteURL);
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed`;
if (frameOptions.colorScheme) {
url.searchParams.set('theme', frameOptions.colorScheme);
}
if (frameOptions.visitor?.token) {
url.searchParams.set('jwt_token', frameOptions.visitor.token);
}
if (frameOptions.visitor?.unsignedClaims) {
Object.entries(frameOptions.visitor.unsignedClaims).forEach(([key, value]) => {
url.searchParams.set(`visitor.${key}`, String(value));
});
}
return url.toString();
},
createFrame: (iframe) => createGitBookFrame(iframe),
};
return client;
}
@@ -0,0 +1,116 @@
import { createChannel } from 'bidc';
import type {
FrameToParentMessage,
GitBookEmbeddableConfiguration,
ParentToFrameMessage,
} from './protocol';
export type GitBookFrameClient = {
/**
* Navigate to a page in the docs tab.
*
* Accepts the page's path within the site (e.g. `getting-started/quickstart`),
* an absolute path (e.g. `/help-center/integrations`), or its full published URL.
* The target page may live in any space/section of the site — including one other
* than the embed is currently showing — and is resolved to the right space.
*/
navigateToPage: (path: string) => void;
/**
* Navigate to the assistant.
*/
navigateToAssistant: () => void;
/**
* Post a message to the chat.
*/
postUserMessage: (message: string) => void;
/**
* Clear the chat.
*/
clearChat: () => void;
/**
* Set the placeholder settings.
*/
configure: (settings: Partial<GitBookEmbeddableConfiguration>) => void;
/**
* Register an event listener.
*/
on: (event: string, listener: (...args: any[]) => void) => () => void;
};
/**
* Create a client to communicate with the GitBook Assistant frame.
*/
export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClient {
if (!iframe.contentWindow) {
throw new Error('Iframe must have a content window');
}
const allowTokens = iframe.allow
.split(';')
.map((token) => token.trim())
.filter(Boolean);
if (!allowTokens.includes('clipboard-write')) {
iframe.allow = [...allowTokens, 'clipboard-write'].join('; ');
}
const channel = createChannel(iframe.contentWindow);
channel.receive((message: FrameToParentMessage) => {
console.log('[gitbook:embed] received message', message);
if (message.type === 'close') {
const listeners = events.get('close') || [];
if (listeners) {
listeners.forEach((listener) => listener());
}
}
});
const sendToFrame = (message: ParentToFrameMessage) => {
console.log('[gitbook:embed] send message', message);
channel.send(message);
};
const events = new Map<string, ((...args: any[]) => void)[]>();
const configuration: GitBookEmbeddableConfiguration = {
tabs: ['assistant', 'search', 'docs'],
actions: [],
greeting: { title: '', subtitle: '' },
suggestions: [],
tools: [],
trademark: true,
};
return {
navigateToPage: (pagePath) => {
sendToFrame({ type: 'navigateToPage', pagePath });
},
navigateToAssistant: () => {
sendToFrame({ type: 'navigateToAssistant' });
},
postUserMessage: (message) => sendToFrame({ type: 'postUserMessage', message }),
configure: (settings) => {
Object.assign(configuration, settings);
sendToFrame({ type: 'configure', settings: configuration });
},
clearChat: () => sendToFrame({ type: 'clearChat' }),
on: (event, listener) => {
const listeners = events.get(event) || [];
listeners.push(listener);
events.set(event, listeners);
return () => {
events.set(
event,
listeners.filter((l) => l !== listener)
);
};
},
};
}
+3
View File
@@ -0,0 +1,3 @@
export * from './createGitBook';
export * from './createGitBookFrame';
export * from './protocol';
+131
View File
@@ -0,0 +1,131 @@
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
/**
* Confirmation action to be displayed to the user before executing a tool.
*/
export type GitBookToolConfirmation = {
icon?: IconName;
label: string;
/**
* Supporting context displayed to the user above the confirmation dialog,
* to help them understand what they are approving or rejecting.
* Limited to 512 characters.
*/
context?: string;
};
/**
* Custom tool definition to be passed to the AI assistant.
*/
export type GitBookToolDefinition = AIToolDefinition & {
/**
* Confirmation action to be displayed to the user before executing the tool.
* Provide a static object, or a function that receives the input provided by
* the AI assistant and returns the confirmation — useful to display dynamic
* context based on the arguments the tool is about to be executed with.
*/
confirmation?: GitBookToolConfirmation | ((input: object) => GitBookToolConfirmation);
/**
* Callback when the tool is executed.
* The input is provided by the AI assistant following the input schema of the tool.
*/
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
};
/**
* Custom button definition to be passed to the embeddable GitBook.
*/
export type GitBookEmbeddableActionDefinition = {
/**
* Icon to be displayed in the button.
*/
icon: IconName;
/**
* Label to be displayed in the button.
*/
label: string;
/**
* Callback when the button is clicked.
*/
onClick: () => void | Promise<void>;
};
/**
* Overall configuration for the layout of the GitBook embed.
*/
export type GitBookEmbeddableConfiguration = {
/** Tabs to display in the embed (if enabled on the site). */
tabs: ('assistant' | 'docs' | 'search')[];
/** Additional buttons to be displayed in the header of the GitBook embed. */
actions: GitBookEmbeddableActionDefinition[];
/**
* Additional buttons to be displayed in the header of the GitBook embed.
* @deprecated Use `actions` instead.
*/
buttons?: GitBookEmbeddableActionDefinition[];
/** Message to be displayed in the welcome page. */
greeting: {
title: string;
subtitle: string;
};
/**
* Override the assistant name displayed in the UI.
* Limited to 32 characters.
*/
assistantName?: string;
/** Suggestions of questions to be displayed in the welcome page. */
suggestions: string[];
/** Tools to be provided to the assistant. */
tools: GitBookToolDefinition[];
/**
* Display GitBook branding in the embed.
*/
trademark?: boolean;
/**
* Display a close button inside the assistant.
*/
closeButton?: boolean;
};
/**
* Messages sent from the parent to the frame.
*/
export type ParentToFrameMessage =
| {
type: 'postUserMessage';
message: string;
}
| {
type: 'clearChat';
}
| {
type: 'configure';
settings: GitBookEmbeddableConfiguration;
}
| {
type: 'navigateToPage';
pagePath: string;
}
| {
type: 'navigateToAssistant';
};
/**
* Messages sent from the frame to the parent.
*/
export type FrameToParentMessage = {
type: 'close';
};
+2
View File
@@ -0,0 +1,2 @@
export * from './client';
export * from './standalone';
+84
View File
@@ -0,0 +1,84 @@
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import type {
GetFrameURLOptions,
GitBookEmbeddableConfiguration,
GitBookFrameClient,
} from '../client';
import { useGitBook } from './GitBookProvider';
export type GitBookFrameProps = {
className?: string;
} & GetFrameURLOptions &
Partial<GitBookEmbeddableConfiguration>;
/**
* Render a frame with the GitBook Assistant in it.
*/
export function GitBookFrame(props: GitBookFrameProps) {
const {
className,
colorScheme,
visitor,
actions = [],
greeting,
suggestions = [],
tools = [],
tabs = ['assistant', 'search', 'docs'],
trademark = true,
closeButton = false,
assistantName,
} = props;
const frameRef = useRef<HTMLIFrameElement>(null);
const gitbook = useGitBook();
const [gitbookFrame, setGitbookFrame] = useState<GitBookFrameClient | null>(null);
const frameURL = useMemo(
() => gitbook.getFrameURL({ visitor, colorScheme }),
[gitbook, visitor, colorScheme]
);
useEffect(() => {
if (frameRef.current) {
setGitbookFrame(gitbook.createFrame(frameRef.current));
}
}, [gitbook]);
useEffect(() => {
gitbookFrame?.configure({
tabs,
actions,
greeting,
suggestions,
tools,
closeButton,
trademark,
assistantName,
});
}, [
gitbookFrame,
actions,
greeting,
suggestions,
tools,
tabs,
closeButton,
trademark,
assistantName,
]);
return (
<iframe
title="GitBook"
ref={frameRef}
src={frameURL}
width="100%"
height="100%"
allow="clipboard-write"
className={className}
/>
);
}
@@ -0,0 +1,37 @@
'use client';
import * as React from 'react';
import { type CreateGitBookOptions, createGitBook } from '../client';
import { GitBookContext } from './context';
/**
* Provider for the GitBook client.
*/
export function GitBookProvider(props: React.PropsWithChildren<CreateGitBookOptions>) {
const { siteURL, children } = props;
const options = React.useMemo(
() => ({
siteURL,
}),
[siteURL]
);
const client = React.useMemo(() => createGitBook(options), [options]);
return <GitBookContext.Provider value={client}>{children}</GitBookContext.Provider>;
}
/**
* Hook to access the GitBook client.
*/
export function useGitBook() {
const context = React.useContext(GitBookContext);
if (!context) {
throw new Error('This component must be used within a <GitBookProvider />');
}
return context;
}
+7
View File
@@ -0,0 +1,7 @@
'use client';
import * as React from 'react';
import type { GitBookClient } from '../client';
export const GitBookContext = React.createContext<GitBookClient | null>(null);
+2
View File
@@ -0,0 +1,2 @@
export * from './GitBookProvider';
export * from './GitBookFrame';
+291
View File
@@ -0,0 +1,291 @@
import './style.css';
import {
type CreateGitBookOptions,
type GetFrameURLOptions,
type GitBookClient,
type GitBookEmbeddableConfiguration,
type GitBookFrameClient,
createGitBook,
} from '../client';
export type GitBook = () => void;
type StandaloneCalls =
// Initialize the widget
| ['init', CreateGitBookOptions, GetFrameURLOptions]
// Unload the widget
| ['unload']
// Show the widget
| ['show']
// Hide the widget
| ['hide']
// Open the window
| ['open']
// Close the window
| ['close']
// Toggle the window
| ['toggle']
// Post a user message
| ['postUserMessage', string]
// Clear the chat
| ['clearChat']
// Configure the embed
| ['configure', Partial<GitBookEmbeddableConfiguration & StandaloneConfiguration>]
// Navigate to a page
| ['navigateToPage', string]
// Navigate to the assistant
| ['navigateToAssistant'];
type StandaloneConfiguration = {
/** Configure the button to open the embed */
button: {
/** Label to be displayed in the button. */
label: string;
/** Icon to be displayed in the button. */
icon: 'assistant' | 'sparkle' | 'help' | 'book';
};
};
export type GitBookStandalone = ((...args: StandaloneCalls) => void) & {
q?: StandaloneCalls[];
};
let widgetIframe: HTMLIFrameElement | undefined;
let _client: GitBookClient | undefined;
let _frame: GitBookFrameClient | undefined;
let frameOptions: GetFrameURLOptions | undefined;
let frameConfigured = false;
let frameConfiguration: GitBookEmbeddableConfiguration & StandaloneConfiguration = {
button: {
label: 'Ask',
icon: 'assistant',
},
actions: [],
greeting: { title: '', subtitle: '' },
suggestions: [],
tools: [],
tabs: ['assistant', 'search', 'docs'],
trademark: true,
};
const widgetButton = document.createElement('button');
widgetButton.id = 'gitbook-widget-button';
widgetButton.addEventListener('click', () => {
GitBook('toggle');
});
widgetButton.innerHTML = `
<span id="gitbook-widget-button-icon" data-icon="${frameConfiguration.button.icon}"></span>
<span id="gitbook-widget-button-label">${frameConfiguration.button.label}</span>
`;
const widgetWindow = document.createElement('div');
widgetWindow.id = 'gitbook-widget-window';
widgetWindow.classList.add('hidden');
document.body.appendChild(widgetButton);
document.body.appendChild(widgetWindow);
/**
* The one scheme everything follows: the widget's chrome, the frame's URL and the docs inside it.
* Either it was configured, or we match the page we are embedded in (RND-12558).
*/
function resolveColorScheme(): 'light' | 'dark' {
const configured = frameOptions?.colorScheme;
// Callers are plain JS, so anything else — a typo, a `system` — falls back to the page rather
// than reaching the CSS and the frame's URL, where the two would disagree.
return configured === 'light' || configured === 'dark' ? configured : hostColorScheme();
}
/**
* The scheme the embedding page renders in, which is not the visitor's OS preference: a page that
* never opted into dark stays light however the OS is set.
*
* Resolving a `light-dark()` is the only way to read it. A page's *used* color scheme isn't exposed
* anywhere — the CSSOM gives computed values, and a `<meta name="color-scheme">` (the common way to
* declare it) never even reaches those.
*/
function hostColorScheme(): 'light' | 'dark' {
const probe = document.createElement('div');
// The first `color` is the fallback where `light-dark()` is unsupported: without it the probe
// would inherit the page's own text colour and a white one would read as dark.
probe.style.cssText =
'display:none;color:rgb(0,0,0);color:light-dark(rgb(0,0,0), rgb(255,255,255))';
document.body.appendChild(probe);
const used = getComputedStyle(probe).color;
probe.remove();
return used === 'rgb(255, 255, 255)' ? 'dark' : 'light';
}
/** Mirror the resolved scheme onto the widget's own chrome, and hand it back for the frame's URL. */
function applyColorScheme(): 'light' | 'dark' {
const colorScheme = resolveColorScheme();
for (const element of [widgetButton, widgetWindow]) {
element.dataset.colorScheme = colorScheme;
}
return colorScheme;
}
function getClient() {
if (!_client) {
throw new Error(
'GitBook client not initialized. Call GitBook("init", { siteURL: "..." }) first.'
);
}
return _client;
}
function getIframe() {
if (!widgetIframe || !_frame) {
const client = getClient();
widgetIframe?.remove();
widgetIframe = document.createElement('iframe');
widgetIframe.id = 'gitbook-widget-iframe';
widgetIframe.allow = 'clipboard-write';
// One read for both, so the docs can't come back in a different scheme than the panel.
widgetIframe.src = client.getFrameURL({ ...frameOptions, colorScheme: applyColorScheme() });
widgetWindow.appendChild(widgetIframe);
_frame = client.createFrame(widgetIframe);
_frame.on('close', () => {
widgetWindow.classList.add('hidden');
widgetButton.classList.remove('open');
});
// A new frame starts from the site's own defaults, so replay whatever the host configured.
if (frameConfigured) {
_frame.configure(frameConfiguration);
}
}
return { iframe: widgetIframe, frame: _frame };
}
const GitBook = (...args: StandaloneCalls) => {
switch (args[0]) {
case 'init': {
// `~gitbook/embed/script.js` already calls `init`, so an integrator following the docs
// ends up calling it a second time. Take the new options instead of throwing: throwing
// here dropped every call queued behind it (RND-12558).
_client = createGitBook(args[1]);
frameOptions = {
// Replace rather than merge: a call that leaves out `visitor` — a logout, another
// site — must not keep the token from the last one.
...args[2],
// Except the scheme, where the first one wins: `script.js` passes the site's own
// theme when it pins one, and that is not the integrator's to override.
colorScheme: frameOptions?.colorScheme ?? args[2]?.colorScheme,
};
const colorScheme = applyColorScheme();
// Rebuild the frame only if the new options change its URL — reloading it on the
// loader's `init` plus the integrator's would throw away a chat for nothing.
const frameURL = _client.getFrameURL({ ...frameOptions, colorScheme });
if (widgetIframe && widgetIframe.src !== frameURL) {
const wasOpen = !widgetWindow.classList.contains('hidden');
widgetIframe.remove();
widgetIframe = undefined;
_frame = undefined;
if (wasOpen) {
getIframe();
}
}
break;
}
case 'unload':
_client = undefined;
_frame = undefined;
widgetIframe?.remove();
widgetIframe = undefined;
frameOptions = undefined;
frameConfigured = false;
applyColorScheme();
widgetWindow.classList.add('hidden');
break;
case 'show':
widgetButton.classList.remove('hidden');
break;
case 'hide':
widgetButton.classList.add('hidden');
break;
case 'open':
widgetWindow.classList.remove('hidden');
widgetButton.classList.add('open');
getIframe();
break;
case 'toggle':
widgetWindow.classList.toggle('hidden');
widgetButton.classList.toggle('open');
getIframe();
break;
case 'close':
widgetWindow.classList.add('hidden');
widgetButton.classList.remove('open');
break;
case 'postUserMessage':
getIframe().frame.postUserMessage(args[1]);
break;
case 'configure': {
const settings = args[1];
// If trademark is disabled, change the (branded) icon to the sparkle icon
if (
settings.trademark === false &&
!settings.button?.icon &&
frameConfiguration.button.icon === 'assistant'
) {
settings.button = {
label: frameConfiguration.button.label,
icon: 'sparkle',
};
}
frameConfiguration = {
...frameConfiguration,
...settings,
};
// Update the button label and icon
if (settings.button?.label) {
const label = widgetButton.querySelector('#gitbook-widget-button-label');
if (label) {
label.textContent = settings.button.label;
}
}
if (settings.button?.icon) {
const icon = widgetButton.querySelector('#gitbook-widget-button-icon');
if (icon) {
icon.setAttribute('data-icon', settings.button.icon);
}
}
frameConfigured = true;
getIframe().frame.configure({
...frameConfiguration,
});
break;
}
case 'clearChat':
getIframe().frame.clearChat();
break;
case 'navigateToPage':
getIframe().frame.navigateToPage(args[1]);
break;
case 'navigateToAssistant':
getIframe().frame.navigateToAssistant();
break;
}
};
// @ts-expect-error - GitBook is not defined in the global scope
const precalls = (window.GitBook as GitBookStandalone | undefined)?.q ?? [];
// @ts-expect-error - GitBook is not defined in the global scope
window.GitBook = GitBook;
// Replay each queued call on its own, so one that throws doesn't drop the rest.
precalls.forEach((call) => {
try {
GitBook(...call);
} catch (error) {
console.error('[gitbook:embed]', error);
}
});
+210
View File
@@ -0,0 +1,210 @@
:root {
--gitbook-widget-top: 1rem;
--gitbook-widget-bottom: 1rem;
--gitbook-widget-right: 1rem;
--gitbook-widget-left: 1rem;
--gitbook-widget-button-height: 46px;
--gitbook-widget-radius: .5rem;
--gitbook-widget-text-size: 1rem;
--gitbook-widget-text-color: #656973;
--gitbook-widget-border-color: #e5e5e5;
--gitbook-widget-background-translucent: rgba(255, 255, 255, 0.9);
--gitbook-widget-background-translucent-hover: rgba(250, 250, 250, 0.9);
--gitbook-widget-background-solid: #FFFFFF;
--gitbook-widget-background-solid-hover: #FBFBFB;
--gitbook-widget-icon-size: 1.25rem;
--gitbook-widget-window-width: 28rem; /* 448px */
--gitbook-widget-window-height: 40rem; /* 640px */
--gitbook-widget-window-spacing: .5rem; /* Spacing between the button and the window */
--gitbook-widget-window-bottom: calc(var(--gitbook-widget-bottom) + var(--gitbook-widget-button-height) + var(--gitbook-widget-window-spacing));
--gitbook-widget-transition-duration-fast: 0.2s;
--gitbook-widget-transition-duration-slow: 0.5s;
--gitbook-widget-easing: cubic-bezier(0.25, 1, 0.5, 1);
--gitbook-widget-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* The widget owns the panel's surface, so it renders in the same scheme as the docs inside the
* iframe. `data-color-scheme` carries that one resolved scheme — see `resolveColorScheme()` — and
* is always set, so the declaration below only shows before the widget initializes (RND-12558).
* The colours are declared here rather than on `:root` because `light-dark()` resolves against the
* color-scheme of the element that declares them. The bundler lowers it to a variable toggle, so
* browsers without `light-dark()` still get both schemes. */
#gitbook-widget-button,
#gitbook-widget-window {
color-scheme: light;
--gitbook-widget-text-color: light-dark(#656973, #FFFFFF);
--gitbook-widget-border-color: light-dark(#e5e5e5, #202020);
--gitbook-widget-background-translucent: light-dark(rgba(255, 255, 255, 0.9), rgba(15, 15, 15, 0.9));
--gitbook-widget-background-translucent-hover: light-dark(rgba(250, 250, 250, 0.9), rgba(20, 20, 20, 0.9));
--gitbook-widget-background-solid: light-dark(#FFFFFF, #0f0f0f);
--gitbook-widget-background-solid-hover: light-dark(#FBFBFB, #141414);
}
#gitbook-widget-button[data-color-scheme="light"],
#gitbook-widget-window[data-color-scheme="light"] {
color-scheme: light;
}
#gitbook-widget-button[data-color-scheme="dark"],
#gitbook-widget-window[data-color-scheme="dark"] {
color-scheme: dark;
}
* {
box-sizing: border-box;
}
/* Button */
#gitbook-widget-button {
position: fixed;
bottom: var(--gitbook-widget-bottom);
right: var(--gitbook-widget-right);
height: var(--gitbook-widget-button-height);
z-index: 9999;
display: flex;
align-items: center;
flex-direction: row-reverse;
gap: .5rem;
padding: .75rem 1rem;
border-radius: 100px;
border: 1px solid var(--gitbook-widget-border-color);
background-color: var(--gitbook-widget-background-translucent);
backdrop-filter: blur(16px);
font-size: var(--gitbook-widget-text-size);
color: var(--gitbook-widget-text-color);
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.05), 0 1px 2px -1px rgba(0,0,0,0.05);
transition: all var(--gitbook-widget-transition-duration-fast) var(--gitbook-widget-easing);
cursor:pointer;
animation: gitbook-widget-present var(--gitbook-widget-transition-duration-slow) var(--gitbook-widget-easing-bounce);
}
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
background-color: var(--gitbook-widget-background-translucent-hover);
}
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
}
#gitbook-widget-button:active {
transform: translateY(0);
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.05), 0 1px 2px -1px rgba(0,0,0,0.05);
}
@media (prefers-contrast: more) {
#gitbook-widget-button {
background-color: var(--gitbook-widget-background-solid);
}
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
background-color: var(--gitbook-widget-background-solid-hover);
}
}
#gitbook-widget-button.hidden {
display: none;
}
#gitbook-widget-button.open {
padding: .75rem;
gap: 0;
}
/* Button: Icon */
#gitbook-widget-button-icon {
width: var(--gitbook-widget-icon-size);
height: var(--gitbook-widget-icon-size);
mask-image: url("https://static-2v.gitbook.com/~gitbook/static/icons/svgs/custom-icons/gitbook-assistant.svg?v=2");
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
background-color: currentColor;
}
#gitbook-widget-button-icon[data-icon="sparkle"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/sparkle.svg?v=2&token=a463935e93");
}
#gitbook-widget-button-icon[data-icon="help"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/circle-question.svg?v=2&token=a463935e93");
}
#gitbook-widget-button-icon[data-icon="book"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/book-open.svg?v=2&token=a463935e93");
}
#gitbook-widget-button.open #gitbook-widget-button-icon {
mask-image: url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/close.svg?v=2&token=a463935e93');
}
/* Button: Label */
#gitbook-widget-button.open #gitbook-widget-button-label {
display: none;
}
/* Window */
#gitbook-widget-window {
position: fixed;
bottom: var(--gitbook-widget-window-bottom);
right: var(--gitbook-widget-right);
z-index: 9998;
width: calc(min(var(--gitbook-widget-window-width), calc(100vw - var(--gitbook-widget-right) - var(--gitbook-widget-left))));
height: calc(min(var(--gitbook-widget-window-height), calc(100vh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
background-color: var(--gitbook-widget-background-translucent);
backdrop-filter: blur(48px);
border: 1px solid var(--gitbook-widget-border-color);
border-radius: var(--gitbook-widget-radius);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
overflow: hidden;
transition-property: transform, opacity, display;
transition-duration: var(--gitbook-widget-transition-duration-slow);
transition-timing-function: var(--gitbook-widget-easing-bounce);
transform-origin: bottom right;
transition-behavior: allow-discrete;
}
@starting-style {
#gitbook-widget-window {
opacity: 0;
transform: scale(0.9);
}
}
body:has(#gitbook-widget-button.hidden) #gitbook-widget-window {
bottom: var(--gitbook-widget-bottom);
}
#gitbook-widget-window.hidden {
transition-property: transform, opacity, display;
transition-duration: var(--gitbook-widget-transition-duration-fast);
transition-timing-function: var(--gitbook-widget-easing);
transition-behavior: allow-discrete;
display: none;
opacity: 0;
transform: scale(0.9);
}
#gitbook-widget-iframe {
width: 100%;
height: 100%;
border: none;
}
@keyframes gitbook-widget-present {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@@ -13,11 +13,12 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"types": [
"bun-types" // add Bun global
]
},
"include": ["src/**/*.ts"],
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: 'src/index.ts',
outDir: 'dist',
},
{
entry: 'src/react/index.ts',
outDir: 'dist/react',
unbundle: true,
},
]);
+8
View File
@@ -0,0 +1,8 @@
{
"extends": ["//"],
"tasks": {
"build": {
"outputs": ["dist/**", "standalone/**"]
}
}
}
+14 -2
View File
@@ -1,13 +1,25 @@
# @gitbook/emoji-codepoints
## 0.2.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 0.2.1
### Patch Changes
- 295f03d: Republish packages
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
- 5f8a8fe: Initial release
+1 -1
View File
@@ -1,6 +1,6 @@
import emojisRaws from 'emoji-assets/emoji.json';
import fs from 'node:fs';
import path from 'node:path';
import emojisRaws from 'emoji-assets/emoji.json';
interface EmojiData {
code_points: {
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "@gitbook/emoji-codepoints",
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
"version": "0.2.0",
"version": "0.2.2",
"private": true,
"exports": "./dist/index.ts",
"dependencies": {},
"sideEffects": false,
"devDependencies": {
"emoji-assets": "^8.0.0"
"emoji-assets": "^9.0.0"
},
"scripts": {
"generate": "bun ./build.ts",
+1
View File
@@ -0,0 +1 @@
dist
+72
View File
@@ -0,0 +1,72 @@
# @gitbook/expr
## 1.3.1
### Patch Changes
- 3fedaaa: Depend on `eval-estree-expression` from the npm registry (`^3.0.1`) instead of a pinned GitHub commit. The published `3.0.1` release is built from the exact commit the package was pinned to, so the code is unchanged — this only removes the fragile git/tarball dependency so consumers install it from npm like any other package.
## 1.3.0
### Minor Changes
- 6eabaec: Implement a getVariables function for ExpressionRuntime
## 1.2.5
### Patch Changes
- 8242f18: Extend gitbook/expr std lib with some additional methods
## 1.2.4
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- 10995e0: Use NPM dependency for eval-estree-expression
## 1.2.3
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 1.2.2
### Patch Changes
- 295f03d: Republish packages
## 1.2.1
### Patch Changes
- a629900: Add dev script for @gitbook/expr
## 1.2.0
### Minor Changes
- 6da3655: Fix exports in gitbook/expr package.json
## 1.1.1
### Patch Changes
- 3548fa6: Fix eval-estree-expr named import
## 1.1.0
### Minor Changes
- e1ff17e: Fix bundling of gitbook/expr package
### Patch Changes
- 8ff1e3b: Add support for every/some array methods
## 1.0.0
### Major Changes
- ada195d: Publish gitbook/expr package to help evaluate user defined expressions.
+3
View File
@@ -0,0 +1,3 @@
# `@gitbook/expr`
Safely evaluate & parse user-defined GitBook expressions.
+51
View File
@@ -0,0 +1,51 @@
{
"name": "@gitbook/expr",
"description": "Safely evaluate & parse user-defined GitBook expressions.",
"version": "1.3.1",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"sideEffects": false,
"dependencies": {
"acorn": "^8.15.0",
"acorn-loose": "^8.5.2",
"acorn-walk": "^8.3.4",
"assert-never": "catalog:",
"escodegen": "^2.1.0",
"eval-estree-expression": "^3.0.1"
},
"devDependencies": {
"@tsconfig/strictest": "catalog:",
"@tsconfig/node20": "catalog:",
"@types/escodegen": "^0.0.10",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
},
"scripts": {
"build": "tsdown",
"typecheck": "tsc --noEmit",
"unit": "bun test",
"clean": "rm -rf ./dist",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
},
"files": [
"dist",
"README.md",
"CHANGELOG.md"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
@@ -0,0 +1,932 @@
import { describe, expect, it } from 'bun:test';
import { ExpressionRuntime } from '../runtime';
import {
SymbolArray,
SymbolBoolean,
SymbolNumber,
SymbolObject,
SymbolString,
SymbolType,
SymbolsTable,
} from '../symbols';
import {
type AutocompleteSuggestions,
type AutocompleteSymbolSuggestion,
SUPPORTED_BINARY_OPERATORS,
SUPPORTED_CONDITIONAL_OPERATORS,
SUPPORTED_LOGICAL_OPERATORS,
} from '../types';
describe('autocomplete', () => {
const runtime = new ExpressionRuntime();
const visitorClaimsHelloArraySymbol = SymbolArray({
name: 'hello',
description: 'An array of string',
items: SymbolString(),
});
const visitorClaimsKeyPropStringSymbol = SymbolString({ name: 'key' });
const symbols = {
visitor: SymbolObject({
name: 'visitor',
properties: {
claims: SymbolObject({
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: visitorClaimsKeyPropStringSymbol,
flags: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
},
methods: [],
}),
hello: visitorClaimsHelloArraySymbol,
role: SymbolString({
name: 'role',
enum: ['admin', 'editor', 'reader'],
}),
},
methods: [],
}),
},
methods: [],
}),
};
const context = new SymbolsTable(symbols);
const SCENARIOS: {
expressionWithCursor: string;
expectedSuggestions: AutocompleteSuggestions;
}[] = [
{
expressionWithCursor: 'visit<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolObject({
name: 'visitor',
properties: {
claims: SymbolObject({
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: SymbolString({ name: 'key' }),
flags: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
},
methods: [],
}),
hello: SymbolArray({
name: 'hello',
description: 'An array of string',
items: SymbolString(),
}),
role: SymbolString({
name: 'role',
enum: ['admin', 'editor', 'reader'],
}),
},
methods: [],
}),
},
methods: [],
}),
ref: 'visitor',
parentRef: undefined,
childrenRefs: ['visitor.claims'],
},
},
],
},
{
expressionWithCursor: 'visitor<cur>',
expectedSuggestions: [],
},
{
expressionWithCursor: 'visitor.<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolObject({
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: SymbolString({ name: 'key' }),
flags: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
},
methods: [],
}),
hello: SymbolArray({
name: 'hello',
description: 'An array of string',
items: SymbolString(),
}),
role: SymbolString({
name: 'role',
enum: ['admin', 'editor', 'reader'],
}),
},
methods: [],
}),
ref: 'visitor.claims',
parentRef: 'visitor',
childrenRefs: [
'visitor.claims.key',
'visitor.claims.flags',
'visitor.claims.hello',
'visitor.claims.role',
],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolString({ name: 'key' }),
ref: 'visitor.claims.key',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.key.length',
'visitor.claims.key.at',
'visitor.claims.key.startsWith',
'visitor.claims.key.endsWith',
'visitor.claims.key.includes',
'visitor.claims.key.toLowerCase',
'visitor.claims.key.toUpperCase',
'visitor.claims.key.trim',
],
},
},
{
type: 'symbol',
symbol: {
definition: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
},
methods: [],
}),
ref: 'visitor.claims.flags',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.flags.FLAG1',
'visitor.claims.flags.FLAG2',
'visitor.claims.flags.FLAG3',
'visitor.claims.flags.FLAG4',
],
},
},
{
type: 'symbol',
symbol: {
definition: SymbolArray({
name: 'hello',
description: 'An array of string',
items: SymbolString(),
}),
ref: 'visitor.claims.hello',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.hello.length',
'visitor.claims.hello.at',
'visitor.claims.hello.includes',
'visitor.claims.hello.some',
'visitor.claims.hello.every',
],
},
},
{
type: 'symbol',
symbol: {
definition: SymbolString({
name: 'role',
enum: ['admin', 'editor', 'reader'],
}),
ref: 'visitor.claims.role',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.role.length',
'visitor.claims.role.at',
'visitor.claims.role.startsWith',
'visitor.claims.role.endsWith',
'visitor.claims.role.includes',
'visitor.claims.role.toLowerCase',
'visitor.claims.role.toUpperCase',
'visitor.claims.role.trim',
],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.ke<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolString({ name: 'key' }),
ref: 'visitor.claims.key',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.key.length',
'visitor.claims.key.at',
'visitor.claims.key.startsWith',
'visitor.claims.key.endsWith',
'visitor.claims.key.includes',
'visitor.claims.key.toLowerCase',
'visitor.claims.key.toUpperCase',
'visitor.claims.key.trim',
],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.key.<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolNumber({
name: 'length',
description:
'The length data property of a String value contains the length of the string in UTF-16 code units.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length',
}),
ref: 'visitor.claims.key.length',
parentRef: 'visitor.claims.key',
childrenRefs: [],
},
},
...visitorClaimsKeyPropStringSymbol.methods.map<AutocompleteSymbolSuggestion>(
(method) => ({
type: 'symbol',
symbol: {
definition: method,
ref: `visitor.claims.key.${method.name}`,
parentRef: 'visitor.claims.key',
childrenRefs: [],
},
})
),
],
},
{
expressionWithCursor: 'visitor.claims.h<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolArray({
name: 'hello',
description: 'An array of string',
items: SymbolString(),
}),
ref: 'visitor.claims.hello',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.hello.length',
'visitor.claims.hello.at',
'visitor.claims.hello.includes',
'visitor.claims.hello.some',
'visitor.claims.hello.every',
],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.hello.<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolNumber({
name: 'length',
description: `The length data property of an Array instance represents the number of elements in that array.
The value is an unsigned, 32-bit integer that is always numerically greater than the highest index in the array.`,
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length',
}),
ref: 'visitor.claims.hello.length',
parentRef: 'visitor.claims.hello',
childrenRefs: [],
},
},
...visitorClaimsHelloArraySymbol.methods.map<AutocompleteSymbolSuggestion>(
(method) => ({
type: 'symbol',
symbol: {
definition: method,
ref: `visitor.claims.hello.${method.name}`,
parentRef: 'visitor.claims.hello',
childrenRefs: [],
},
})
),
],
},
{
expressionWithCursor: 'visitor.claims.f<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
},
methods: [],
}),
ref: 'visitor.claims.flags',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.flags.FLAG1',
'visitor.claims.flags.FLAG2',
'visitor.claims.flags.FLAG3',
'visitor.claims.flags.FLAG4',
],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.fl<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
},
methods: [],
}),
ref: 'visitor.claims.flags',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.flags.FLAG1',
'visitor.claims.flags.FLAG2',
'visitor.claims.flags.FLAG3',
'visitor.claims.flags.FLAG4',
],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolBoolean({ name: 'FLAG1' }),
ref: 'visitor.claims.flags.FLAG1',
parentRef: 'visitor.claims.flags',
childrenRefs: [],
},
},
{
type: 'symbol',
symbol: {
definition: SymbolBoolean({ name: 'FLAG2' }),
ref: 'visitor.claims.flags.FLAG2',
parentRef: 'visitor.claims.flags',
childrenRefs: [],
},
},
{
type: 'symbol',
symbol: {
definition: SymbolBoolean({ name: 'FLAG3' }),
ref: 'visitor.claims.flags.FLAG3',
parentRef: 'visitor.claims.flags',
childrenRefs: [],
},
},
{
type: 'symbol',
symbol: {
definition: SymbolBoolean({ name: 'FLAG4' }),
ref: 'visitor.claims.flags.FLAG4',
parentRef: 'visitor.claims.flags',
childrenRefs: [],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FL<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolBoolean({ name: 'FLAG1' }),
ref: 'visitor.claims.flags.FLAG1',
parentRef: 'visitor.claims.flags',
childrenRefs: [],
},
},
{
type: 'symbol',
symbol: {
definition: SymbolBoolean({ name: 'FLAG2' }),
ref: 'visitor.claims.flags.FLAG2',
parentRef: 'visitor.claims.flags',
childrenRefs: [],
},
},
{
type: 'symbol',
symbol: {
definition: SymbolBoolean({ name: 'FLAG3' }),
ref: 'visitor.claims.flags.FLAG3',
parentRef: 'visitor.claims.flags',
childrenRefs: [],
},
},
{
type: 'symbol',
symbol: {
definition: SymbolBoolean({ name: 'FLAG4' }),
ref: 'visitor.claims.flags.FLAG4',
parentRef: 'visitor.claims.flags',
childrenRefs: [],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1<cur>',
expectedSuggestions: [],
},
{
expressionWithCursor: 'visitor.claims.key <cur>',
expectedSuggestions: [...SUPPORTED_BINARY_OPERATORS].map((op) => ({
type: 'operator',
...op,
})),
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 <cur>',
expectedSuggestions: [
...[...SUPPORTED_BINARY_OPERATORS, ...SUPPORTED_LOGICAL_OPERATORS].filter((op) =>
['==', '!=', '===', '!==', '&&', '||'].includes(op.operator)
),
...SUPPORTED_CONDITIONAL_OPERATORS,
].map((op) => ({
type: 'operator',
...op,
})),
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 =<cur>',
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter((op) =>
['==', '==='].includes(op.operator)
).map((op) => ({
type: 'operator',
...op,
})),
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 ==<cur>',
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter(
(op) => op.operator === '==='
).map((op) => ({
type: 'operator',
...op,
})),
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 !<cur>',
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter((op) =>
['!=', '!=='].includes(op.operator)
).map((op) => ({
type: 'operator',
...op,
})),
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 !=<cur>',
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter(
(op) => op.operator === '!=='
).map((op) => ({
type: 'operator',
...op,
})),
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 == <cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
},
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 == t<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 == tr<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 == true<cur>',
expectedSuggestions: [],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 == f<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 == fa<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 == non<cur>',
expectedSuggestions: [],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 == false<cur>',
expectedSuggestions: [],
},
{
expressionWithCursor: 'visitor.claims.role == <cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
},
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
},
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.String, data: 'reader' },
},
],
},
{
expressionWithCursor: 'visitor.claims.role == ad<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
},
],
},
{
expressionWithCursor: 'visitor.claims.role == admin<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
},
],
},
{
expressionWithCursor: 'visitor.claims.role == "ad<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
},
],
},
{
expressionWithCursor: 'visitor.claims.role == "admin<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
},
],
},
{
expressionWithCursor: 'visitor.claims.role == "admin"<cur>',
expectedSuggestions: [],
},
{
expressionWithCursor: 'visitor.claims.role == edit<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
},
],
},
{
expressionWithCursor: 'visitor.claims.role == "edit<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
},
],
},
{
expressionWithCursor: 'visitor.claims.role == editor<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
},
],
},
{
expressionWithCursor: 'visitor.claims.role == "editor"<cur>',
expectedSuggestions: [],
},
{
expressionWithCursor: 'visitor.claims.hello == <cur>',
expectedSuggestions: [],
},
{
expressionWithCursor: 'visitor.claims.hello[1] == <cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: {
kind: 'in-array',
srcSymbol: visitorClaimsHelloArraySymbol,
matchedLiteralString: '',
},
},
],
},
{
expressionWithCursor: 'visitor.claims.hello[1] == "test<cur>',
expectedSuggestions: [
{
type: 'literal-value',
value: {
kind: 'in-array',
srcSymbol: visitorClaimsHelloArraySymbol,
matchedLiteralString: '"test',
},
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 == true <cur>',
expectedSuggestions: [
...SUPPORTED_LOGICAL_OPERATORS.filter((op) => ['&&', '||'].includes(op.operator)),
...SUPPORTED_CONDITIONAL_OPERATORS,
].map((op) => ({
type: 'operator',
...op,
})),
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 !== true && v<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolObject({
name: 'visitor',
properties: {
claims: SymbolObject({
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: SymbolString({ name: 'key' }),
flags: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
},
methods: [],
}),
hello: SymbolArray({
name: 'hello',
description: 'An array of string',
items: SymbolString(),
}),
role: SymbolString({
name: 'role',
enum: ['admin', 'editor', 'reader'],
}),
},
methods: [],
}),
},
methods: [],
}),
ref: 'visitor',
parentRef: undefined,
childrenRefs: ['visitor.claims'],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 ? <cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolObject({
name: 'visitor',
properties: {
claims: SymbolObject({
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: SymbolString({ name: 'key' }),
flags: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
},
methods: [],
}),
hello: SymbolArray({
name: 'hello',
description: 'An array of string',
items: SymbolString(),
}),
role: SymbolString({
name: 'role',
enum: ['admin', 'editor', 'reader'],
}),
},
methods: [],
}),
},
methods: [],
}),
ref: 'visitor',
parentRef: undefined,
childrenRefs: ['visitor.claims'],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 ? visit<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolObject({
name: 'visitor',
properties: {
claims: SymbolObject({
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: SymbolString({ name: 'key' }),
flags: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
},
methods: [],
}),
hello: SymbolArray({
name: 'hello',
description: 'An array of string',
items: SymbolString(),
}),
role: SymbolString({
name: 'role',
enum: ['admin', 'editor', 'reader'],
}),
},
methods: [],
}),
},
methods: [],
}),
ref: 'visitor',
parentRef: undefined,
childrenRefs: ['visitor.claims'],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.flags.FLAG1 ? visitor.claims.fl<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
},
methods: [],
}),
ref: 'visitor.claims.flags',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.flags.FLAG1',
'visitor.claims.flags.FLAG2',
'visitor.claims.flags.FLAG3',
'visitor.claims.flags.FLAG4',
],
},
},
],
},
];
it.each(SCENARIOS)(
'should provide matching suggestion for expression with cursor: $expressionWithCursor',
({ expressionWithCursor, expectedSuggestions }) => {
const { expression, cursorOffset } = extractCursorPosition(
expressionWithCursor,
'<cur>'
);
const { suggestions } = runtime.autocomplete(expression, cursorOffset, context);
expect(suggestions).toStrictEqual(expectedSuggestions);
}
);
});
function extractCursorPosition(
expressionWithCursor: string,
cursorPlaceholder: string
): { expression: string; cursorOffset: number } {
const cursorOffset = expressionWithCursor.indexOf(cursorPlaceholder);
if (cursorOffset === -1) {
throw new Error(
`Cursor position (${cursorPlaceholder}) not found in the expression string.`
);
}
const expression = expressionWithCursor.replace(cursorPlaceholder, '');
return { expression, cursorOffset };
}
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'bun:test';
import { inferDefaultInputValuesFromObjectJSONSchema } from '../input-values';
describe('inferDefaultInputValuesFromObjectJSONSchema', () => {
it('should infer properly the default input value based on the JSON schema of an object', () => {
const defaultInputValues = inferDefaultInputValuesFromObjectJSONSchema({
type: 'object',
properties: {
claims: {
type: 'object',
properties: {
key: {
type: 'string',
},
flags: {
type: 'object',
properties: {
FLAG1: { type: 'string' },
FLAG2: { type: 'string' },
FLAG3: { type: 'string' },
},
},
isAlphaUser: {
type: 'boolean',
},
hello: {
type: 'string',
enum: ['enumValue1', 'enumValue2', 'enumValue3'],
},
},
},
},
});
expect(defaultInputValues).toMatchObject({
claims: {
key: 'default',
flags: {
FLAG1: 'default',
FLAG2: 'default',
FLAG3: 'default',
},
isAlphaUser: true,
hello: 'enumValue1',
},
});
});
});
+263
View File
@@ -0,0 +1,263 @@
import { describe, expect, it } from 'bun:test';
import { ExpressionError } from '../errors';
import { ExpressionRuntime } from '../runtime';
import type { Logger } from '../types';
const SILENT_LOGGER: Logger = {
debug: () => {},
info: () => {},
error: () => {},
};
describe('ExpressionRuntime', () => {
const runtime = new ExpressionRuntime(SILENT_LOGGER);
describe('evaluate', () => {
it.each([
{
scenario: 'simple condition',
condition: 'isBetaUser === true',
inputs: { isBetaUser: false },
expectedResult: false,
},
{
scenario: 'simple condition with multiple inputs variables',
condition: 'useProductA && !isBetaUser',
inputs: {
useProductA: true,
isBetaUser: false,
},
expectedResult: true,
},
{
scenario: 'condition with objects in inputs variables',
condition: 'products.includes("productA") && userSegments.alpha',
inputs: {
products: ['productA', 'productB'],
userSegments: {
alpha: true,
beta: false,
},
},
expectedResult: true,
},
{
scenario: 'array method',
condition: 'reviews.every(review => !!review.status)',
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
expectedResult: true,
},
{
scenario: 'array every',
condition: 'reviews.every(review => review.status === "approved")',
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
expectedResult: true,
},
{
scenario: 'array includes',
condition: 'reviews.includes("approved")',
inputs: { reviews: ['pending', 'approved'] },
expectedResult: true,
},
{
scenario: 'array map',
condition: '[1, 2, 3].map(n => n * x)',
inputs: { x: 2 },
expectedResult: [2, 4, 6],
},
{
scenario: 'string startsWith',
condition: 'user.role.startsWith("ad")',
inputs: { user: { role: 'admin' } },
expectedResult: true,
},
{
scenario: 'string endsWith',
condition: 'user.role.endsWith("min")',
inputs: { user: { role: 'admin' } },
expectedResult: true,
},
{
scenario: 'string includes',
condition: 'user.role.includes("dm")',
inputs: { user: { role: 'admin' } },
expectedResult: true,
},
{
scenario: 'string toLowerCase',
condition: 'user.role.toLowerCase() === "admin"',
inputs: { user: { role: 'ADMIN' } },
expectedResult: true,
},
{
scenario: 'string toUpperCase',
condition: 'user.role.toUpperCase() === "ADMIN"',
inputs: { user: { role: 'admin' } },
expectedResult: true,
},
{
scenario: 'string trim',
condition: 'user.role.trim() === "admin"',
inputs: { user: { role: ' admin ' } },
expectedResult: true,
},
])(
'should properly evaluate/safeEvaluate a valid conditional expression: $scenario',
({ condition, inputs, expectedResult }) => {
expect(runtime.evaluate(condition, inputs)).toEqual(expectedResult);
expect(runtime.safeEvaluate(condition, inputs).value).toEqual(expectedResult);
}
);
const INVALID_EXPRESSSIONS = [
{
scenario: 'invalid syntax',
condition: 't}=d',
inputs: {},
},
{
scenario: 'non conditional expression',
condition: 'const a = 1;',
inputs: {},
},
{
scenario: 'unsafe expression',
condition: 'while (1) {}',
inputs: {},
},
{
scenario: 'unsafe expression',
condition: '[1, 2, 3].map(() => { while (1) {}})',
inputs: {},
},
];
it.each(INVALID_EXPRESSSIONS)(
'should return an object with the error for non conditional expression or syntax errors when using safeEvaluate is on (default): $scenario',
({ condition, inputs }) => {
const result = runtime.safeEvaluate(condition, inputs);
expect(result.value).toBeUndefined();
expect(result.error instanceof ExpressionError).toBe(true);
}
);
it.each(INVALID_EXPRESSSIONS)(
'should throw an error when using evaluate with invalid expressions',
({ condition, inputs }) => {
expect(() => runtime.evaluate(condition, inputs)).toThrowError(ExpressionError);
}
);
});
describe('parse', () => {
it('should produce a valid ESTree compatible AST node for conditional expressions', () => {
const ast = runtime.parse('isBetaUser === true');
expect(ast.result).toEqual({
type: 'BinaryExpression',
start: 0,
end: 19,
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 19 } },
left: {
type: 'Identifier',
start: 0,
end: 10,
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
name: 'isBetaUser',
},
operator: '===',
right: {
type: 'Literal',
start: 15,
end: 19,
loc: { start: { line: 1, column: 15 }, end: { line: 1, column: 19 } },
value: true,
raw: 'true',
},
});
});
it.each([
{
scenario: 'invalid syntax',
condition: 't}=d',
},
{
scenario: 'non conditional expression',
condition: 'const a = 1;',
},
])(
'should throw an error for non conditional expressions or syntax errors: $scenario',
({ condition }) => {
expect(() => runtime.parse(condition)).toThrowError(ExpressionError);
}
);
});
describe('getVariables', () => {
it.each([
{
scenario: 'single variable',
condition: 'isBetaUser === true',
expectedVariables: ['isBetaUser'],
},
{
scenario: 'multiple variables',
condition: 'useProductA && !isBetaUser',
expectedVariables: ['useProductA', 'isBetaUser'],
},
{
scenario: 'member expression',
condition: 'user.role === "admin"',
expectedVariables: ['user.role'],
},
{
scenario: 'nested member expression with method call',
condition: 'products.includes("productA") && userSegments.alpha',
expectedVariables: ['products.includes', 'userSegments.alpha'],
},
])(
'should return variables used in expression: $scenario',
({ condition, expectedVariables }) => {
expect(runtime.getVariables(condition)).toEqual(expectedVariables);
}
);
it.each([
{
scenario: 'invalid syntax',
condition: 't}=d',
},
{
scenario: 'non conditional expression',
condition: 'const a = 1;',
},
])('should return an empty array for invalid expressions: $scenario', ({ condition }) => {
expect(runtime.getVariables(condition)).toEqual([]);
});
});
describe.skip('generate', () => {
it.each([
{
scenario: 'simple condition',
condition: 'isBetaUser === true',
},
{
scenario: 'simple condition with multiple inputs variables',
condition: 'useProductA && !isBetaUser',
},
{
scenario: 'condition with objects in inputs variables',
condition: 'products.includes("productA") && userSegments.alpha',
},
])(
'should produce the original expression using an AST node produced by parse: $scenario',
({ condition }) => {
const { result } = runtime.parse(condition);
expect(runtime.generate(result)).toStrictEqual(condition);
}
);
});
});
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'bun:test';
import { ExpressionRuntime, parseTemplate } from '../';
describe('template expressions', () => {
it('should parse template into parts', () => {
const parts = parseTemplate('Hello {{ user.name }}!');
expect(parts).toEqual([
{ type: 'text', value: 'Hello ', start: 0, end: 6 },
{ type: 'expression', value: 'user.name', start: 8, end: 19 },
{ type: 'text', value: '!', start: 21, end: 22 },
]);
});
it('should parse template starting with an expression', () => {
const parts = parseTemplate('{{ user.name }} is cool');
expect(parts).toEqual([
{ type: 'expression', value: 'user.name', start: 2, end: 13 },
{ type: 'text', value: ' is cool', start: 15, end: 23 },
]);
});
it('should parse template without expressions', () => {
const parts = parseTemplate('Hello world');
expect(parts).toEqual([{ type: 'text', value: 'Hello world', start: 0, end: 11 }]);
});
it('should evaluate template', () => {
const runtime = new ExpressionRuntime();
const result = runtime.evaluateTemplate('Hello {{ user.name }}!', {
user: { name: 'John' },
});
expect(result).toBe('Hello John!');
});
});
+567
View File
@@ -0,0 +1,567 @@
import type {
Node as AcornNode,
AnyNode,
BinaryExpression,
Expression,
Identifier,
Literal,
MemberExpression,
PrivateIdentifier,
Super,
} from 'acorn';
import { isDummy } from 'acorn-loose';
import * as walk from 'acorn-walk';
import assertNever from 'assert-never';
import { type ExtractSymbolDef, SymbolType, SymbolsTable } from './symbols';
import {
type AutocompleteLiteralValueSuggestion,
type AutocompleteOperatorSuggestion,
type AutocompleteSuggestions,
type AutocompleteSymbolSuggestion,
type DirectLiteralValueSuggestion,
type ExpressionParserResult,
type Logger,
SUPPORTED_BINARY_OPERATORS,
SUPPORTED_CONDITIONAL_OPERATORS,
SUPPORTED_LOGICAL_OPERATORS,
} from './types';
interface ExpressionParser {
parse(expr: string, options: { loose?: boolean }): ExpressionParserResult;
}
export class AutoComplete {
#parser: ExpressionParser;
#logger: Logger;
constructor(parser: ExpressionParser, logger: Logger = console) {
this.#parser = parser;
this.#logger = logger;
}
/**
* Generates autocomplete suggestions based on the input expression and cursor offset position.
*/
public getSuggestions(
expr: string,
cursorOffset: number,
context: SymbolsTable
): AutocompleteSuggestions {
if (!expr.length) {
return [];
}
try {
const { result, invalidNodes } = this.#parser.parse(expr, { loose: true });
// Locate the node at the cursor position.
const nodeAtCursorFound = walk.findNodeAround(result, cursorOffset, (_type, node) =>
isNodeAtCursor(node, cursorOffset)
);
if (!nodeAtCursorFound) {
// When we can't find one we might be in the boundary of the program/expression.
// We could possibly be in a whitespace at the end of the expression or in a situation where the parsed
// tree may contain 2 top level ExpressionStatement (second one returned as invalid node by the parser).
//
// In this case we want to provide operators as suggestions and refine the search of the "cursor" node to either:
// - using the end position of the whole expression AST (e.g white space at the very end of the expression string)
// - or using the second top level ExpressionStatement node found by the parser as the cursor is at the end of that node.
if (cursorOffset > result.end) {
const ast = invalidNodes.length > 0 ? invalidNodes[0]?.expression : result;
if (!ast) {
return [];
}
const lastNodeFound = walk.findNodeAround(ast, ast.end, (_type, node) =>
isNodeAtCursor(node, ast.end)
);
if (!lastNodeFound) {
return [];
}
const { node } = lastNodeFound;
if (!isAnyNode(node)) {
throw Error(`Unexpected node type ${node.type}`);
}
return this.getOperatorSuggestionsForNode(ast, node, result.end, context);
}
return [];
}
const { node } = nodeAtCursorFound;
if (!isAnyNode(node)) {
throw Error(`Unexpected node type ${node.type}`);
}
return this.getSuggestionsForNode(result, node, expr, cursorOffset, context);
} catch (error) {
this.#logger.error('Error while computing autocomplete suggestions', error);
return [];
}
}
/**
* Provides autocomplete suggestions for a specific node in the AST.
*/
private getSuggestionsForNode(
ast: Expression,
node: AnyNode,
expr: string,
cursorOffset: number,
context: SymbolsTable
): AutocompleteSuggestions {
// When the node is an identifier look up the parent to get more context for the suggestions.
let inferNode: AnyNode = node;
if (node.type === 'Identifier' || node.type === 'Literal') {
const parent = findParentNode(node, ast);
inferNode =
parent &&
!['ExpressionStatement', 'LogicalExpression', 'ConditionalExpression'].includes(
parent.type
)
? parent
: node;
}
switch (inferNode.type) {
case 'Identifier':
case 'MemberExpression': {
const pathParts = this.getSymbolsPathPartsForMemberExpressionNode(
inferNode,
cursorOffset
);
// Fetch suggestions from the symbol table
const candidatesKeys = context.getMatchingSymbolsKeys(pathParts);
const suggestions: AutocompleteSymbolSuggestion[] = [];
for (const candidate of candidatesKeys) {
const symbolInfo = context.getSymbolInfo(candidate);
if (symbolInfo) {
suggestions.push({ type: 'symbol', symbol: symbolInfo });
}
}
if (suggestions.length === 1) {
const lastPathSegment = pathParts.at(-1)?.replace(/\*$/, '');
// Return no suggestion when the only match is an exact match of the
// typed token.
return lastPathSegment !== suggestions[0]?.symbol.definition.name
? suggestions
: [];
}
return suggestions;
}
case 'AssignmentExpression':
case 'UnaryExpression': {
// Provide suggestions for binary or logical operators based on the parsed operator
// of the partial expression (e.g suggest "==" when typing "=" (parsed as AssignmentExpression)).
return this.getOperatorSuggestionsForNode(ast, inferNode, cursorOffset, context);
}
case 'BinaryExpression': {
const { left, right } = inferNode;
const isOperatorBinaryOp = SUPPORTED_BINARY_OPERATORS.some(
(op) => op.operator === inferNode.operator
);
const operatorIndex = expr.indexOf(inferNode.operator, left.end);
const operatorOffset = operatorIndex + inferNode.operator.length;
const isCursorAfterOperator = cursorOffset > operatorOffset;
const shouldSuggestValues =
isCursorAfterOperator &&
isOperatorBinaryOp &&
isNodeAtCursor(right, cursorOffset);
if (shouldSuggestValues) {
return this.getLiteralValueSuggestionsForNode(inferNode, cursorOffset, context);
}
// Provide suggestions for binary operators based on the parsed operator
return this.getOperatorSuggestionsForNode(ast, inferNode, cursorOffset, context);
}
case 'ConditionalExpression': {
const { consequent, alternate } = inferNode;
if (isNodeAtCursor(consequent, cursorOffset)) {
return this.getSuggestionsForNode(ast, consequent, expr, cursorOffset, context);
}
if (isNodeAtCursor(alternate, cursorOffset)) {
return this.getSuggestionsForNode(ast, alternate, expr, cursorOffset, context);
}
}
}
return [];
}
/**
* Return a path corresponding to the MemberExpression node that can be used to lookup matching symbols in the symbol table.
*/
private getSymbolsPathPartsForMemberExpressionNode(
node: MemberExpression | Identifier,
cursorOffset: number,
options?: { withWildcardMatches: boolean }
): string[] {
const withWildcardMatches = options?.withWildcardMatches ?? true;
const pathParts: string[] = [];
switch (node.type) {
case 'MemberExpression':
{
const memberProperty = node.property;
// Only support identifier or literal expressions as member properties.
if (!isSupportedMemberProperty(memberProperty)) {
return [];
}
// Push the path part corresponding to the member property (e.g b in a.b or a['b'])
const propertyPathPart = this.getSymbolsPathPartFromMemberProperty(
memberProperty,
cursorOffset
);
if (propertyPathPart) {
pathParts.push(
withWildcardMatches ? `${propertyPathPart}*` : propertyPathPart
);
}
// Go through the parent(s) in the chain and add their path parts as well
let parent: Expression | Super | undefined = node.object;
while (parent) {
const parentProperty = 'property' in parent ? parent.property : parent;
// Only support identifier or literal expressions as parent property.
const parentPropertyPathPart = isSupportedMemberProperty(parentProperty)
? this.getSymbolsPathPartFromMemberProperty(
parentProperty,
cursorOffset
)
: undefined;
if (parentPropertyPathPart) {
pathParts.unshift(parentPropertyPathPart);
}
parent = 'object' in parent ? parent.object : undefined;
}
}
break;
case 'Identifier': {
const propertyPathPart = this.getSymbolsPathPartFromMemberProperty(
node,
cursorOffset
);
if (propertyPathPart) {
pathParts.push(withWildcardMatches ? `${propertyPathPart}*` : propertyPathPart);
}
break;
}
default:
assertNever(node);
}
return pathParts;
}
/**
* Return a part of a symbol path corresponding to a MemberExpression node property.
*/
private getSymbolsPathPartFromMemberProperty(
node: Identifier | Literal,
cursorOffset: number
): string {
switch (node.type) {
case 'Identifier': {
if (isDummy(node)) {
return '*';
}
return isNodeAtCursor(node, cursorOffset)
? node.name.slice(0, cursorOffset)
: node.name;
}
case 'Literal': {
if (isDummy(node) || !node.value) {
return '*';
}
const value = String(node.value);
return isNodeAtCursor(node, cursorOffset) ? value.slice(0, cursorOffset) : value;
}
default:
assertNever(node);
}
}
/**
* Provides autocomplete literal value suggestions for a specific node in the AST.
*/
private getLiteralValueSuggestionsForNode(
node: BinaryExpression,
cursorOffset: number,
context: SymbolsTable
): AutocompleteLiteralValueSuggestion[] {
const { left, right } = node;
if (left.type !== 'MemberExpression' && left.type !== 'Identifier') {
return [];
}
if (right.type !== 'Identifier' && right.type !== 'Literal') {
return [];
}
const leftSymbolPath = this.getSymbolsPathPartsForMemberExpressionNode(left, cursorOffset, {
withWildcardMatches: false,
});
const isLeftComputedMember = left.type === 'MemberExpression' && left.computed;
const leftSymbolInfo = context.getSymbolInfo(
isLeftComputedMember ? leftSymbolPath.slice(0, -1) : leftSymbolPath
);
if (!leftSymbolInfo) {
return [];
}
switch (leftSymbolInfo.definition.type) {
case SymbolType.Boolean: {
const suggestions: DirectLiteralValueSuggestion[] = [
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
},
{
type: 'literal-value',
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
},
];
return isDummy(right)
? suggestions
: suggestions.filter((literalValue) => {
const literalValueString = String(literalValue.value.data);
const rightNodeValue =
right.type === 'Identifier' ? right.name : (right.raw ?? '');
return (
literalValueString !== rightNodeValue &&
literalValueString.startsWith(rightNodeValue)
);
});
}
case SymbolType.String: {
if (!leftSymbolInfo.definition.enum) {
return [];
}
const rightNodeValue = (() => {
if (right.type === 'Identifier') {
return right.name;
}
const nodeValue = right.raw ?? '';
if (/^(['"])(.*)\1$/.test(nodeValue)) {
return null;
}
return nodeValue.replaceAll(/["']/g, '');
})();
if (rightNodeValue === null) {
return [];
}
const suggestions = isDummy(right)
? leftSymbolInfo.definition.enum
: leftSymbolInfo.definition.enum.filter((enumValue) =>
enumValue.startsWith(rightNodeValue)
);
return suggestions.map((value) => ({
type: 'literal-value',
value: {
kind: 'direct',
type: SymbolType.String,
data: value,
},
}));
}
case SymbolType.Array: {
// Only return a literal in array value suggestion when the left hand side of the binary expression
// is computed, e.g myArray[1]
return isLeftComputedMember
? [
{
type: 'literal-value',
value: {
kind: 'in-array',
srcSymbol: leftSymbolInfo.definition,
matchedLiteralString: !isDummy(right)
? right.type === 'Identifier'
? right.name
: (right.raw ?? '')
: '',
},
},
]
: [];
}
default:
return [];
}
}
/**
* Provides autocomplete operator suggestions for a specific node in the AST.
*/
private getOperatorSuggestionsForNode(
ast: Expression,
node: AnyNode,
cursorOffset: number,
context: SymbolsTable
): AutocompleteOperatorSuggestion[] {
if (node.type === 'Literal') {
const parent = findParentNode(node, ast);
if (parent?.type === 'BinaryExpression') {
return [...SUPPORTED_LOGICAL_OPERATORS, ...SUPPORTED_CONDITIONAL_OPERATORS].map(
(op) => ({
type: 'operator',
...op,
})
);
}
const literalSymbol = SymbolsTable.inferSymbolFromValue(node.raw);
return this.getOperatorSuggestionsForSymbol(literalSymbol);
}
// When the node is an identifier look the parent to get more context for the suggestions
let inferNode: AnyNode = node;
if (node.type === 'Identifier') {
const parent = findParentNode(node, ast);
inferNode = parent && parent.type !== 'ExpressionStatement' ? parent : node;
}
switch (inferNode.type) {
case 'MemberExpression': {
const pathParts = this.getSymbolsPathPartsForMemberExpressionNode(
inferNode,
cursorOffset,
{
withWildcardMatches: false,
}
);
const symbolInfo = context.getSymbolInfo(pathParts);
return symbolInfo
? this.getOperatorSuggestionsForSymbol(symbolInfo.definition)
: [];
}
case 'AssignmentExpression':
case 'BinaryExpression':
case 'UnaryExpression': {
// Starting to write a binary/logical operator so suggest operator matching the already
// typed character as operator.
const operator = inferNode.operator;
return (
[...SUPPORTED_BINARY_OPERATORS, ...SUPPORTED_LOGICAL_OPERATORS]
.filter((op) => op.operator.startsWith(operator))
// No need to include the operator that match exactly
.filter((op) => op.operator !== operator)
.map((op) => ({
type: 'operator',
...op,
}))
);
}
default:
return [];
}
}
/**
* Provides autocomplete operator suggestions based on the type of a symbol.
*/
private getOperatorSuggestionsForSymbol(
symbol: ExtractSymbolDef<SymbolType>
): AutocompleteOperatorSuggestion[] {
switch (symbol.type) {
case SymbolType.Number:
case SymbolType.Boolean:
case SymbolType.Null:
case SymbolType.Undefined:
case SymbolType.Object:
case SymbolType.Array: {
const equalityOps = SUPPORTED_BINARY_OPERATORS.slice(0, 4);
const finalSuggestions =
symbol.type === SymbolType.Boolean
? [
...equalityOps,
...SUPPORTED_LOGICAL_OPERATORS,
...SUPPORTED_CONDITIONAL_OPERATORS,
]
: equalityOps;
return finalSuggestions.map((op) => ({
type: 'operator',
...op,
}));
}
case SymbolType.String:
return SUPPORTED_BINARY_OPERATORS.map((op) => ({
type: 'operator',
...op,
}));
case SymbolType.Function:
return this.getOperatorSuggestionsForSymbol(symbol.returns);
case SymbolType.Union: {
return symbol.members.reduce<AutocompleteOperatorSuggestion[]>((prev, cur) => {
prev.push(...this.getOperatorSuggestionsForSymbol(cur));
return prev;
}, []);
}
default:
assertNever(symbol);
}
}
}
/**
* Finds the parent of child node in the provided AST.
*/
function findParentNode(child: AnyNode, ast: Expression): AnyNode | undefined {
let foundParent: AnyNode | undefined;
walk.ancestor(ast, {
[child.type]: (node: AcornNode, _state: undefined, ancestors: AnyNode[]) => {
if (node.start === child.start && node.end === child.end) {
// The parent is the second last ancestor in the stack (last one is the actual node)
foundParent = ancestors[ancestors.length - 2];
}
},
});
return foundParent;
}
function isSupportedMemberProperty(node: Expression | PrivateIdentifier | Super) {
return node.type === 'Identifier' || node.type === 'Literal';
}
function isNodeAtCursor(node: AcornNode, cursorOffset: number) {
return cursorOffset >= node.start && cursorOffset <= node.end;
}
function isAnyNode(node: AcornNode): node is AnyNode {
return 'type' in node;
}
+24
View File
@@ -0,0 +1,24 @@
import type { Position, Token } from 'acorn';
export class ExpressionError extends Error {
/**
* The location of the error in the parsed expression.
*/
public location?: Position | null;
/**
* The expression token with the error
*/
public token?: Token;
constructor(message: string, loc?: Position | null, token?: Token) {
super(message);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ExpressionError);
}
this.name = 'ExpressionError';
this.location = loc;
this.token = token;
}
}
+7
View File
@@ -0,0 +1,7 @@
export * from './errors';
export * from './input-values';
export * from './runtime';
export * from './symbols';
export * from './template';
export * from './types';
export * from './utils';
+78
View File
@@ -0,0 +1,78 @@
import type { JSONSchema7 } from 'json-schema';
import { filterOutNullable } from './utils';
type InputValuesType =
| null
| string
| number
| boolean
| { [key: string]: InputValuesType }
| InputValuesType[];
/**
* Infers a default inputValues object based on the JSON schema of an object.
*/
export function inferDefaultInputValuesFromObjectJSONSchema(
schema: JSONSchema7
): Record<string, InputValuesType> {
if (schema.type !== 'object' || !schema.properties) {
throw new Error(`Expected schema of object to be provided: ${schema.type}`);
}
const result: Record<string, InputValuesType> = {};
for (const [key, propertySchema] of Object.entries(schema.properties)) {
if (typeof propertySchema === 'boolean') {
continue;
}
result[key] = inferDefaultInputValueFromJSONSchema(propertySchema);
}
return result;
}
function inferDefaultInputValueFromJSONSchema(schema: JSONSchema7): InputValuesType {
switch (schema.type) {
case 'object':
return inferDefaultInputValuesFromObjectJSONSchema(schema);
case 'array': {
if (schema.items && Array.isArray(schema.items)) {
return schema.items.map((itemSchema) => {
if (typeof itemSchema === 'boolean') {
return false;
}
return inferDefaultInputValueFromJSONSchema(itemSchema);
});
}
return [];
}
case 'string':
case 'number':
case 'integer':
case 'boolean':
case 'null':
return inferDefaultInputValueFromPrimitive(schema);
default:
throw new Error(`Unsupported schema type: ${schema.type}`);
}
}
function inferDefaultInputValueFromPrimitive(schema: JSONSchema7): InputValuesType {
switch (schema.type) {
case 'boolean':
return true;
case 'number':
case 'integer':
return 1234;
case 'string': {
const enumValues = schema.enum?.filter(filterOutNullable);
return enumValues?.[0] ?? 'default';
}
case 'null':
return null;
default:
throw new Error(`Unsupported schema type: ${schema.type}`);
}
}
+311
View File
@@ -0,0 +1,311 @@
import {
type Options as AcornOptions,
type Expression,
type ExpressionStatement,
type Position,
type Program,
type Token,
parse,
tokenizer,
} from 'acorn';
import { parse as parseLoose } from 'acorn-loose';
import escodegen from 'escodegen';
import evalESTreeExpr from 'eval-estree-expression';
const { evaluate, variables } = evalESTreeExpr;
import { AutoComplete } from './autocomplete';
import { ExpressionError } from './errors';
import type { SymbolsTable } from './symbols';
import type { TemplatePart } from './template';
import { parseTemplate as parseTemplateParts } from './template';
import type { ExpressionAutocompleteResults, ExpressionParserResult, Logger } from './types';
import { formatExpressionResult } from './utils';
export class ExpressionRuntime {
#parserOptions: AcornOptions;
#autocompleter: AutoComplete;
#logger: Logger;
constructor(logger: Logger = console) {
this.#parserOptions = {
ecmaVersion: 'latest',
sourceType: 'script',
allowHashBang: false,
locations: true,
};
this.#autocompleter = new AutoComplete(this, logger);
this.#logger = logger;
}
/**
* Evaluates an expression based on the given inputs/context.
*/
public evaluate(expr: string, inputs: object): unknown {
try {
const parsed = this.parse(expr);
if (parsed.invalidNodes.length > 0) {
throw new ExpressionError('Invalid nodes found when parsing');
}
return evaluate.sync<Expression>(parsed.result, inputs, {
functions: true,
withMembers: true,
generate: escodegen.generate,
});
} catch (error) {
throw error instanceof Error
? new ExpressionError(error.message)
: new ExpressionError('Unexpected error');
}
}
/**
* Evaluates an expression safely by returning the error instead of throwing when invalid.
*/
public safeEvaluate(
expr: string,
inputs: object
): { value: unknown; error?: undefined } | { value?: undefined; error: ExpressionError } {
try {
const value = this.evaluate(expr, inputs);
return {
value,
};
} catch (error) {
this.#logger.error(`Error while evaluating expression ${expr}`, error);
if (error instanceof ExpressionError) {
return {
value: undefined,
error,
};
}
return {
value: undefined,
error:
error instanceof Error
? new ExpressionError(error.message)
: new ExpressionError('Unexpected error'),
};
}
}
/**
* Evaluates a condition safely to a boolean.
*/
public evaluateBoolean(expr: string, inputs: object): boolean {
if (expr.trim().length === 0) {
return true;
}
const evalResult = this.safeEvaluate(expr, inputs);
if (typeof evalResult.error !== 'undefined') {
return false;
}
return Boolean(evalResult.value);
}
/**
* Evaluates an array of conditions as a single logical expression.
* The function treats the conditions as if they were joined by an AND operator,
* meaning the evaluation returns `true` only if all conditions are truthy.
*/
public evaluateBooleanAll(expressions: string[], inputs: object): boolean {
if (expressions.length === 0) {
return true;
}
return expressions.every((expression) => this.evaluateBoolean(expression, inputs));
}
/**
* Parse a template and validate all embedded expressions.
*/
public parseTemplate(template: string): { parts: TemplatePart[]; errors: ExpressionError[] } {
const parts = parseTemplateParts(template);
const errors: ExpressionError[] = [];
for (const part of parts) {
if (part.type === 'expression') {
try {
const { invalidNodes } = this.parse(part.value);
if (invalidNodes.length > 0) {
errors.push(new ExpressionError('Invalid expression'));
}
} catch (error) {
errors.push(error as ExpressionError);
}
}
}
return { parts, errors };
}
/**
* Evaluate a template string containing `{{ expression }}` placeholders.
*/
public evaluateTemplate(template: string, inputs: object): string {
const { parts } = this.parseTemplate(template);
return parts
.map((part) => {
if (part.type === 'text') {
return part.value;
}
const result = this.evaluate(part.value, inputs);
return formatExpressionResult(result, '');
})
.join('');
}
/**
* Given an expression, returns a list of variables used in the expression.
*/
public getVariables(expr: string): string[] {
try {
const parsed = this.parse(expr);
if (parsed.invalidNodes.length > 0) {
throw new ExpressionError('Invalid nodes found when parsing');
}
return variables(parsed.result, {
functions: true,
withMembers: true,
generate: escodegen.generate,
});
} catch (error) {
this.#logger.error(`Error while parsing expression ${expr} to get variables`, error);
return [];
}
}
/**
* Parses a binary expression and returns an @ExpressionParserResult.
*/
public parse(
expr: string,
options: { loose?: boolean } = {
loose: false,
}
): ExpressionParserResult {
try {
const ast = options.loose
? parseLoose(expr, { ...this.#parserOptions })
: parse(expr, { ...this.#parserOptions });
if (!ast.body || ast.body.length === 0) {
throw new ExpressionError('Empty or invalid expression');
}
// Extract the first expression statement that we find
const firstExprIndex = ast.body.findIndex((node) => isParsedExpressionStatement(node));
const [statement] = ast.body.splice(firstExprIndex, 1);
if (!statement || !isParsedExpressionStatement(statement)) {
throw new ExpressionError('Empty or invalid expression');
}
// Return information on the other nodes as invalid nodes
const invalidNodes = ast.body.filter(filterOutModuleDeclarationStatement);
return {
result: statement.expression,
invalidNodes,
};
} catch (error) {
if (error instanceof SyntaxError) {
throw createExpressionErrorFromSyntaxError(expr, error);
}
if (error instanceof ExpressionError) {
throw error;
}
throw new ExpressionError('Unexpected error');
}
}
/**
* Provides autocomplete suggestions for the given expression at the provided cursor offset.
*/
public autocomplete(
expr: string,
cursorOffset: number,
context: SymbolsTable
): ExpressionAutocompleteResults {
const suggestions = this.#autocompleter.getSuggestions(expr, cursorOffset, context);
return { suggestions };
}
public generate(_node: Expression): string {
throw new Error('Not yet implemented');
}
}
function createExpressionErrorFromSyntaxError(
code: string,
error: SyntaxError & { loc?: Position }
): ExpressionError {
const loc = error.loc;
if (!loc) {
return new ExpressionError(error.message);
}
const errorMessage = `${error.message.replace(/\s*\(\d+:\d+\)$/, '')} at ${code.split('\n').length > 1 ? `line ${loc.line}, ` : ''}char ${loc.column}`;
const token = getTokenAtLoc(code, loc);
if (!token) {
return new ExpressionError(errorMessage, loc);
}
return new ExpressionError(errorMessage, loc, token);
}
function getTokenAtLoc(code: string, errorLoc: Position): Token | undefined {
const tokens = tokenizer(code, {
ecmaVersion: 'latest',
locations: true,
});
try {
for (const token of tokens) {
if (!token.loc) {
continue;
}
const { start, end } = token.loc;
const onSameLine = errorLoc.line === start.line;
const inColumnRange = errorLoc.column >= start.column && errorLoc.column < end.column;
if (onSameLine && inColumnRange) {
return token;
}
}
} catch (_error) {
return undefined;
}
return undefined;
}
function isParsedExpressionStatement(
statement: Program['body'][number]
): statement is ExpressionStatement {
return statement.type === 'ExpressionStatement';
}
export function filterOutModuleDeclarationStatement(
statement: Program['body'][number]
): statement is ExpressionStatement {
return ![
'ImportDeclaration',
'ExportNamedDeclaration',
'ExportDefaultDeclaration',
'ExportAllDeclaration',
].includes(statement.type);
}
@@ -0,0 +1,509 @@
import { describe, expect, it } from 'bun:test';
import { SymbolArray, SymbolObject, SymbolString } from '../symbols';
import { SymbolsTable } from '../symbols-table';
import type { SymbolType } from '../types';
describe('ExpressionRuntime', () => {
const initialSymbols = {
visitor: SymbolObject({
name: 'visitor',
properties: {
claims: SymbolObject({
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: SymbolString({ name: 'key' }),
flags: SymbolObject({
name: 'flags',
properties: {
FLAG1: SymbolString({ name: 'FLAG1' }),
FLAG2: SymbolString({ name: 'FLAG2' }),
FLAG3: SymbolString({ name: 'FLAG3' }),
},
methods: [],
}),
hello: SymbolArray({
name: 'hello',
description: 'An array of string',
items: SymbolString(),
}),
},
methods: [],
}),
},
methods: [],
}),
};
describe('addSymbols', () => {
it('should the symbols matching the provided object to the table', () => {
const symbolsTable = new SymbolsTable(initialSymbols);
expect(symbolsTable.getSymbolInfo(['visitor'])).toMatchObject({
definition: {
type: 'object',
name: 'visitor',
properties: {
claims: {
type: 'object',
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: {
type: 'string',
name: 'key',
},
flags: {
type: 'object',
name: 'flags',
properties: {
FLAG1: {
type: 'string',
name: 'FLAG1',
},
FLAG2: {
type: 'string',
name: 'FLAG2',
},
FLAG3: {
type: 'string',
name: 'FLAG3',
},
},
methods: [],
},
hello: {
type: 'array',
name: 'hello',
description: 'An array of string',
items: {
type: 'string',
},
},
},
methods: [],
},
},
methods: [],
},
ref: 'visitor',
childrenRefs: ['visitor.claims'],
});
symbolsTable.addSymbols({
space: SymbolObject({
name: 'space',
properties: {
id: SymbolString({ name: 'id' }),
title: SymbolString({ name: 'title' }),
},
methods: [],
}),
});
expect(symbolsTable.getSymbolInfo(['space'])).toMatchObject({
definition: {
type: 'object',
name: 'space',
properties: {
id: {
type: 'string',
name: 'id',
},
title: {
type: 'string',
name: 'title',
},
},
methods: [],
},
ref: 'space',
childrenRefs: ['space.id', 'space.title'],
});
});
});
describe('Symbols standard library', () => {
it('should allow to access methods & properties defined as part of the standard library', () => {
const symbolsTable = new SymbolsTable({
id: SymbolString({ name: 'id' }),
title: SymbolString({ name: 'title' }),
});
expect(
symbolsTable.getSymbolInfo<SymbolType.String>('id')?.definition.properties.length
).toMatchObject({
type: 'number',
name: 'length',
description:
'The length data property of a String value contains the length of the string in UTF-16 code units.',
});
});
});
describe('getSymbolInfo', () => {
it('should add the symbols matching the initial symbol definition passed to the constructor', () => {
const symbolsTable = new SymbolsTable(initialSymbols);
expect(symbolsTable.getSymbolInfo(['visitor'])).toMatchObject({
definition: {
type: 'object',
name: 'visitor',
properties: {
claims: {
type: 'object',
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: {
type: 'string',
name: 'key',
},
flags: {
type: 'object',
name: 'flags',
properties: {
FLAG1: {
type: 'string',
name: 'FLAG1',
},
FLAG2: {
type: 'string',
name: 'FLAG2',
},
FLAG3: {
type: 'string',
name: 'FLAG3',
},
},
methods: [],
},
hello: {
type: 'array',
name: 'hello',
description: 'An array of string',
items: {
type: 'string',
},
},
},
methods: [],
},
},
methods: [],
},
ref: 'visitor',
childrenRefs: ['visitor.claims'],
});
expect(symbolsTable.getSymbolInfo(['visitor', 'claims'])).toMatchObject({
definition: {
type: 'object',
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: {
type: 'string',
name: 'key',
},
flags: {
type: 'object',
name: 'flags',
properties: {
FLAG1: {
type: 'string',
name: 'FLAG1',
},
FLAG2: {
type: 'string',
name: 'FLAG2',
},
FLAG3: {
type: 'string',
name: 'FLAG3',
},
},
methods: [],
},
hello: {
type: 'array',
name: 'hello',
description: 'An array of string',
items: {
type: 'string',
},
},
},
methods: [],
},
ref: 'visitor.claims',
parentRef: 'visitor',
childrenRefs: [
'visitor.claims.key',
'visitor.claims.flags',
'visitor.claims.hello',
],
});
expect(symbolsTable.getSymbolInfo(['visitor', 'claims', 'key'])).toMatchObject({
definition: {
type: 'string',
name: 'key',
},
ref: 'visitor.claims.key',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.key.length',
'visitor.claims.key.at',
'visitor.claims.key.startsWith',
'visitor.claims.key.endsWith',
'visitor.claims.key.includes',
'visitor.claims.key.toLowerCase',
'visitor.claims.key.toUpperCase',
'visitor.claims.key.trim',
],
});
expect(symbolsTable.getSymbolInfo(['visitor', 'claims', 'flags'])).toMatchObject({
definition: {
type: 'object',
name: 'flags',
properties: {
FLAG1: {
type: 'string',
name: 'FLAG1',
},
FLAG2: {
type: 'string',
name: 'FLAG2',
},
FLAG3: {
type: 'string',
name: 'FLAG3',
},
},
methods: [],
},
ref: 'visitor.claims.flags',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.flags.FLAG1',
'visitor.claims.flags.FLAG2',
'visitor.claims.flags.FLAG3',
],
});
expect(
symbolsTable.getSymbolInfo(['visitor', 'claims', 'flags', 'FLAG1'])
).toMatchObject({
definition: {
type: 'string',
name: 'FLAG1',
},
ref: 'visitor.claims.flags.FLAG1',
parentRef: 'visitor.claims.flags',
childrenRefs: [
'visitor.claims.flags.FLAG1.length',
'visitor.claims.flags.FLAG1.at',
'visitor.claims.flags.FLAG1.startsWith',
'visitor.claims.flags.FLAG1.endsWith',
'visitor.claims.flags.FLAG1.includes',
'visitor.claims.flags.FLAG1.toLowerCase',
'visitor.claims.flags.FLAG1.toUpperCase',
'visitor.claims.flags.FLAG1.trim',
],
});
expect(
symbolsTable.getSymbolInfo(['visitor', 'claims', 'flags', 'FLAG2'])
).toMatchObject({
definition: {
type: 'string',
name: 'FLAG2',
},
ref: 'visitor.claims.flags.FLAG2',
parentRef: 'visitor.claims.flags',
childrenRefs: [
'visitor.claims.flags.FLAG2.length',
'visitor.claims.flags.FLAG2.at',
'visitor.claims.flags.FLAG2.startsWith',
'visitor.claims.flags.FLAG2.endsWith',
'visitor.claims.flags.FLAG2.includes',
'visitor.claims.flags.FLAG2.toLowerCase',
'visitor.claims.flags.FLAG2.toUpperCase',
'visitor.claims.flags.FLAG2.trim',
],
});
expect(symbolsTable.getSymbolInfo(['visitor', 'claims', 'hello'])).toMatchObject({
definition: {
type: 'array',
name: 'hello',
description: 'An array of string',
items: {
type: 'string',
},
},
ref: 'visitor.claims.hello',
parentRef: 'visitor.claims',
childrenRefs: [
'visitor.claims.hello.length',
'visitor.claims.hello.at',
'visitor.claims.hello.includes',
'visitor.claims.hello.some',
'visitor.claims.hello.every',
],
});
});
});
describe('inferSymbolFromValue', () => {
it('should infer properly a symbol based on a value', () => {
const symbolDef = SymbolsTable.inferSymbolFromValue(
{
visitor: {
claims: {
key: 'test',
flags: {
FLAG1: 'testflag1',
FLAG2: 'testflag2',
FLAG3: 'testflag3',
},
hello: ['test', 'test1', 'test2'],
},
},
},
'context'
);
expect(symbolDef).toMatchObject({
type: 'object',
name: 'context',
properties: {
visitor: {
type: 'object',
name: 'visitor',
properties: {
claims: {
type: 'object',
name: 'claims',
properties: {
key: {
type: 'string',
name: 'key',
},
flags: {
type: 'object',
name: 'flags',
properties: {
FLAG1: {
type: 'string',
name: 'FLAG1',
},
FLAG2: {
type: 'string',
name: 'FLAG2',
},
FLAG3: {
type: 'string',
name: 'FLAG3',
},
},
methods: [],
},
hello: {
type: 'array',
name: 'hello',
items: {
type: 'string',
},
},
},
methods: [],
},
},
methods: [],
},
},
methods: [],
});
});
});
describe('inferSymbolFromJSONSchema', () => {
it('should infer properly a symbol table based on a JSON schema', () => {
const symbolDef = SymbolsTable.inferSymbolFromJSONSchema(
{
type: 'object',
description: `The attributes tied to a site's visitor.`,
properties: {
claims: {
type: 'object',
properties: {
key: {
type: 'string',
},
flags: {
type: 'object',
description: 'The user feature flags',
properties: {
FLAG1: { type: 'string' },
FLAG2: { type: 'string' },
FLAG3: { type: 'string' },
},
},
hello: {
type: 'string',
enum: ['test', 'test1', 'test2'],
},
},
},
},
},
'visitor'
);
expect(symbolDef).toMatchObject({
type: 'object',
name: 'visitor',
description: `The attributes tied to a site's visitor.`,
properties: {
claims: {
type: 'object',
name: 'claims',
properties: {
key: {
type: 'string',
name: 'key',
},
flags: {
type: 'object',
name: 'flags',
description: 'The user feature flags',
properties: {
FLAG1: {
type: 'string',
name: 'FLAG1',
},
FLAG2: {
type: 'string',
name: 'FLAG2',
},
FLAG3: {
type: 'string',
name: 'FLAG3',
},
},
methods: [],
},
hello: {
type: 'string',
enum: ['test', 'test1', 'test2'],
},
},
methods: [],
},
},
methods: [],
});
});
});
});
+3
View File
@@ -0,0 +1,3 @@
export * from './symbols';
export * from './symbols-table';
export * from './types';
+350
View File
@@ -0,0 +1,350 @@
import type { JSONSchema7 } from 'json-schema';
import { filterOutNullable } from '../utils';
import {
SymbolArray,
SymbolBoolean,
SymbolNull,
SymbolNumber,
SymbolObject,
SymbolString,
SymbolUndefined,
} from './symbols';
import {
type ExtractSymbolDef,
type GenericSymbolDef,
type ObjectSymbolDef,
SymbolType,
type SymbolWithMethods,
type SymbolWithProperties,
resolveSymbolDef,
} from './types';
export interface SymbolInfo<T extends SymbolType = SymbolType> {
/**
* Definition of the symbol.
*/
definition: ExtractSymbolDef<T>;
/**
* Reference of the symbol in the table of symbols.
*/
ref: string;
/**
* Stores the reference to the parent symbol.
*/
parentRef?: string;
/**
* Stores the reference to the children symbols.
*/
childrenRefs?: string[];
}
export class SymbolError extends Error {
constructor(message: string) {
super(message);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, SymbolError);
}
this.name = 'SymbolError';
}
}
export class SymbolsTable {
/**
* Internal table that keeps track of all symbols reference.
*/
#table: Record<string, SymbolInfo>;
/**
* Internal table that keep track of the raw symbols definitions.
*/
#rawSymbols: Record<string, GenericSymbolDef>;
constructor(initialContext: Record<string, GenericSymbolDef> = {}) {
this.#table = {};
this.#rawSymbols = {};
this.addSymbols(initialContext);
}
/**
* Create a new symbols table by merging the current one with the provided one.
*/
merge(other: SymbolsTable): SymbolsTable {
return new SymbolsTable({
...this.#rawSymbols,
...other.#rawSymbols,
});
}
toString() {
return JSON.stringify(this.#table, null, 2);
}
/**
* Infer the symbol of a value and generate the appropriate symbol definition.
*/
static inferSymbolFromValue(value: unknown, name?: string): ExtractSymbolDef<SymbolType> {
if (Array.isArray(value)) {
if (value.length === 0) {
return SymbolArray({ items: SymbolUndefined() });
}
const firstItemSymbol = SymbolsTable.inferSymbolFromValue(value.at(0));
// Check that the array is not a mixin of different items types.
if (value.length > 1) {
const secondItemSymbol = SymbolsTable.inferSymbolFromValue(value.at(1));
if (firstItemSymbol.type !== secondItemSymbol.type) {
throw new SymbolError('Array with mixin items types are not supported');
}
}
return SymbolArray({ name, items: firstItemSymbol });
}
if (typeof value === 'undefined') {
return SymbolUndefined({ name });
}
if (value === null) {
return SymbolNull({ name });
}
const valueType = typeof value;
switch (valueType) {
case 'string':
return SymbolString({ name });
case 'number':
return SymbolNumber({ name });
case 'boolean':
return SymbolBoolean({ name });
case 'object': {
const properties = Object.entries(value).reduce<Record<string, GenericSymbolDef>>(
(prev, [name, val]) => {
prev[name] = SymbolsTable.inferSymbolFromValue(val, name);
return prev;
},
{}
);
return SymbolObject({ name, properties, methods: [] });
}
default:
throw new SymbolError(`Unsupported symbol type ${valueType}`);
}
}
/**
* Infer a table of symbol based on a JSON schema object describing it.
*/
static inferSymbolFromJSONSchema(
schema: JSONSchema7,
name?: string
): ExtractSymbolDef<SymbolType> {
switch (schema.type) {
case 'string':
return SymbolString({
name,
...(schema.description ? { description: schema.description } : {}),
...(schema.enum
? {
enum: schema.enum
.filter(filterOutNullable)
.map((enumValue) => enumValue.toString()),
}
: {}),
});
case 'number':
case 'integer':
return SymbolNumber({
name,
...(schema.description ? { description: schema.description } : {}),
});
case 'boolean':
return SymbolBoolean({
name,
...(schema.description ? { description: schema.description } : {}),
});
case 'null':
return SymbolNull({
name,
...(schema.description ? { description: schema.description } : {}),
});
case 'object':
return SymbolsTable.#buildObjectSymbolFromJSONSchemaObject(schema, name);
case 'array':
return SymbolsTable.#buildArraySymbolFromJSONSchemaArray(schema, name);
default:
throw new Error(`Unsupported schema type: ${schema.type}`);
}
}
static #buildObjectSymbolFromJSONSchemaObject(
schema: JSONSchema7,
name?: string
): ObjectSymbolDef {
const properties: Record<string, GenericSymbolDef> = {};
if (schema.properties) {
Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => {
properties[propertyName] = SymbolsTable.inferSymbolFromJSONSchema(
propertySchema as JSONSchema7,
propertyName
);
});
}
return SymbolObject({
name,
properties,
...(schema.description ? { description: schema.description } : {}),
methods: [],
});
}
static #buildArraySymbolFromJSONSchemaArray(
schema: JSONSchema7,
name?: string
): ExtractSymbolDef<SymbolType.Array> {
if (schema.items) {
const itemSymbol = SymbolsTable.inferSymbolFromJSONSchema(
schema.items as JSONSchema7,
`${name || ''}_item`
);
return SymbolArray({
name,
...(schema.description ? { description: schema.description } : {}),
items: itemSymbol,
});
}
return SymbolArray({
name,
...(schema.description ? { description: schema.description } : {}),
items: SymbolUndefined(),
});
}
private generateSymbolRefPath(path: string[]): string {
return path.join('.');
}
/**
* Add a single symbol to the table at the provided path.
*/
private addSymbol(path: string[], definition: GenericSymbolDef, raw = false): void {
const fullPath = this.generateSymbolRefPath(path);
const parentPath = path.slice(0, -1).join('.');
if (this.#table[fullPath]) {
throw new SymbolError(`Symbol "${fullPath}" already exists.`);
}
if (raw) {
this.#rawSymbols[fullPath] = definition;
}
// Add the new symbol linking it to its parent
this.#table[fullPath] = {
definition: resolveSymbolDef(definition),
ref: fullPath,
parentRef: parentPath || undefined,
childrenRefs: [],
};
if (parentPath && this.#table[parentPath]) {
if (this.#table[parentPath].childrenRefs) {
this.#table[parentPath].childrenRefs.push(fullPath);
}
}
// Add any nested symbols if the value is a symbol with properties...
if (isSymbolWithProperties(definition)) {
Object.entries(definition.properties).forEach(([propKey, propSymbol]) => {
if (isObjectSymbol(propSymbol)) {
this.addSymbols({ [propKey]: propSymbol }, path, false);
} else {
this.addSymbol([...path, propKey], propSymbol, false);
}
});
}
// ...or a symbol with methods
if (isSymbolWithMethods(definition)) {
definition.methods.forEach((methodSymbol) => {
this.addSymbol([...path, methodSymbol.name], methodSymbol, false);
});
}
}
/**
* Add the provided object of symbols definitions to the symbol table.
*/
public addSymbols(
symbols: Record<string, GenericSymbolDef>,
prefix: string[] = [],
raw = true
): void {
for (const [key, symbolDef] of Object.entries(symbols)) {
const path = [...prefix, key];
// Add the current symbol to the table.
this.addSymbol(path, symbolDef, raw);
}
}
/**
* Get a symbol's information using its path in the table.
*/
public getSymbolInfo<T extends SymbolType>(path: string | string[]): SymbolInfo<T> | undefined {
const key = Array.isArray(path) ? path.join('.') : path;
const info = this.#table[key];
return info ? typedSymbolInfo(info) : undefined;
}
/**
* Get all symbol keys matching the pattern defined by the provided path.
*/
public getMatchingSymbolsKeys(path: string[]): string[] {
const wildcardRegex = new RegExp(
`^${path
.map((segment) => {
if (segment.includes('*')) {
return `${segment.split('*')[0]}([^.]+)?`;
}
return segment;
})
.join('\\.')}$`
);
return Object.keys(this.#table)
.filter((key) => wildcardRegex.test(key))
.filter(filterOutNullable);
}
}
function isObjectSymbol(symbol: GenericSymbolDef): symbol is ObjectSymbolDef {
return symbol.type === SymbolType.Object;
}
function isSymbolWithProperties(symbol: GenericSymbolDef): symbol is SymbolWithProperties {
return (
symbol.type === SymbolType.Object ||
symbol.type === SymbolType.Array ||
symbol.type === SymbolType.String
);
}
function isSymbolWithMethods(symbol: GenericSymbolDef): symbol is SymbolWithMethods {
return (
symbol.type === SymbolType.Object ||
symbol.type === SymbolType.Array ||
symbol.type === SymbolType.String
);
}
function typedSymbolInfo<T extends SymbolType>(info: SymbolInfo): SymbolInfo<T> {
const definition = resolveSymbolDef(info.definition);
return { ...info, definition } as SymbolInfo<T>;
}
+380
View File
@@ -0,0 +1,380 @@
import {
type ArraySymbolDef,
type BooleanSymbolDef,
type ExtractSymbolDef,
type FunctionSymbolDef,
type GenericSymbolDef,
type NullSymbolDef,
type NumberSymbolDef,
type ObjectSymbolDef,
type StringSymbolDef,
SymbolType,
type SymbolsWithPropertiesAndMethods,
type UndefinedSymbolDef,
type UnionSymbolDef,
} from './types';
export function SymbolBoolean(args: Omit<BooleanSymbolDef, 'type'> = {}): BooleanSymbolDef {
return {
type: SymbolType.Boolean,
...args,
};
}
export function SymbolNumber(args: Omit<NumberSymbolDef, 'type'> = {}): NumberSymbolDef {
return {
type: SymbolType.Number,
...args,
};
}
export function SymbolString(
args: Omit<StringSymbolDef, 'type' | 'methods' | 'properties'> = {}
): StringSymbolDef {
return createSymbolWithPropertiesAndMethods<StringSymbolDef>(SymbolType.String, args);
}
export function SymbolObject(args: Omit<ObjectSymbolDef, 'type'>): ObjectSymbolDef {
return {
type: SymbolType.Object,
...args,
};
}
export function SymbolArray(
args: Omit<ArraySymbolDef, 'type' | 'methods' | 'properties'>
): ArraySymbolDef {
return createSymbolWithPropertiesAndMethods(SymbolType.Array, args);
}
export function SymbolFunction(args: Omit<FunctionSymbolDef, 'type'>): FunctionSymbolDef {
return {
type: SymbolType.Function,
...args,
};
}
export function OptionalFunctionArg(
optionalArg: ExtractSymbolDef<SymbolType>
): ExtractSymbolDef<SymbolType> & { optional: true } {
return {
...optionalArg,
optional: true,
};
}
export function SymbolUnion(args: Omit<UnionSymbolDef, 'type'>): UnionSymbolDef {
return {
type: SymbolType.Union,
...args,
};
}
export function SymbolUndefined(args: Omit<UndefinedSymbolDef, 'type'> = {}): UndefinedSymbolDef {
return {
type: SymbolType.Undefined,
...args,
};
}
export function SymbolNull(args: Omit<NullSymbolDef, 'type'> = {}): NullSymbolDef {
return {
type: SymbolType.Null,
...args,
};
}
function createSymbolWithPropertiesAndMethods<
T extends SymbolsWithPropertiesAndMethods & { type: SymbolType },
>(type: T['type'], args: Omit<T, 'type' | 'methods' | 'properties'>): T {
const symbol = { type, ...args } as T;
Object.defineProperty(symbol, 'properties', {
get() {
if (symbol.type === SymbolType.Array) {
return isArraySymbol(symbol)
? StandardLibrary[SymbolType.Array]?.(symbol).properties
: {};
}
return StandardLibrary[symbol.type]?.properties || {};
},
});
Object.defineProperty(symbol, 'methods', {
get() {
if (symbol.type === SymbolType.Array) {
return isArraySymbol(symbol)
? StandardLibrary[SymbolType.Array]?.(symbol).methods
: [];
}
return StandardLibrary[symbol.type]?.methods || [];
},
});
return symbol;
}
// TODO-ADAPTIVE-CONTENT: extend the definition of the supported standard library methods and properties.
const StandardLibrary: Partial<
{
[key in Exclude<SymbolType, SymbolType.Array>]: {
properties: Record<string, GenericSymbolDef>;
methods: FunctionSymbolDef[];
};
} & {
[SymbolType.Array]: (symbol: ArraySymbolDef) => {
properties: Record<string, GenericSymbolDef>;
methods: FunctionSymbolDef[];
};
}
> = {
[SymbolType.String]: {
properties: {
length: SymbolNumber({
name: 'length',
description:
'The length data property of a String value contains the length of the string in UTF-16 code units.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length',
}),
},
methods: [
SymbolFunction({
name: 'at',
description: `Takes an integer value and returns the item at that index, allowing for positive and negative integers.
Negative integers count back from the last item in the string.`,
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at',
args: [
SymbolNumber({
name: 'index',
description: 'The index (position) of the string character to be returned',
}),
],
returns: SymbolUnion({
description: `A String consisting of the single UTF-16 code unit located at the specified position.
Returns undefined if the given index can not be found.`,
members: [SymbolString(), SymbolUndefined()],
}),
}),
SymbolFunction({
name: 'startsWith',
description: `Returns true if the given characters are found at the beginning of the string, including when searchString
is an empty string. Otherwise returns false.`,
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith',
args: [
SymbolString({
name: 'searchString',
description: `The characters to be searched for at the start of this string. Cannot be a regex.
All values that are not regexes are coerced to strings, so omitting it or passing undefined causes startsWith() to search for
the string "undefined", which is rarely what you want.`,
}),
OptionalFunctionArg(
SymbolNumber({
name: 'position',
description: `The start position at which searchString is expected to be found
(the index of searchString's first character). Defaults to 0.`,
})
),
],
returns: SymbolBoolean({
description: `true if the given characters are found at the beginning of the string, including when searchString is an empty string;
otherwise, false.`,
}),
}),
SymbolFunction({
name: 'endsWith',
description: `Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding
elements of this object (converted to a String) starting at endPosition length(this). Otherwise returns false.`,
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith',
args: [
SymbolString({
name: 'searchString',
description: `The characters to be searched for at the end of str. Cannot be a regex.
All values that are not regexes are coerced to strings, so omitting it or passing undefined causes endsWith() to search for
the string "undefined", which is rarely what you want.`,
}),
OptionalFunctionArg(
SymbolNumber({
name: 'endPosition',
description: `The end position at which searchString is expected to be found
(the index of searchString's last character plus 1). Defaults to str.length.`,
})
),
],
returns: SymbolBoolean({
description: `true if the given characters are found at the end of the string, including when searchString is an empty string;
otherwise, false.`,
}),
}),
SymbolFunction({
name: 'includes',
description: `Returns true if searchString appears as a substring of the result of converting this object to a String, at one or more positions
that are greater than or equal to position; otherwise, returns false.`,
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes',
args: [
SymbolString({
name: 'searchString',
description: `A string to be searched for within str. Cannot be a regex. All values that are not regexes are coerced to strings, so omitting it
or passing undefined causes includes() to search for the string "undefined", which is rarely what you want.`,
}),
OptionalFunctionArg(
SymbolNumber({
name: 'position',
description:
'The position within the string at which to begin searching for searchString. (Defaults to 0.)',
})
),
],
returns: SymbolBoolean({
description: `true if the search string is found anywhere within the given string, including when searchString is an empty string;
otherwise, false.`,
}),
}),
SymbolFunction({
name: 'toLowerCase',
description:
'Returns the value of the string converted to lower case. toLowerCase() does not affect the value of the string str itself.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase',
args: [],
returns: SymbolString({
description:
'A new string representing the calling string converted to lower case.',
}),
}),
SymbolFunction({
name: 'toUpperCase',
description:
'Returns the value of the string converted to uppercase. toUpperCase() does not affect the value of the string str itself.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase',
args: [],
returns: SymbolString({
description:
'A new string representing the calling string converted to upper case.',
}),
}),
SymbolFunction({
name: 'trim',
description:
'Returns new string representing str stripped of whitespace from both its beginning and end.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim',
args: [],
returns: SymbolString({
description:
'A new string representing str stripped of whitespace from both its beginning and end.',
}),
}),
],
},
[SymbolType.Array]: (arraySymbolDef: ArraySymbolDef) => ({
properties: {
length: SymbolNumber({
name: 'length',
description: `The length data property of an Array instance represents the number of elements in that array.
The value is an unsigned, 32-bit integer that is always numerically greater than the highest index in the array.`,
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length',
}),
},
methods: [
SymbolFunction({
name: 'at',
description: `Takes an integer value and returns the item at that index, allowing for positive and negative integers.
Negative integers count back from the last item in the array.`,
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at',
args: [
SymbolNumber({
name: 'index',
description: `Zero-based index of the array element to be returned, converted to an integer.
Negative index counts back from the end of the array — if index < 0, index + array.length is accessed.`,
}),
],
returns: SymbolUnion({
description: `The element in the array matching the given index. Always returns undefined if index < -array.length or index >= array.length
without attempting to access the corresponding property.`,
members: [arraySymbolDef.items, SymbolUndefined()],
}),
}),
SymbolFunction({
name: 'includes',
description:
'Determines whether an array includes a certain value among its entries, returning true or false as appropriate.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes',
args: [
{
...arraySymbolDef.items,
name: 'searchElement',
description: 'The value to be searched for within the array.',
},
OptionalFunctionArg(
SymbolNumber({
name: 'fromIndex',
description:
'The position within the string at which to begin searching for searchString. (Defaults to 0.)',
})
),
],
returns: SymbolBoolean({
description:
'true if the value searchElement is found within the array (or the part of the array indicated by the index fromIndex, if specified).',
}),
}),
SymbolFunction({
name: 'some',
description:
'Tests whether at least one element in the array passes the test implemented by the provided function.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some',
args: [
SymbolFunction({
name: 'callback',
description: 'A function that tests each element of the array.',
args: [
{
...arraySymbolDef.items,
name: 'element',
description: 'The current element being processed in the array.',
},
],
returns: SymbolBoolean({
description:
'true if the callback function returns a truthy value for at least one element in the array.',
}),
}),
],
returns: SymbolBoolean({
description:
'true if the callback function returns a truthy value for at least one element in the array.',
}),
}),
SymbolFunction({
name: 'every',
description:
'Tests whether all elements in the array pass the test implemented by the provided function.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every',
args: [
SymbolFunction({
name: 'callback',
description: 'A function that tests each element of the array.',
args: [
{
...arraySymbolDef.items,
name: 'element',
description: 'The current element being processed in the array.',
},
],
returns: SymbolBoolean({
description:
'true if the callback function returns a truthy value for all elements in the array.',
}),
}),
],
returns: SymbolBoolean({
description:
'true if the callback function returns a truthy value for all elements in the array.',
}),
}),
],
}),
};
function isArraySymbol(symbol: GenericSymbolDef): symbol is ArraySymbolDef {
return symbol.type === SymbolType.Array;
}
+193
View File
@@ -0,0 +1,193 @@
import type { MandateProps } from '../utils';
export enum SymbolType {
Boolean = 'boolean',
Number = 'number',
String = 'string',
Object = 'object',
Array = 'array',
Function = 'function',
Union = 'union',
Undefined = 'undefined',
Null = 'null',
}
export interface SymbolMetadata {
/**
* Long description of the symbol.
*/
description?: string;
/**
* Link to a documentation/manual page.
*/
link?: string;
}
export interface GenericSymbolDef extends SymbolMetadata {
/**
* Type of the symbol.
*/
type: SymbolType;
/**
* Name of the symbol.
*/
name?: string;
}
export interface SymbolWithProperties extends GenericSymbolDef {
/**
* Properties on the symbol type.
*/
properties: Record<string, GenericSymbolDef>;
}
export interface SymbolWithMethods extends GenericSymbolDef {
/**
* Methods that can be called on the symbol type.
*/
methods: FunctionSymbolDef[];
}
export interface BooleanSymbolDef extends GenericSymbolDef {
type: SymbolType.Boolean;
}
export interface NumberSymbolDef extends GenericSymbolDef {
type: SymbolType.Number;
}
export interface StringSymbolDef extends SymbolWithProperties, SymbolWithMethods {
type: SymbolType.String;
/**
* Set of enumerated values that the string symbol is retristred to.
*/
enum?: string[];
/**
* Properties on strings.
*/
properties: {
length: NumberSymbolDef;
};
}
export interface ObjectSymbolDef extends SymbolWithProperties, SymbolWithMethods {
type: SymbolType.Object;
}
export interface ArraySymbolDef extends SymbolWithProperties, SymbolWithMethods {
type: SymbolType.Array;
/**
* Symbol representing the type of the items of the array
*/
items: ExtractSymbolDef<SymbolType>;
/**
* Properties on arrays.
*/
properties: {
length: NumberSymbolDef;
};
}
export interface FunctionSymbolDef extends MandateProps<GenericSymbolDef, 'name'> {
type: SymbolType.Function;
/**
* Symbols describing the arguments of the function.
*/
args: (ExtractSymbolDef<SymbolType> & { optional?: boolean })[];
/**
* Symbol describing the returned value of the function.
*/
returns: ExtractSymbolDef<SymbolType>;
}
export interface UnionSymbolDef extends GenericSymbolDef {
type: SymbolType.Union;
/**
* Symbols composing the union.
*/
members: ExtractSymbolDef<SymbolType>[];
}
export interface UndefinedSymbolDef extends GenericSymbolDef {
type: SymbolType.Undefined;
}
export interface NullSymbolDef extends GenericSymbolDef {
type: SymbolType.Null;
}
export type SymbolsWithPropertiesAndMethods = ArraySymbolDef | ObjectSymbolDef | StringSymbolDef;
export type ExtractSymbolDef<T extends SymbolType> = T extends SymbolType.String
? StringSymbolDef
: T extends SymbolType.Number
? NumberSymbolDef
: T extends SymbolType.Boolean
? BooleanSymbolDef
: T extends SymbolType.Array
? ArraySymbolDef
: T extends SymbolType.Object
? ObjectSymbolDef
: T extends SymbolType.Function
? FunctionSymbolDef
: T extends SymbolType.Union
? UnionSymbolDef
: T extends SymbolType.Undefined
? UndefinedSymbolDef
: T extends SymbolType.Null
? NullSymbolDef
: never;
export function resolveSymbolDef(
symbol: GenericSymbolDef
):
| StringSymbolDef
| NumberSymbolDef
| BooleanSymbolDef
| ArraySymbolDef
| ObjectSymbolDef
| FunctionSymbolDef
| UnionSymbolDef
| UndefinedSymbolDef
| NullSymbolDef {
switch (symbol.type) {
case SymbolType.String:
return symbol as StringSymbolDef;
case SymbolType.Number:
return symbol as NumberSymbolDef;
case SymbolType.Boolean:
return symbol as BooleanSymbolDef;
case SymbolType.Array:
return symbol as ArraySymbolDef;
case SymbolType.Object:
return symbol as ObjectSymbolDef;
case SymbolType.Function:
return symbol as FunctionSymbolDef;
case SymbolType.Union:
return symbol as UnionSymbolDef;
case SymbolType.Undefined:
return symbol as UndefinedSymbolDef;
case SymbolType.Null:
return symbol as NullSymbolDef;
default:
throw new Error(`Unknown symbol type: ${symbol.type}`);
}
}
+57
View File
@@ -0,0 +1,57 @@
export type TemplateText = {
type: 'text';
value: string;
start: number;
end: number;
};
export type TemplateExpression = {
type: 'expression';
value: string;
start: number; // Start index of the expression content (after `{{`)
end: number; // End index of the expression content (before `}}`)
};
export type TemplatePart = TemplateText | TemplateExpression;
/**
* Parse a template string containing `{{ expression }}` placeholders.
*/
export function parseTemplate(template: string): TemplatePart[] {
const parts: TemplatePart[] = [];
const regex = /\{\{(.*?)\}\}/gs;
let lastIndex = 0;
for (const match of template.matchAll(regex)) {
const matchStart = match.index ?? 0;
const matchEnd = matchStart + match[0].length;
if (matchStart > lastIndex) {
parts.push({
type: 'text',
value: template.slice(lastIndex, matchStart),
start: lastIndex,
end: matchStart,
});
}
parts.push({
type: 'expression',
value: (match[1] ?? '').trim(),
start: matchStart + 2,
end: matchEnd - 2,
});
lastIndex = matchEnd;
}
if (lastIndex < template.length) {
parts.push({
type: 'text',
value: template.slice(lastIndex),
start: lastIndex,
end: template.length,
});
}
return parts;
}
+176
View File
@@ -0,0 +1,176 @@
import type { BinaryOperator, Expression, ExpressionStatement, LogicalOperator } from 'acorn';
import type { ArraySymbolDef, SymbolInfo, SymbolType } from './symbols';
export interface ExpressionGenerator {
/**
* Converts an ESTree compatible AST node into a string representing the corresponding expression.
*/
generate(node: Expression): string;
}
export interface ExpressionParserResult {
/**
* The expression statement from the valid portion of the parsed expression.
*
* It is undefined when no valid expression statements could be found.
*/
result: Expression;
/**
* The information of the invalid (non-expression) nodes found from the other portions of the parsed expression.
*/
invalidNodes: ExpressionStatement[];
}
export interface ExpressionAutocompleteResults {
suggestions: AutocompleteSuggestions;
}
type ConditionalOperator = '?';
export const SUPPORTED_BINARY_OPERATORS = [
{
operator: '==',
description: 'Checks whether two values are equal.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality',
},
{
operator: '!=',
description: 'Checks whether two values are unequal.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality',
},
{
operator: '===',
description: 'Checks whether two values are equal (strict comparison).',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality',
},
{
operator: '!==',
description: 'Checks whether two values are unequal (strict comparison).',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_inequality',
},
{
operator: '<',
description: 'Checks if the left value is less than the right value.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Less_than',
},
{
operator: '<=',
description: 'Checks if the left value is less than or equal to the right value.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Less_than_or_equal',
},
{
operator: '>',
description: 'Checks if the left value is greater than the right value.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Greater_than',
},
{
operator: '>=',
description: 'Checks if the left value is greater than or equal to the right value.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Greater_than_or_equal',
},
{
operator: 'in',
description: 'Checks if a property exists in an object or if a value is in an array.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/in',
},
] as const;
export const SUPPORTED_LOGICAL_OPERATORS = [
{
operator: '&&',
description: 'Logical AND operator; returns true if both operands are true.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND',
},
{
operator: '||',
description: 'Logical OR operator; returns true if at least one operand is true.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR',
},
] as const;
export const SUPPORTED_CONDITIONAL_OPERATORS = [
{
operator: '?',
description:
'Conditional (ternary) operator; returns one of two values based on a condition.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator',
},
] as const;
type DirectLiteralValue = {
kind: 'direct';
} & (
| {
type: SymbolType.Boolean;
data: boolean;
}
| {
type: SymbolType.Number;
data: number;
}
| {
type: SymbolType.String;
data: string;
}
| {
type: SymbolType.Null;
data: null;
}
);
type InArrayLiteralValue = {
kind: 'in-array';
srcSymbol: ArraySymbolDef;
matchedLiteralString: string;
};
export type DirectLiteralValueSuggestion = {
type: 'literal-value';
value: DirectLiteralValue;
};
export type InArrayLiteralValueSuggestion = {
type: 'literal-value';
value: InArrayLiteralValue;
};
export type AutocompleteLiteralValueSuggestion =
| DirectLiteralValueSuggestion
| InArrayLiteralValueSuggestion;
export interface AutocompleteOperatorSuggestion {
type: 'operator';
operator:
| Extract<BinaryOperator, (typeof SUPPORTED_BINARY_OPERATORS)[number]['operator']>
| Extract<LogicalOperator, (typeof SUPPORTED_LOGICAL_OPERATORS)[number]['operator']>
| Extract<
ConditionalOperator,
(typeof SUPPORTED_CONDITIONAL_OPERATORS)[number]['operator']
>;
description: string;
link: string;
}
export interface AutocompleteSymbolSuggestion {
type: 'symbol';
symbol: SymbolInfo;
}
export type AutocompleteSuggestions = (
| AutocompleteSymbolSuggestion
| AutocompleteLiteralValueSuggestion
| AutocompleteOperatorSuggestion
)[];
type LoggerFn = (message: string, ...args: any[]) => void;
/**
* A logger that can be passed to the runtime.
*/
export interface Logger {
debug: LoggerFn;
info: LoggerFn;
error: LoggerFn;
}
+40
View File
@@ -0,0 +1,40 @@
/**
* Format the result value of an expression for display as a string.
*/
export function formatExpressionResult(value: any, defaultValue = ''): string {
if (value === undefined || value === null) {
return defaultValue;
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return value.toString();
}
return defaultValue;
}
/**
* Filter function to exclude `null` values
*/
export function filterOutNullable<T>(value: T): value is NonNullable<T> {
return !!value;
}
/**
* Type to make optional properties on a object mandatory.
*
* interface SomeObject {
* uid: string;
* price: number | null;
* location?: string;
* }
*
* type ValuableObject = MandateProps<SomeObject, 'price' | 'location'>;
*/
export type MandateProps<T extends {}, K extends keyof T> = T & {
[MK in K]-?: NonNullable<T[MK]>;
};
+18
View File
@@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/node20/tsconfig.json"],
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"module": "ESNext",
"moduleResolution": "bundler",
"isolatedModules": true,
"incremental": true,
"noEmit": true,
"noPropertyAccessFromIndexSignature": false,
"exactOptionalPropertyTypes": false,
"types": [
"bun-types" // add Bun global
]
},
"include": ["types/**/*.d.ts", "src/**/*.ts"]
}
+67
View File
@@ -0,0 +1,67 @@
declare module 'eval-estree-expression' {
/**
* Options for evaluation and compilation.
*/
export interface EvalESTreeExpressionOptions {
/**
* Force logical operators to return a boolean result. Default: undefined
*/
booleanLogicalOperators?: boolean;
/**
* Allow function calls to be evaluated. This is unsafe, please enable this option at your own risk. Default: false
*/
functions?: boolean;
/**
* Enable support for function statements and expressions by enabling the functions option AND by passing the .generate() function from the escodegen library. Default: undefined
*/
generate?: boolean | ((node: any) => string);
/**
* Enable the =~ regex operator to support testing values without using functions (example name =~ /^a.*c$/). Default: true
*/
regexOperator?: boolean;
/**
* Throw an error when variables are undefined. Default: false
*/
strict?: boolean;
/**
* Used with the variables method to return nested variables (e.g., variables with dot notation, like foo.bar.baz). Default: undefined
*/
withMembers?: boolean;
}
/**
* Evaluates an ESTree expression asynchronously against a given context.
* @param expression - An object representing an ESTree-compliant AST node.
* @param context - An object containing variables and values to be used during evaluation.
* @returns A promise resolving to the result of the evaluation.
*/
export function evaluate<ASTNode>(
ast: ASTNode,
context: object,
options?: EvalESTreeExpressionOptions
): Promise<any>;
/**
* Given an ESTree-compliant AST node, returns a list of variables used in the expression.
* @param ast An object representing an ESTree-compliant AST node.
* @param options Options for evaluation and compilation.
*/
export function variables<ASTNode>(
ast: ASTNode,
options?: EvalESTreeExpressionOptions
): string[];
/**
* Evaluates an ESTree expression synchronously against a given context.
* @param expression - An object representing an ESTree-compliant AST node.
* @param context - An object containing variables and values to be used during evaluation.
* @returns The result of the evaluation.
*/
export namespace evaluate {
function sync<ASTNode>(
expression: ASTNode,
context: object,
options?: EvalESTreeExpressionOptions
): any;
}
}
+2
View File
@@ -0,0 +1,2 @@
dist/
src/data/*.json
+25
View File
@@ -0,0 +1,25 @@
# @gitbook/fonts
## 0.1.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.1.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 0.1.1
### Patch Changes
- 295f03d: Republish packages
## 0.1.0
### Minor Changes
- fbfcca5: Initial version of the package

Some files were not shown because too many files have changed in this diff Show More