mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-11-12 14:48:29 +00:00
Compare commits
249 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91ec127e01 | |||
| 7d1cd9f9df | |||
| 6029a31c07 | |||
| 4fb7e14436 | |||
| 31e88d16eb | |||
| 7ddd023d72 | |||
| b53e73c0e7 | |||
| 4a405475f3 | |||
| aade3189ca | |||
| 8689736e3a | |||
| 1b29a7051e | |||
| 6c128729b2 | |||
| 6344646974 | |||
| 192863526f | |||
| 09a8310c9d | |||
| a023a7131b | |||
| 198cafe978 | |||
| 268fa83683 | |||
| 123454d2ac | |||
| e6b8502036 | |||
| 3837c484a6 | |||
| 4dfe630a70 | |||
| 26ec43e870 | |||
| 063e6851e7 | |||
| 04f39f1472 | |||
| 1383a798cb | |||
| 4a72ead12a | |||
| ea018507c7 | |||
| 347a1cc006 | |||
| a512777351 | |||
| 508f1bc132 | |||
| 38f60f2633 | |||
| be658e1b32 | |||
| e745c88166 | |||
| f61578f01f | |||
| a5d58bc210 | |||
| 7e096d5131 | |||
| bd431f1f8d | |||
| b7b8ae663b | |||
| 96c568393e | |||
| 96f8ccc5cd | |||
| c00a7b3b88 | |||
| 3d83277b61 | |||
| d053984d34 | |||
| 02f4cbc779 | |||
| 9f22456eef | |||
| 3370103307 | |||
| b0cb03558d | |||
| fce8a6f2b6 | |||
| 5f38ae6f9f | |||
| fc06bc694b | |||
| a17ed29e35 | |||
| bb47dadfbe | |||
| bfe45fede4 | |||
| 9f940678b6 | |||
| 96626fea7c | |||
| d839d9dc57 | |||
| 30fa152f0f | |||
| 0540ebc1b9 | |||
| 0b515b0970 | |||
| 65acba0b61 | |||
| f7dc7faa65 | |||
| 03aa0f75f5 | |||
| a625112106 | |||
| 1bf3bd4294 | |||
| 9d62cb4bad | |||
| 8bc381d852 | |||
| a22923b56b | |||
| a73800e0ef | |||
| a0c1bdf58e | |||
| d685660e1d | |||
| f1d5123c0b | |||
| 679ea0706c | |||
| c366796756 | |||
| f35be52c80 | |||
| 848a39d1c8 | |||
| 9f447aab29 | |||
| 552538a0d9 | |||
| 5c4a808ba8 | |||
| ad6262b6f9 | |||
| 2137c2df7b | |||
| 990e702035 | |||
| 3ed448fed7 | |||
| 4856a1f744 | |||
| ac843657ec | |||
| b1102fcebe | |||
| 7996c362c1 | |||
| c7c1b79788 | |||
| dff0c7903e | |||
| ffebd1790e | |||
| 0e0085e49a | |||
| 8e131693ea | |||
| 96325161be | |||
| 9f5290bccc | |||
| 61992424af | |||
| 380af10236 | |||
| 0911abc55c | |||
| d8dc59b1a9 | |||
| d04f8bc05e | |||
| 576a781a8f | |||
| df2841cf36 | |||
| 70bfdda39c | |||
| afbc57c3e0 | |||
| e4638f1833 | |||
| d7f2addb03 | |||
| 7f8ab7dcc7 | |||
| 8dd9444596 | |||
| bdc2497771 | |||
| 581abe0bb7 | |||
| 4503be2f19 | |||
| bdfc98b97e | |||
| 0f0cfde6ba | |||
| 357af3f6d2 | |||
| b95add7b27 | |||
| 99984ebbb3 | |||
| a52daf8b15 | |||
| f80429e4e8 | |||
| 6b4c453497 | |||
| c7d042a733 | |||
| e94a5085eb | |||
| 3ae1524a88 | |||
| b967b248e7 | |||
| a9e6814744 | |||
| 5725da19e2 | |||
| 05cff0a03b | |||
| 1e8c34a426 | |||
| cd881f9ac1 | |||
| 39316fc205 | |||
| c08d05ade5 | |||
| b9453a92f8 | |||
| 2c4d40ad97 | |||
| 688515efcf | |||
| 931cbe717e | |||
| 3ef1802e7e | |||
| 8a0e0df84e | |||
| a9041a309a | |||
| 625c108196 | |||
| 8e87856501 | |||
| 9b822abbb6 | |||
| 81dba6455b | |||
| 1faa57b812 | |||
| 40150f0a6c | |||
| bf29570bd5 | |||
| e8e979b6c0 | |||
| 1f250d1fa8 | |||
| 46b3a18b25 | |||
| 2ff77e15b5 | |||
| d7c867aa63 | |||
| bcd1c41aa1 | |||
| 77058e3f17 | |||
| 8ad7465801 | |||
| 1b7ac0e41b | |||
| 5f88f4e680 | |||
| 7009bb9311 | |||
| e460ec381e | |||
| 0a82628298 | |||
| 18ff11b68b | |||
| 3bf55cef10 | |||
| e041e49f11 | |||
| c1b5be05eb | |||
| 7543eb83ad | |||
| 523b7cd4b6 | |||
| af698a49f5 | |||
| db67585ee2 | |||
| a566153d98 | |||
| 530f98a9c4 | |||
| 39391259c8 | |||
| 35be3835f3 | |||
| 4452fd793c | |||
| 90566879d2 | |||
| 40a879ad5f | |||
| cc441c99c6 | |||
| 64e143a15b | |||
| 8c10d92f77 | |||
| 0a22eb5340 | |||
| d9fc4608e3 | |||
| ded2f560e3 | |||
| a9c5d1b546 | |||
| 0ff21b7ae2 | |||
| 94a496c561 | |||
| 148a43ac01 | |||
| 5da854f09c | |||
| b772f74c2b | |||
| e0bd04f641 | |||
| 8d7c3edda8 | |||
| 80951e9889 | |||
| 185dd8337e | |||
| da7fb13d83 | |||
| 49c993f181 | |||
| 90b54682e5 | |||
| 1dccf8f06c | |||
| 810244e8a9 | |||
| 0dd2f4fbcd | |||
| 8a700222e6 | |||
| b13fd91afc | |||
| 8a6baaba6a | |||
| 48fba7cb49 | |||
| 6928a9b00a | |||
| 7bab574c63 | |||
| 75bdff3ff3 | |||
| 56c25587db | |||
| cf4efc7213 | |||
| 1ef71609e7 | |||
| 195c9e6b84 | |||
| 09f39f8300 | |||
| 9002f6598a | |||
| 01c9b059c6 | |||
| 6d02b8ab72 | |||
| 177ef8582a | |||
| 78c589ffba | |||
| e4b214e6bd | |||
| 64ce9e180d | |||
| c64d3a50e8 | |||
| fd070ce9ca | |||
| 048c4e4c70 | |||
| fb01dc9ecb | |||
| d33e570bc7 | |||
| 87fd234d55 | |||
| 0f32eb17d1 | |||
| 3e29680792 | |||
| b3db1c58c8 | |||
| 0f994016f4 | |||
| 419094b79d | |||
| 95d23775ce | |||
| aa98859560 | |||
| ea19801cfa | |||
| 13059b843c | |||
| 3db14ad950 | |||
| 5fac9e37ee | |||
| 1424c566a0 | |||
| 65f99eafe5 | |||
| 7594a334d8 | |||
| 827e4f9856 | |||
| a9a1a609c5 | |||
| 61405a3036 | |||
| 634a24ff03 | |||
| 2ccd43ee5e | |||
| cd506e3f79 | |||
| a6644074cb | |||
| 4be043d96e | |||
| 673f4b6076 | |||
| d0a63bab91 | |||
| ca9a1dd396 | |||
| 996d7ec021 | |||
| f9ad9b5356 | |||
| 332089eca9 | |||
| 6ac4cf4b76 | |||
| 5c74eef602 | |||
| 1ee1853995 |
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: write-pr-description
|
||||
description: Write a concise, product-focused PR description from the changes and available discussion. Use when asked for a PR summary or description.
|
||||
---
|
||||
|
||||
# Write a PR description
|
||||
|
||||
Read the full branch diff and commit history against the PR's base branch, not just the latest commit. Use the PR discussion and linked issues for context when available. The PR's changes are the source of truth for what ships if an issue describes something different.
|
||||
|
||||
Write for contributors and reviewers who may not know the area. Lead with what changes, prefer user or product impact when supported, and include implementation details only when they help review. Keep the proposed changes short, using bullets only when useful.
|
||||
|
||||
Include relevant validation and screenshots when available. Do not claim checks passed unless they ran. Use only information appropriate for a public repository; do not copy private issue details or customer information into the description.
|
||||
|
||||
## Template and changelog
|
||||
|
||||
Read `.github/pull_request_template.md` immediately before writing. Preserve its required section names and order. Include `Proposed changes` and `Changelog`; omit `Demo` when there is nothing to show. Add optional `Context` before `Changelog` only when it meets the rule below.
|
||||
|
||||
Write a short changelog entry describing what shipped. Use `[Feature]` for user-facing additions or improvements, `[Fix]` for user-facing bug fixes, and `[Chore]` for internal maintenance. For features and fixes, say what users can do or what no longer goes wrong in plain words. Keep chores useful to teammates without inventing user impact. Remove unused examples and placeholders. Usually one entry is enough; use more only for distinct changes. The PR changelog does not replace any changeset required by `AGENTS.md`.
|
||||
|
||||
## Optional context
|
||||
|
||||
Write `Proposed changes` and `Changelog` first. Add `Context` only when it tells someone unfamiliar with the area something material that the changelog does not. `Problem` names a concrete failure or limitation supported by the PR or discussion. `Outcome` names what this PR delivers at its actual scope. Omit the section if either field would repeat the changelog, describe only implementation, or guess at an impact.
|
||||
|
||||
```markdown
|
||||
## Context
|
||||
|
||||
**Problem:** <concrete failure or limitation>
|
||||
|
||||
**Outcome:** <result delivered by this PR>
|
||||
```
|
||||
|
||||
Keep each field to 1–2 short, non-technical sentences. Prefer user or product impact when supported. For infrastructure changes, explain the failure or limitation they caused. Do not invent an actor, customer segment, ownership, or impact, or force the change into a user story. Do not turn a preparatory change into a claim that the whole workflow is fixed. Keep the roles distinct: `Context` explains why this matters, `Changelog` says what shipped, and `Proposed changes` can explain implementation.
|
||||
|
||||
Examples:
|
||||
|
||||
- A dependency bump with no demonstrated user effect needs no `Context`.
|
||||
- If creating the first change request leaves the user on an empty screen, say that it now opens after creation. Leave the animation-listener details in `Proposed changes`.
|
||||
- If a PR indexes links for later checks, describe the indexing result. Do not claim every link has already been checked.
|
||||
|
||||
## Writing pass
|
||||
|
||||
- Use plain words and direct verbs: "use" instead of "leverage", "help" instead of "facilitate". Prefer active voice when the actor is known.
|
||||
- Name the concrete behavior instead of saying "improves the experience" or "makes it more robust". Support performance claims with measurements.
|
||||
- Cut filler, promotional language, and generic conclusions. If a sentence could fit almost any PR, make it specific or delete it.
|
||||
- Keep one idea per sentence and use consistent names. Remove redundant hedging without hiding real uncertainty.
|
||||
- Avoid "not just X, but Y", forced groups of three, em dashes, decorative emojis, and unnecessary bold text.
|
||||
|
||||
Return the description in a Markdown code block unless asked to update the PR. When asked to update it, apply the description with `gh pr edit` using a body file.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Force software rendering (SwiftShader) in Playwright Chromium to eliminate image downscaling drift between GPU-equipped local runs and headless CI runs in Argos screenshots.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add an assistant tool to submit feedback about the current page on behalf of the user.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Assistant: you can now send follow-up questions while an answer is still being written. Each one appears as your own message with a "Queued" badge (hover for when it will send, × to cancel), and they're sent automatically one at a time as each answer completes.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add a carousel layout option to cards blocks, rendering them as a horizontally-scrolling, scroll-snapping row instead of a wrapping grid.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Assistant: the "Explored briefly" activity heading no longer appears when there's nothing to show. It now renders only when the answer is preceded by a real preamble or one or more tool calls, so a simple answer with an empty reasoning step no longer surfaces an empty collapsible.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix the docs embed `navigateToPage` API on multi-space sites. Deep-linking to a page in a different space/section (e.g. `navigateToPage('/help-center/integrations')`) previously 404'd because the section base was not placed before `~gitbook/embed/page`. The target is now resolved to its space server-side, so pages in any space resolve correctly. The input accepts the page path, an absolute path, or the full published URL.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add configurable default visibility to Prompt block
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@gitbook/colors": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
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.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@gitbook/openapi-parser": patch
|
||||
"@gitbook/react-openapi": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add missing link reference to OpenAPI models
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add cover image background mode and masks
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Button blocks now respect the `size` option, so you can render small, medium, or large buttons.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@gitbook/browser-types": patch
|
||||
"@gitbook/embed": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
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`).
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add an `askQuestion` tool to the site MCP server. Alongside `searchDocumentation` and `getPage`, MCP clients can now ask a natural-language question and get a synthesized answer with links to the source pages, powered by the same AI search backend as the site's "ask a question" experience. The tool accepts an optional `goal` param so calling agents can attach the intent they're trying to accomplish, which tailors the answer and is tracked in analytics. The tool is only exposed on sites that have AI enabled.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Submit `sendFeedback` MCP tool findings through the dedicated `submitSiteAgentFeedback` API endpoint. The `pageUrl` is now required and an optional `goal` can be provided.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Serve `X-Robots-Tag: noindex` on internal search/assistant URLs (`?q=` / `?ask=`) and stop disallowing them in robots.txt, so Google can crawl the directive and drop them from the index instead of reporting "Indexed, though blocked by robots.txt".
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add an "On this page" table of contents on OpenAPI models pages. Each model in a grouped/multi-model "Models" section is now listed as its own section, matching operations and webhooks.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Serve the OpenAPI "Test it" request proxy from GitBook's own domain.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Remove the ChatGPT-specific plain text response for Markdown and llms.txt routes; these now always serve `text/markdown`.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Remove GBO's redundant re-selection of the best-scoring search section. The search API now returns a single highest-scoring section per page (and orders sections highest-score-first), so GBO no longer needs its own `getBestScoredResult` helper to pick the best section for the search and MCP previews. No user-visible change.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Sync the API reference responses selector with the "Responses" collapsibles, and keep the selected response in sync across every operation on the page (like the code sample language selector). Selecting a status code now expands the matching response section and applies to all operations at once.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix a light/dark flash on published sites configured to respect the system default (no theme toggle). Such sites forced the `system` theme, but `next-themes`' pre-paint script applies a forced value verbatim without resolving `prefers-color-scheme`, so the page painted light and only switched to dark after hydration. We now leave the theme unforced when the default is `system` (only concrete light/dark themes are forced), letting `next-themes`' existing pre-paint script resolve the system preference before first paint.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add a `sendFeedback` MCP tool so AI agents can report documentation findings (outdated / incoherent / gap / other) as `agent_feedback` insights events. The tool only accepts finding categories, so it never records positive feedback.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Separate the prompt block actions into a primary "Open in" dropdown and a secondary "Copy prompt" button, instead of a single combined button group, and align the block's design with the expandable block (bordered frame, left disclosure chevron, and subtle elevation when expanded).
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix the docs embed widget shipping a stale script: declare the embed package's `standalone/` bundle as a Turbo build output. Because it wasn't declared, changes confined to the standalone widget (which compiles to `standalone/` but not `dist/`) didn't invalidate the downstream `generate` cache that copies it into the app, so the deployed widget could lag the source — e.g. the `clipboard-write` permission on the widget iframe never reached production, breaking the copy button in the Assistant embed.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix ScrollContainer scroll buttons not reflecting content overflow immediately or after dynamic content changes (e.g. search results).
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Keep the last search query visible after closing search, and restore it when reopening, without breaking navigation when clicking a search result.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Refine the per-paragraph AI ask button: shorten its tooltip to "Ask" (from "Ask <assistant> about this"), and hide it inside cards where it would otherwise be clipped by the card's overflow.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Split the default-scope site search into two parallel API requests — one restricted to the current site space and one for the other site spaces — rendering each result set as soon as its response arrives. All results are ranked together by score, with the current site space scores boosted.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Keep the "On this page" and "Ask" buttons pinned below the header while scrolling on desktop API reference pages, so the page outline stays reachable throughout long operations.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Hide unfocusable unlabelled button from screen readers
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Only show the "Back to [space]" shortcut for cross-space links in the table of contents, not for in-content text links or other ways of reaching another space.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix heading anchor links being unreachable on touch devices by adding a tap-to-reveal state. The anchor icon now appears after the heading text without wrapping onto an orphan line while retaining its existing desktop placement. Use `pointerup` for the dismiss listener to fix unreliable dismissal on iOS Safari, and enlarge the anchor's touch tap target to a square 24px area (meeting the WCAG 2.5.8 minimum) so the icon stays centered instead of overflowing shorter headings' line height.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
a11y screen reader fixes
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@gitbook/react-contentkit": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Let integration block webframes navigate the reader to another page in the site by posting a `@webframe.navigate` action with a `path` (and optional `anchor`). Resolved client-side against the site base path, so navigation stays in-site and drives the standard navigation progress bar.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@gitbook/react-contentkit": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Expose the current page (`id`, `path`, `title`) to integration block webframes through the client-only webframe `state.page`, alongside adaptive visitor claims.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix an issue where certain keywords could cause an exception when rendering emojis
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/write-pr-description
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/write-pr-description
|
||||
@@ -0,0 +1,10 @@
|
||||
# Stop editors from looking for .editorconfig files in parent directories.
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
@@ -1,3 +0,0 @@
|
||||
# Changes to the API data cache functions can invalidate all existing data cache
|
||||
# causing a massive amount of revalidation, impacting our API.
|
||||
packages/gitbook/src/lib/data/api.ts @SamyPesse
|
||||
+47
-66
@@ -1,110 +1,91 @@
|
||||
# Welcome to GitBook's contributing guide!
|
||||
# Contributing to GitBook
|
||||
|
||||
> _For help, support, feature requests, and product questions - head to our [GitHub Community](https://github.com/orgs/GitbookIO/discussions) 🤖_
|
||||
> _For help, support and product questions, head to our [GitHub Community](https://github.com/orgs/GitbookIO/discussions)._
|
||||
|
||||
Thank you for investing your time in contributing to GitBook. Any contribution you make will be reviewed by our team. In this guide, you'll learn the different ways you can contribute.
|
||||
Thanks for helping make GitBook better. This repository holds the open source renderer behind every published GitBook site, and the team reads everything that comes in here.
|
||||
|
||||
## Types of Contributions
|
||||
## Ways to contribute
|
||||
|
||||
This repository contains code related to the rendering engine of GitBook's published content. Depending on what you'd like to contribute to, head to the section below to find the necessary steps.
|
||||
- **Open an issue.** Report a bug, suggest a feature, or tell us when something feels off. This is the most useful thing you can do.
|
||||
- **Join the conversation.** Share ideas and help others in [GitHub Community](https://github.com/orgs/GitbookIO/discussions).
|
||||
- **Build with GitBook.** Publish your docs, try new features early, and tell us what works and what doesn't.
|
||||
|
||||
### Add a feature
|
||||
## Issues instead of pull requests
|
||||
|
||||
Because this portion of GitBook is open source and available for you to use - if you think you can provide extra value through a new feature - you're welcome to add it! If you plan to distribute the code, keep the source code public to comply with GNU GPLv3. To clone in a private repository, you must first acquire a [commercial license](https://www.gitbook.com/pricing).
|
||||
Pull requests are limited to the GitBook team. If you've found a bug or want something to change, please [open an issue](https://github.com/GitbookIO/gitbook/issues/new/choose) instead.
|
||||
|
||||
### Create a new issue
|
||||
This isn't because we don't want your help. Every change here ships to every published GitBook site, so we need to own each one from start to finish. A clear issue often helps us more than a pull request: it tells us what's wrong and why, and lets us fix it in a way that fits the rest of the codebase.
|
||||
|
||||
If you spot a problem within a repository, [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). If a related issue doesn't exist, you can open a new issue here!Please make sure any added issues are
|
||||
A good issue includes:
|
||||
|
||||
- Descriptive
|
||||
- Thoughtful
|
||||
- Organized
|
||||
- What you expected to happen, and what happened instead
|
||||
- A link to a published GitBook site that shows the problem, if you have one
|
||||
- Steps to reproduce it, plus screenshots or a recording for anything visual
|
||||
- Your browser and OS, if it looks like it might matter
|
||||
|
||||
We recommend adding as many relevant links, minimal reproductions of the issue, and other materials that will help our team solve the issue fast.
|
||||
Before opening one, [search existing issues](https://github.com/GitbookIO/gitbook/issues). If someone has already reported it, add a 👍 or any extra details you have.
|
||||
|
||||
### Solve an issue
|
||||
Already fixed it in your fork? Link the branch or commit in your issue. We may open a pull request from it, and your commits keep your name.
|
||||
|
||||
If you're interested in solving an issue in our repository, start by scanning through it's exisiting issues to find one that you're interested in working on. If you find an issue to work on, you are welcome to open a PR with a fix. See the following sections below for more information on contributing for specific sections.
|
||||
If you open a pull request anyway, we'll close it and point you here. Nothing personal, and we'd still love the issue.
|
||||
|
||||
### Documentation
|
||||
## Reach out
|
||||
|
||||
The official documentation on GitBook open can be found directly in this Readme. Any updates or changes you would like to make, you can make directly to the README of this repository.
|
||||
Not sure if it's a bug? Have a half-formed idea? Open an issue or start a [discussion](https://github.com/orgs/GitbookIO/discussions) anyway. We'd much rather hear from you than not, and no report is too small.
|
||||
|
||||
## Contributing
|
||||
Spotted a wrong or missing translation? Open an issue with the string and the wording you'd suggest.
|
||||
|
||||
### Make changes locally
|
||||
## Using AI
|
||||
|
||||
Any contribution you make can be made to the code located in this repository. In order to contribute, you'll need to start off of a local version of this repository.
|
||||
You're welcome to use AI to help you write an issue. Please read and check what it produces before you post: reports that are long, vague or unverified take time away from fixing real problems, and we may close them without a reply.
|
||||
|
||||
#### 1. Fork the repository
|
||||
## Running locally
|
||||
|
||||
##### Using GitHub Desktop:
|
||||
You don't need a local setup to open an issue, but it can help you reproduce a problem or self-host the renderer.
|
||||
|
||||
- [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop.
|
||||
- Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)!
|
||||
### Prerequisites
|
||||
|
||||
##### Using the command line:
|
||||
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
|
||||
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
|
||||
|
||||
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
|
||||
### Setup
|
||||
|
||||
#### 2. Create a working branch and start with your changes
|
||||
1. Use the project's version of Node:
|
||||
|
||||
After forking this repository, you'll want to [create a branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) to work off of.
|
||||
|
||||
#### 3. Install dependencies and run the project locally
|
||||
|
||||
##### Prerequisites:
|
||||
|
||||
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
|
||||
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
|
||||
|
||||
##### Setup steps:
|
||||
|
||||
1. Ensure you are using the project's version of Node:
|
||||
```bash
|
||||
nvm use
|
||||
```
|
||||
|
||||
2. Install dependencies using Bun:
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
Additional development commands:
|
||||
- `bun format`: Format the code using Biome
|
||||
- `bun typecheck`: Run TypeScript type checking
|
||||
- `bun unit`: Run unit tests
|
||||
- `bun e2e`: Run end-to-end tests
|
||||
Other commands:
|
||||
|
||||
#### 4. Preview your changes
|
||||
- `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
|
||||
|
||||
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/url`.
|
||||
### Preview a site
|
||||
|
||||
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.
|
||||
With the development server running, any published GitBook site can be rendered through your local version at `http://localhost:3000/url/<published-url>`. For example, visit `http://localhost:3000/url/gitbook.com/docs` to see our own docs.
|
||||
|
||||
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.
|
||||
The site must be [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to render locally.
|
||||
|
||||
### Commit your update
|
||||
## License
|
||||
|
||||
[Commit your changes](https://github.com/git-guides/git-commit) once you are happy with them. See [Atom's contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages) to know how to use emoji for commit messages!
|
||||
The renderer is distributed under the [GNU GPLv3 License](https://github.com/GitbookIO/gitbook/blob/main/LICENSE). If you distribute the code, you must keep the source code public. To use it in a private repository, you need a [commercial license](https://www.gitbook.com/pricing).
|
||||
|
||||
Once your changes are ready, don't forget to self-review your code to double check that your chagnes are ready to be added.
|
||||
|
||||
### Pull Request
|
||||
|
||||
When you're finished with the changes, [create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request), also known as a PR.
|
||||
|
||||
- Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
|
||||
- Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. Once you submit your PR, a GitBook team member will review your proposal. We may ask questions or request for additional information.
|
||||
- We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch.
|
||||
- As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
|
||||
- If you run into any merge issues, checkout this [git tutorial](https://lab.github.com/githubtraining/managing-merge-conflicts) to help you resolve merge conflicts and other issues.
|
||||
|
||||
### Your PR is merged
|
||||
|
||||
Congratulations 🎉 Thank you for your contribution! Once your PR is merged, your contributions will be publicly visible on the relevant repository.
|
||||
The [`@gitbook/embed`](https://github.com/GitbookIO/gitbook/blob/main/packages/embed) package is distributed under the [Apache License 2.0](https://github.com/GitbookIO/gitbook/blob/main/packages/embed/LICENSE). Contributions to it are made under the same license.
|
||||
|
||||
@@ -1,83 +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
|
||||
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 }}
|
||||
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
|
||||
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
|
||||
# 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
|
||||
- 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
|
||||
# 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: 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 }}"
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"
|
||||
|
||||
@@ -1,34 +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
|
||||
commitTag:
|
||||
description: 'Commit branch to associate with the deployment'
|
||||
required: true
|
||||
commitMessage:
|
||||
description: 'Commit message to associate with the deployment'
|
||||
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.upload_middleware.outputs.deployment-url }}
|
||||
description: 'Deployment URL'
|
||||
value: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@@ -42,33 +42,34 @@ 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_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
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
|
||||
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
|
||||
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
|
||||
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ inputs.opItem }}/NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
|
||||
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
|
||||
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
|
||||
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
|
||||
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
|
||||
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
|
||||
GITBOOK_OAUTH_SERVER_URL: ${{ inputs.opItem }}/GITBOOK_OAUTH_SERVER_URL
|
||||
GITBOOK_SITE_OAUTH_SIGNING_SECRET: ${{ inputs.opItem }}/GITBOOK_SITE_OAUTH_SIGNING_SECRET
|
||||
GITBOOK_PREVIEW_BASE_URL: ${{ inputs.opItem }}/GITBOOK_PREVIEW_BASE_URL
|
||||
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
|
||||
GITBOOK_INTEGRATIONS_CONTENT_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_CONTENT_HOST
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
|
||||
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL_2
|
||||
GITBOOK_IMAGE_RESIZE_SALT: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SALT
|
||||
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
- name: Build worker
|
||||
run: bun run turbo build:cloudflare
|
||||
env:
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
GITBOOK_DISABLE_INSIGHTS: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
shell: bash
|
||||
|
||||
- name: Upload the DO worker
|
||||
@@ -135,11 +136,10 @@ runs:
|
||||
middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
|
||||
deploy: ${{ inputs.deploy }}
|
||||
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
|
||||
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
|
||||
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
|
||||
|
||||
@@ -1,31 +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
|
||||
headSha:
|
||||
description: 'Git ref to deploy, used for the deploymentId'
|
||||
required: false
|
||||
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 }}
|
||||
description: 'Deployment URL'
|
||||
value: ${{ steps.deploy.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@@ -40,51 +40,52 @@ 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_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
|
||||
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
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
echo "resolved HEAD_SHA: $HEAD_SHA"
|
||||
echo "GITBOOK_HEAD_SHA=$HEAD_SHA" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "GITBOOK_RUNTIME=vercel" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "GITBOOK_BLOCK_SEARCH_INDEXATION=true" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE=true" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "GITBOOK_DISABLE_INSIGHTS=true" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
|
||||
cat .vercel/.env.${{ inputs.environment }}.local
|
||||
- name: Build Project Artifacts
|
||||
run: bun run vercel build --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
|
||||
shell: bash
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
GITBOOK_RUNTIME: vercel
|
||||
GITBOOK_HEAD_SHA: ${{ inputs.headSha }}
|
||||
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
|
||||
@@ -92,10 +93,9 @@ 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: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
## Proposed changes
|
||||
|
||||
<!-- Briefly describe what changed. Include relevant validation. -->
|
||||
|
||||
## Demo
|
||||
|
||||
<!-- Add screenshots or a recording when useful. Otherwise, remove this section. -->
|
||||
|
||||
<!-- Optional: add Context here only when it explains a concrete problem and result that the changelog does not. Remove this comment before submitting. -->
|
||||
|
||||
## Changelog
|
||||
|
||||
<!-- Keep the relevant entries and remove unused placeholders. Feature = user-facing addition or improvement; Fix = user-facing bug fix; Chore = internal maintenance. -->
|
||||
|
||||
- [Feature] ...
|
||||
- [Fix] ...
|
||||
- [Chore] ...
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"display_information": {
|
||||
"name": "PR Status",
|
||||
"description": "Updates PR status reactions in engineering"
|
||||
},
|
||||
"features": {
|
||||
"bot_user": {
|
||||
"display_name": "PR Status",
|
||||
"always_online": false
|
||||
}
|
||||
},
|
||||
"oauth_config": {
|
||||
"scopes": {
|
||||
"bot": ["channels:history", "reactions:read", "reactions:write"]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"org_deploy_enabled": false,
|
||||
"socket_mode_enabled": false,
|
||||
"token_rotation_enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Slack PR status
|
||||
|
||||
[GitbookIO/slack-pr-status](https://github.com/GitbookIO/slack-pr-status) adds review-status reactions to PR links posted in [#engineering](https://gitbook.slack.com/archives/C01NXGWJELS). It runs from `.github/workflows/slack-pr-status.yml` in `GitbookIO/gitbook`, `GitbookIO/gitbook-x` and `GitbookIO/integrations`. Emojis and statuses are configured in that workflow; see the action's README for the options.
|
||||
|
||||
Owner: Peter White.
|
||||
|
||||
| Configuration | Value |
|
||||
| --- | --- |
|
||||
| Slack app | **PR Status** (`A0C2AGLAD0T`), created from `.github/slack-pr-status-app-manifest.json` |
|
||||
| Bot token | Repository Actions secret `SLAPR_SLACK_BOT_TOKEN` |
|
||||
| GitHub token | The workflow's `GITHUB_TOKEN`, read-only |
|
||||
|
||||
Posts that link PRs from more than one repository are skipped, because each workflow's token can only read its own repository. PRs from forks get no reactions, because their workflows don't receive secrets.
|
||||
|
||||
To disable it in one repository, run `gh workflow disable slack-pr-status.yml --repo GitbookIO/<repository>`. To stop it everywhere, revoke the PR Status app's token.
|
||||
@@ -20,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
|
||||
|
||||
@@ -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 }}
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.fork }}
|
||||
environment:
|
||||
name: preview-approval
|
||||
name: preview-approval
|
||||
steps:
|
||||
- name: Approval gate
|
||||
run: echo "Preview deployment approved."
|
||||
@@ -21,8 +21,8 @@ jobs:
|
||||
needs: approval
|
||||
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
|
||||
environment:
|
||||
name: 2v-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
name: 2v-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
@@ -47,8 +47,8 @@ jobs:
|
||||
needs: approval
|
||||
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
|
||||
environment:
|
||||
name: 2c-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
name: 2c-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}
|
||||
steps:
|
||||
@@ -72,10 +72,10 @@ jobs:
|
||||
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
|
||||
if [[ "${{ steps.deploy.outputs.command-output }}" =~ Worker\ Version\ ID:\ ([0-9a-f]{8})-([0-9a-f-]+) ]]; then
|
||||
WORKER_ID_FIRST_PART="${BASH_REMATCH[1]}"
|
||||
echo "worker-url=https://${WORKER_ID_FIRST_PART}-gitbook-open-v2-preview.gitbook.workers.dev/" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}"
|
||||
@@ -187,6 +187,54 @@ jobs:
|
||||
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)
|
||||
|
||||
@@ -7,7 +7,7 @@ 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:
|
||||
@@ -19,16 +19,16 @@ jobs:
|
||||
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:
|
||||
@@ -50,4 +50,4 @@ jobs:
|
||||
commitMessage: ${{ github.sha }}
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
|
||||
@@ -7,7 +7,7 @@ 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:
|
||||
@@ -19,16 +19,16 @@ jobs:
|
||||
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:
|
||||
@@ -50,4 +50,4 @@ jobs:
|
||||
commitMessage: ${{ github.sha }}
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
@@ -25,6 +25,8 @@ jobs:
|
||||
with:
|
||||
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
||||
fetch-depth: 0
|
||||
# Otherwise git pushes with GITHUB_TOKEN instead of the PAT, and the release PR never triggers Preview
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Slack PR status
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
pull_request:
|
||||
types: [closed, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
slack-pr-status:
|
||||
# Secrets aren't available to workflows triggered from forks.
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: GitbookIO/slack-pr-status@main
|
||||
with:
|
||||
slack-token: ${{ secrets.SLAPR_SLACK_BOT_TOKEN }}
|
||||
config: |
|
||||
channels: [C01NXGWJELS]
|
||||
approvalsRequired: 1
|
||||
multiPr: aggregate
|
||||
emojis:
|
||||
commented: false
|
||||
partiallyApproved: next_track_button
|
||||
approved: pr-approved
|
||||
changesRequested: construction
|
||||
merged: merged
|
||||
closed: no_entry_sign
|
||||
partiallyReviewed: hourglass_flowing_sand
|
||||
@@ -29,3 +29,6 @@ yarn-error.log*
|
||||
|
||||
# Bun pack artifacts
|
||||
packages/*/*.tgz
|
||||
|
||||
# Playwright MCP artifacts
|
||||
.playwright-mcp/
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"sortImports": {
|
||||
"customGroups": [
|
||||
{
|
||||
"groupName": "workspace",
|
||||
"elementNamePattern": ["@gitbook/**"]
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
["value-builtin", "value-external", "type-builtin", "type-external"],
|
||||
{ "newlinesBetween": true },
|
||||
"workspace",
|
||||
{ "newlinesBetween": true },
|
||||
[
|
||||
"value-internal",
|
||||
"type-internal",
|
||||
"value-parent",
|
||||
"type-parent",
|
||||
"value-sibling",
|
||||
"type-sibling",
|
||||
"value-index",
|
||||
"type-index"
|
||||
],
|
||||
"type-import",
|
||||
"unknown"
|
||||
]
|
||||
},
|
||||
"sortPackageJson": false,
|
||||
"sortTailwindcss": {
|
||||
"config": "./packages/gitbook/tailwind.config.ts",
|
||||
"attributes": ["class", "className", "style"],
|
||||
"functions": ["clsx", "tw"]
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/public/**",
|
||||
"**/.next/**",
|
||||
"**/.open-next/**",
|
||||
"**/.turbo/**",
|
||||
"**/.vercel/**",
|
||||
"**/.cache/**",
|
||||
"**/.wrangler/**",
|
||||
"**/*.log",
|
||||
"**/*.MD",
|
||||
"**/*.md",
|
||||
"**/*.mdx",
|
||||
"**/*.html",
|
||||
"**/*.css",
|
||||
"packages/embed/standalone/**",
|
||||
"packages/openapi-parser/src/fixtures/**",
|
||||
"packages/emoji-codepoints/index.ts",
|
||||
"packages/icons/src/data/*.json",
|
||||
"packages/gitbook/worker-configuration.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"categories": {
|
||||
"correctness": "warn",
|
||||
"suspicious": "warn",
|
||||
"perf": "warn"
|
||||
},
|
||||
"plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "jsx-a11y", "vitest"],
|
||||
"env": {
|
||||
"builtin": true,
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"shared-node-browser": true,
|
||||
"worker": true
|
||||
},
|
||||
"globals": {
|
||||
"Bun": "readonly",
|
||||
"GitBookIntegrationEvent": "readonly",
|
||||
"React": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
"eslint/no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": ["assert", "error", "warn"]
|
||||
}
|
||||
],
|
||||
"eslint/no-control-regex": "warn",
|
||||
"eslint/no-prototype-builtins": "warn",
|
||||
"eslint/no-cond-assign": "warn",
|
||||
"eslint/no-undef": "error",
|
||||
"eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true,
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"react/button-has-type": "warn",
|
||||
"react/exhaustive-deps": "warn",
|
||||
"react/jsx-key": "warn",
|
||||
"react/jsx-no-useless-fragment": "warn",
|
||||
"react/no-array-index-key": "warn",
|
||||
"react/void-dom-elements-no-children": "warn",
|
||||
"react/rules-of-hooks": "error",
|
||||
"jsx-a11y/alt-text": "warn",
|
||||
"jsx-a11y/anchor-is-valid": "warn",
|
||||
"jsx-a11y/click-events-have-key-events": "warn",
|
||||
"jsx-a11y/iframe-has-title": "warn",
|
||||
"jsx-a11y/interactive-supports-focus": "warn",
|
||||
"jsx-a11y/label-has-associated-control": "warn",
|
||||
"jsx-a11y/no-noninteractive-tabindex": "warn",
|
||||
"jsx-a11y/role-has-required-aria-props": "warn",
|
||||
"jsx-a11y/tabindex-no-positive": "warn",
|
||||
"typescript/array-type": "error",
|
||||
"typescript/no-explicit-any": "warn",
|
||||
"typescript/no-non-null-assertion": "warn",
|
||||
"typescript/no-confusing-void-expression": "warn",
|
||||
"typescript/only-throw-error": "error",
|
||||
"vitest/no-focused-tests": "error",
|
||||
"no-delete-var": "warn",
|
||||
"no-debugger": "error",
|
||||
"no-throw-literal": "error",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/public/**",
|
||||
"**/.next/**",
|
||||
"**/.open-next/**",
|
||||
"**/.turbo/**",
|
||||
"**/.vercel/**",
|
||||
"**/.cache/**",
|
||||
"**/.wrangler/**",
|
||||
"packages/embed/standalone/**",
|
||||
"packages/openapi-parser/src/fixtures/**",
|
||||
"packages/emoji-codepoints/index.ts",
|
||||
"packages/icons/src/data/*.json",
|
||||
"packages/gitbook/worker-configuration.d.ts",
|
||||
"**/*.css"
|
||||
]
|
||||
}
|
||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"recommendations": ["biomejs.biome"]
|
||||
"recommendations": ["oxc.oxc-vscode"]
|
||||
}
|
||||
|
||||
Vendored
+3
-5
@@ -8,14 +8,12 @@
|
||||
["style \\=([^;]*);", "\\`([^\\`]*)\\`"]
|
||||
],
|
||||
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit"
|
||||
"source.fixAll.oxc": "always"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
bun install # Install dependencies
|
||||
bun dev # Start dev server (all packages)
|
||||
bun run build # Build all packages
|
||||
bun run format # Format with Biome (run after every change)
|
||||
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
|
||||
```
|
||||
@@ -74,7 +75,7 @@ Save as `.changeset/<name>.md`, then commit it separately with message: `changes
|
||||
|
||||
## Formatting
|
||||
|
||||
Uses Biome (not ESLint/Prettier). Always run `bun run format` before committing.
|
||||
Linting uses Oxlint and formatting uses Oxfmt. Always run `bun run format` before committing.
|
||||
|
||||
## Comments
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ GitBook Open uses fontawesome. For self-hosting and local development, for licen
|
||||
|
||||
## Contributing
|
||||
|
||||
GitBook's rendering engine is fully open source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to learn more about the workflow on adding your first Pull Request.
|
||||
GitBook's rendering engine is fully open source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to learn how to report bugs and suggest changes.
|
||||
|
||||
### Types of contributions
|
||||
|
||||
@@ -93,22 +93,22 @@ We encourage you to contribute to GitBook to help us build the best tool for doc
|
||||
|
||||
#### Translations
|
||||
|
||||
The GitBook UI is rendered using a set of translation files found in [`packages/gitbook/src/intl/translations`](/packages/gitbook/src/intl/translations/). We welcome all additional translations for the UI.
|
||||
The GitBook UI is rendered using a set of translation files found in [`packages/gitbook/src/intl/translations`](/packages/gitbook/src/intl/translations/). Spotted a wrong or missing translation? Open an issue and let us know.
|
||||
|
||||
#### Bugs
|
||||
|
||||
Encounter a bug or find an issue you'd like to fix? Helping us fix issues related to GitBook greatly improves the experience for everyone. Head to the issues section of this repository to learn more about the types of bugs you can already help out with.
|
||||
Encounter a bug? [Open an issue](https://github.com/GitbookIO/gitbook/issues/new/choose) with as much detail as you can. Clear reports help us fix things faster for everyone.
|
||||
|
||||
## Deployment
|
||||
|
||||
> [!WARNING]
|
||||
> While it is possible to self-host this project, we do not recommend this unless you are certain this option fits your need.
|
||||
>
|
||||
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to get started._
|
||||
> _Looking for a specific feature in GitBook? Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to suggest it._
|
||||
>
|
||||
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
|
||||
>
|
||||
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to contribute to our product directly instead of creating your own version.
|
||||
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to share your ideas with us instead of creating your own version.
|
||||
|
||||
This project allows you to self-host the rendering portion of your GitBook published content. Self-hosting has pros and cons.
|
||||
|
||||
@@ -124,6 +124,8 @@ If you plan to distribute the code, you must make the source code public to comp
|
||||
|
||||
See `LICENSE` for more information.
|
||||
|
||||
The [`@gitbook/embed`](packages/embed) package is an exception: it is distributed under the [Apache License 2.0](packages/embed/LICENSE), so it can be used in proprietary applications.
|
||||
|
||||
## Badges
|
||||
|
||||
<p align="left">
|
||||
@@ -161,4 +163,4 @@ GitBook wouldn't be possible without these projects:
|
||||
|
||||
## Legacy GitBook (Deprecated)
|
||||
|
||||
Our previous version of GitBook and it's CLI tool are now deprecated. You can still view the old repository and it's commits on this [branch](https://github.com/GitbookIO/gitbook/tree/legacy).
|
||||
Our previous version of GitBook and it's CLI tool are now deprecated. You can still view the old repository and it's commits on this [branch](https://github.com/GitbookIO/gitbook/tree/legacy).
|
||||
-178
@@ -1,178 +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/embed/standalone/**/*",
|
||||
"packages/openapi-parser/src/fixtures/**/*",
|
||||
"packages/emoji-codepoints/index.ts",
|
||||
"packages/icons/src/data/*.json",
|
||||
"packages/gitbook/worker-configuration.d.ts",
|
||||
"gitbook/tsconfig.json",
|
||||
"**/*.css"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"useEditorconfig": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 100,
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"performance": {
|
||||
"noDelete": "warn"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"noUselessFragments": "warn",
|
||||
"noBannedTypes": "warn"
|
||||
},
|
||||
"correctness": {
|
||||
"noUndeclaredVariables": "error",
|
||||
"noUnusedVariables": "error",
|
||||
"useArrayLiterals": "error",
|
||||
"useHookAtTopLevel": "error",
|
||||
"noUnusedImports": "error",
|
||||
"noVoidElementsWithChildren": "warn",
|
||||
"useJsxKeyInIterable": "warn",
|
||||
"useExhaustiveDependencies": "warn",
|
||||
"noUnknownFunction": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "warn",
|
||||
"noParameterAssign": "off",
|
||||
"useThrowOnlyError": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noConsole": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"allow": ["assert", "error", "warn"]
|
||||
}
|
||||
},
|
||||
"noExplicitAny": "warn",
|
||||
"noImplicitAnyLet": "warn",
|
||||
"noConfusingVoidType": "warn",
|
||||
"noControlCharactersInRegex": "warn",
|
||||
"noPrototypeBuiltins": "warn",
|
||||
"noAssignInExpressions": "warn",
|
||||
"noArrayIndexKey": "warn"
|
||||
},
|
||||
"a11y": {
|
||||
"useSemanticElements": "warn",
|
||||
"useKeyWithClickEvents": "warn",
|
||||
"noSvgWithoutTitle": "warn",
|
||||
"useButtonType": "warn",
|
||||
"useIframeTitle": "warn",
|
||||
"useAltText": "warn",
|
||||
"noPositiveTabindex": "warn",
|
||||
"useFocusableInteractive": "warn",
|
||||
"useAriaPropsForRole": "warn",
|
||||
"useValidAnchor": "warn",
|
||||
"noLabelWithoutControl": "warn",
|
||||
"noNoninteractiveTabindex": "warn"
|
||||
},
|
||||
"nursery": {
|
||||
"useSortedClasses": {
|
||||
"level": "error",
|
||||
"fix": "safe",
|
||||
"options": {
|
||||
"attributes": ["class", "className", "style"],
|
||||
"functions": ["clsx", "tw"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "es5",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": [
|
||||
"packages/gitbook/**/*",
|
||||
"packages/react-openapi/**/*",
|
||||
"packages/react-math/**/*",
|
||||
"packages/react-contentkit/**/*",
|
||||
"packages/icons/**/*"
|
||||
],
|
||||
"javascript": {
|
||||
"globals": ["React"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["packages/gitbook/**/*"],
|
||||
"javascript": {
|
||||
"globals": ["React", "GitBookIntegrationEvent"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.css"],
|
||||
"javascript": {
|
||||
"globals": ["theme"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.test.ts", "packages/gitbook/tests/**/*"],
|
||||
"javascript": {
|
||||
"globals": ["Bun"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"packages/cache-do/**/*",
|
||||
"packages/gitbook/cf-env.d.ts",
|
||||
"packages/gitbook/src/cloudflare-entrypoint.ts"
|
||||
],
|
||||
"javascript": {
|
||||
"globals": [
|
||||
"DurableObjectLocationHint",
|
||||
"DurableObjectNamespace",
|
||||
"DurableObjectStub",
|
||||
"ContinentCode",
|
||||
"Fetcher",
|
||||
"ExportedHandler"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+17
-10
@@ -5,9 +5,10 @@
|
||||
"node": "^22.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.31.0",
|
||||
"turbo": "^2.10.3",
|
||||
"@changesets/cli": "^2.31.1",
|
||||
"oxfmt": "^0.62.0",
|
||||
"oxlint": "^1.93.0",
|
||||
"turbo": "^2.11.11",
|
||||
"vercel": "50.37.3"
|
||||
},
|
||||
"packageManager": "bun@1.3.7",
|
||||
@@ -18,8 +19,7 @@
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"esbuild": "0.27.3",
|
||||
"axios": "1.8.4",
|
||||
"@radix-ui/react-slot": "1.2.4"
|
||||
"axios": "1.8.4"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -27,8 +27,10 @@
|
||||
"build": "turbo run build",
|
||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"format": "biome check --write ./",
|
||||
"format:check": "biome check --diagnostic-level=error ./",
|
||||
"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",
|
||||
@@ -39,11 +41,14 @@
|
||||
"clean": "turbo run clean"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"catalog": {
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@gitbook/api": "0.191.0",
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@gitbook/api": "0.208.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
@@ -62,6 +67,8 @@
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
|
||||
"next@16.2.11": "patches/next@16.2.11.patch"
|
||||
"mermaid@11.16.1": "patches/mermaid@11.16.1.patch",
|
||||
"next@16.4.5": "patches/next@16.4.5.patch",
|
||||
"@shikijs/langs@3.21.0": "patches/@shikijs%2Flangs@3.21.0.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @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
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:"
|
||||
@@ -25,7 +25,11 @@
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
|
||||
@@ -24,7 +24,11 @@
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @gitbook/colors
|
||||
|
||||
## 0.4.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6c12872: Expand 3-digit hex colors correctly in `hexToRgbArray`.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.5",
|
||||
"devDependencies": {
|
||||
"bun-types": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
@@ -21,7 +21,11 @@
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { colorScale } from './transformations';
|
||||
import { colorScale, hexToRgbArray } from './transformations';
|
||||
|
||||
describe('colorScale exact base', () => {
|
||||
it('takes a very light tint as the exact background on step 1 (clean/bold)', () => {
|
||||
@@ -67,3 +67,15 @@ describe('colorScale exact base', () => {
|
||||
expect(scale[0]).not.toBe('#F5F3EF');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hexToRgbArray', () => {
|
||||
it('parses a 6-digit hex color', () => {
|
||||
expect(hexToRgbArray('#1a2b3c')).toEqual([26, 43, 60]);
|
||||
});
|
||||
|
||||
it('expands a 3-digit hex color', () => {
|
||||
expect(hexToRgbArray('#abc')).toEqual([170, 187, 204]);
|
||||
expect(hexToRgbArray('abc')).toEqual([170, 187, 204]);
|
||||
expect(hexToRgbArray('#fff')).toEqual([255, 255, 255]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -311,7 +311,9 @@ export function hexToRgbArray(hex: string): RGBColor {
|
||||
const originalHex = hex;
|
||||
|
||||
let value = hex.replace('#', '');
|
||||
if (hex.length === 3) value = value + value;
|
||||
if (value.length === 3) {
|
||||
value = [...value].map((digit) => digit + digit).join('');
|
||||
}
|
||||
|
||||
const r = value.substring(0, 2);
|
||||
const g = value.substring(2, 4);
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @gitbook/embed
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 31e88d1: Relicense under Apache 2.0 and drop the dependency on `@gitbook/icons`.
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- f35be52: Add `defaultTab` and `defaultPage` options to the Docs Embed to choose where it opens. `defaultTab` opens the embed on the assistant, search or docs tab. `defaultPage` sets the page the docs tab opens on in place of the site's home page, and the embed opens on that page unless `defaultTab` says otherwise. Both work with the standalone script's `configure`, `frame.configure` and `<GitBookFrame>` props.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -19,15 +19,27 @@ 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>
|
||||
// Initialize with Authenticated Access (optional)
|
||||
window.GitBook('init',
|
||||
window.GitBook('init',
|
||||
{ siteURL: 'https://docs.company.com' },
|
||||
{ visitor: { token: 'your-jwt-token' } }
|
||||
);
|
||||
window.GitBook('show');
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -42,6 +54,7 @@ GitBook('configure', {
|
||||
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
|
||||
},
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultPage: '/getting-started',
|
||||
actions: [
|
||||
{
|
||||
icon: 'circle-question',
|
||||
@@ -95,6 +108,7 @@ frame.clearChat();
|
||||
// Configure the embed (see Configuration section for all options)
|
||||
frame.configure({
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultPage: '/getting-started',
|
||||
actions: [
|
||||
{
|
||||
icon: 'circle-question',
|
||||
@@ -130,6 +144,7 @@ import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
|
||||
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
|
||||
}}
|
||||
tabs={['assistant', 'search', 'docs']}
|
||||
defaultPage="/getting-started"
|
||||
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
|
||||
assistantName="Support Assistant"
|
||||
suggestions={['What is GitBook?', 'How do I get started?']}
|
||||
@@ -237,6 +252,33 @@ Override which tabs are displayed. Defaults to your site's configuration.
|
||||
tabs: ['assistant', 'search', 'docs']
|
||||
```
|
||||
|
||||
### `defaultTab`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
The tab the embed opens on. Without it, the embed opens on `defaultPage` if set, otherwise on the assistant (or the docs when the assistant isn't available). The tab must be one of the enabled `tabs`.
|
||||
|
||||
- **Type**: `'assistant' | 'search' | 'docs'`
|
||||
|
||||
```javascript
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultTab: 'search'
|
||||
```
|
||||
|
||||
`defaultTab` and `defaultPage` apply when the embed first loads: the standalone widget keeps its place when it is closed and reopened. To move it later, use `navigateToPage` or `navigateToAssistant`.
|
||||
|
||||
### `defaultPage`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
The page the docs tab opens on, in place of your site's home page. Accepts the same references as `navigateToPage`: the page's path within the site, an absolute path, or its full published URL. Unless `defaultTab` says otherwise, the embed opens on this page.
|
||||
|
||||
- **Type**: `string`
|
||||
|
||||
```javascript
|
||||
defaultPage: '/getting-started/quickstart'
|
||||
```
|
||||
|
||||
### `closeButton`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
@@ -464,20 +506,25 @@ visitor: {
|
||||
|
||||
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
|
||||
|
||||
Override the embed's color scheme. When omitted, the embed follows the iframe's CSS `color-scheme`, which lets it inherit the parent page or browser preference.
|
||||
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**: Pass as the second argument to `GitBook('init', options, frameOptions)`
|
||||
**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'`
|
||||
|
||||
```javascript
|
||||
colorScheme: '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
|
||||
@@ -495,3 +542,7 @@ button: {
|
||||
icon: 'assistant'
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the [Apache License 2.0](./LICENSE).
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"version": "0.5.1",
|
||||
"version": "0.7.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:",
|
||||
"bidc": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -39,7 +39,12 @@
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
|
||||
@@ -10,7 +10,9 @@ export type CreateGitBookOptions = {
|
||||
export type GetFrameURLOptions = {
|
||||
/**
|
||||
* Override the color scheme used by the embedded docs.
|
||||
* When omitted, the embed follows the iframe's CSS `color-scheme`.
|
||||
* 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';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createChannel } from 'bidc';
|
||||
|
||||
import type {
|
||||
FrameToParentMessage,
|
||||
GitBookEmbeddableConfiguration,
|
||||
@@ -76,7 +77,7 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
|
||||
channel.send(message);
|
||||
};
|
||||
|
||||
const events = new Map<string, Array<(...args: any[]) => void>>();
|
||||
const events = new Map<string, ((...args: any[]) => void)[]>();
|
||||
|
||||
const configuration: GitBookEmbeddableConfiguration = {
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
|
||||
/**
|
||||
* Name of a Font Awesome icon (e.g. `bolt`), or a GitBook custom icon.
|
||||
*/
|
||||
// Kept as a plain string so the package doesn't depend on @gitbook/icons and its licence.
|
||||
type IconName = string;
|
||||
|
||||
/**
|
||||
* Confirmation action to be displayed to the user before executing a tool.
|
||||
@@ -62,6 +67,12 @@ export type GitBookEmbeddableConfiguration = {
|
||||
/** Tabs to display in the embed (if enabled on the site). */
|
||||
tabs: ('assistant' | 'docs' | 'search')[];
|
||||
|
||||
/** Tab to open the embed on. */
|
||||
defaultTab?: 'assistant' | 'docs' | 'search';
|
||||
|
||||
/** Page to open the docs tab on, instead of the site's home page. */
|
||||
defaultPage?: string;
|
||||
|
||||
/** Additional buttons to be displayed in the header of the GitBook embed. */
|
||||
actions: GitBookEmbeddableActionDefinition[];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type {
|
||||
GetFrameURLOptions,
|
||||
GitBookEmbeddableConfiguration,
|
||||
@@ -26,6 +27,8 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
suggestions = [],
|
||||
tools = [],
|
||||
tabs = ['assistant', 'search', 'docs'],
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
trademark = true,
|
||||
closeButton = false,
|
||||
assistantName,
|
||||
@@ -49,6 +52,8 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
useEffect(() => {
|
||||
gitbookFrame?.configure({
|
||||
tabs,
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
actions,
|
||||
greeting,
|
||||
suggestions,
|
||||
@@ -64,6 +69,8 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
suggestions,
|
||||
tools,
|
||||
tabs,
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
closeButton,
|
||||
trademark,
|
||||
assistantName,
|
||||
@@ -78,7 +85,6 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
height="100%"
|
||||
allow="clipboard-write"
|
||||
className={className}
|
||||
style={colorScheme ? { colorScheme } : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { type CreateGitBookOptions, createGitBook } from '../client';
|
||||
import { GitBookContext } from './context';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { GitBookClient } from '../client';
|
||||
|
||||
export const GitBookContext = React.createContext<GitBookClient | null>(null);
|
||||
|
||||
@@ -55,6 +55,7 @@ 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',
|
||||
@@ -85,6 +86,47 @@ 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(
|
||||
@@ -102,12 +144,8 @@ function getIframe() {
|
||||
widgetIframe = document.createElement('iframe');
|
||||
widgetIframe.id = 'gitbook-widget-iframe';
|
||||
widgetIframe.allow = 'clipboard-write';
|
||||
if (frameOptions?.colorScheme) {
|
||||
widgetIframe.style.colorScheme = frameOptions.colorScheme;
|
||||
}
|
||||
widgetIframe.src = client.getFrameURL({
|
||||
...frameOptions,
|
||||
});
|
||||
// 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);
|
||||
@@ -115,25 +153,53 @@ function getIframe() {
|
||||
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':
|
||||
if (_client) {
|
||||
throw new Error(
|
||||
'GitBook client already initialized. Call GitBook("unload") first.'
|
||||
);
|
||||
}
|
||||
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 = args[2];
|
||||
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':
|
||||
@@ -192,6 +258,7 @@ const GitBook = (...args: StandaloneCalls) => {
|
||||
}
|
||||
}
|
||||
|
||||
frameConfigured = true;
|
||||
getIframe().frame.configure({
|
||||
...frameConfiguration,
|
||||
});
|
||||
@@ -214,4 +281,11 @@ const precalls = (window.GitBook as GitBookStandalone | undefined)?.q ?? [];
|
||||
|
||||
// @ts-expect-error - GitBook is not defined in the global scope
|
||||
window.GitBook = GitBook;
|
||||
precalls.forEach((call) => GitBook(...call));
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -29,14 +29,30 @@
|
||||
--gitbook-widget-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--gitbook-widget-text-color: #FFFFFF;
|
||||
--gitbook-widget-border-color: #202020;
|
||||
--gitbook-widget-background-translucent: rgba(15, 15, 15, 0.9);
|
||||
--gitbook-widget-background-translucent-hover: rgba(20, 20, 20, 0.9);
|
||||
--gitbook-widget-background-solid: #f0f0f0;
|
||||
}
|
||||
/* 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;
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/expr
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 02f4cbc: Restrict expressions to an allowlist of methods and syntax, and report unsupported expressions when parsing.
|
||||
|
||||
## 1.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@gitbook/expr",
|
||||
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||
"version": "1.3.1",
|
||||
"version": "1.4.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -16,7 +16,7 @@
|
||||
"acorn-walk": "^8.3.4",
|
||||
"assert-never": "catalog:",
|
||||
"escodegen": "^2.1.0",
|
||||
"eval-estree-expression": "^3.0.1"
|
||||
"eval-estree-expression": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/strictest": "catalog:",
|
||||
@@ -36,7 +36,11 @@
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
|
||||
@@ -58,10 +58,10 @@ describe('autocomplete', () => {
|
||||
}),
|
||||
};
|
||||
const context = new SymbolsTable(symbols);
|
||||
const SCENARIOS: Array<{
|
||||
const SCENARIOS: {
|
||||
expressionWithCursor: string;
|
||||
expectedSuggestions: AutocompleteSuggestions;
|
||||
}> = [
|
||||
}[] = [
|
||||
{
|
||||
expressionWithCursor: 'visit<cur>',
|
||||
expectedSuggestions: [
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { parseExpressionAt } from 'acorn';
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
|
||||
import { ExpressionError } from '../errors';
|
||||
import {
|
||||
ALLOWED_METHODS,
|
||||
MAX_CALLBACK_CALLS,
|
||||
UnsafeExpressionError,
|
||||
evaluateRestricted,
|
||||
} from '../restricted';
|
||||
import { ExpressionRuntime } from '../runtime';
|
||||
import { SymbolArray, SymbolString } from '../symbols';
|
||||
import type { Logger } from '../types';
|
||||
|
||||
const SILENT_LOGGER: Logger = {
|
||||
@@ -10,6 +18,17 @@ const SILENT_LOGGER: Logger = {
|
||||
error: () => {},
|
||||
};
|
||||
|
||||
const getInputs = () => ({ user: { role: 'editor' }, tags: ['b', 'a'], count: 1 });
|
||||
|
||||
// Returns the thrown error instead of throwing, so that side effects can be asserted first.
|
||||
const attempt = (fn: () => unknown) => {
|
||||
try {
|
||||
return fn();
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
describe('ExpressionRuntime', () => {
|
||||
const runtime = new ExpressionRuntime(SILENT_LOGGER);
|
||||
|
||||
@@ -66,6 +85,51 @@ describe('ExpressionRuntime', () => {
|
||||
inputs: { x: 2 },
|
||||
expectedResult: [2, 4, 6],
|
||||
},
|
||||
{
|
||||
scenario: 'array filter',
|
||||
condition: 'reviews.filter(review => review.status === "approved").length >= 2',
|
||||
inputs: {
|
||||
reviews: [
|
||||
{ status: 'approved' },
|
||||
{ status: 'pending' },
|
||||
{ status: 'approved' },
|
||||
],
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array some',
|
||||
condition: 'tags.some(tag => tag.startsWith("b") && tag.length === x)',
|
||||
inputs: { tags: ['a', 'bb'], x: 2 },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'nested callbacks reading outer parameters',
|
||||
condition: 'required.every(id => reviews.some(review => review.reviewer === id))',
|
||||
inputs: {
|
||||
required: ['u1', 'u2'],
|
||||
reviews: [{ reviewer: 'u2' }, { reviewer: 'u1' }],
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'callback parameter shadowing an input',
|
||||
condition: 'tags.some(role => role === "b") && role === "admin"',
|
||||
inputs: { tags: ['a', 'b'], role: 'admin' },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array literal includes',
|
||||
condition: '["admin", "editor"].includes(user.role)',
|
||||
inputs: { user: { role: 'admin' } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array and string at',
|
||||
condition: 'tags.at(-1).at(0)',
|
||||
inputs: { tags: ['a', 'bc'] },
|
||||
expectedResult: 'b',
|
||||
},
|
||||
{
|
||||
scenario: 'string startsWith',
|
||||
condition: 'user.role.startsWith("ad")',
|
||||
@@ -102,6 +166,79 @@ describe('ExpressionRuntime', () => {
|
||||
inputs: { user: { role: ' admin ' } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'string length and index',
|
||||
condition: 'user.role.length === 5 && user.role[0] === "a"',
|
||||
inputs: { user: { role: 'admin' } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'missing property',
|
||||
condition: 'user.plan === undefined && missing === undefined',
|
||||
inputs: { user: { role: 'admin' } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'boolean claim',
|
||||
condition: 'visitor.claims.flag === true',
|
||||
inputs: { visitor: { claims: { flag: true } } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'nested claims joined with ||',
|
||||
condition: 'visitor.claims.group.a === true || visitor.claims.group.b === true',
|
||||
inputs: { visitor: { claims: { group: { b: true } } } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'negated guard',
|
||||
condition: "!visitor.claims.unsigned || visitor.claims.unsigned.region !== 'eu'",
|
||||
inputs: { visitor: { claims: { unsigned: { region: 'us' } } } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'negated guard with a missing claim',
|
||||
condition: "!visitor.claims.unsigned || visitor.claims.unsigned.region !== 'eu'",
|
||||
inputs: { visitor: { claims: {} } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'key with a colon',
|
||||
condition: "visitor.claims['custom:tier'] !== 'gold'",
|
||||
inputs: { visitor: { claims: { 'custom:tier': 'silver' } } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'key with a colon and a missing claim',
|
||||
condition: "visitor.claims['custom:tier'] !== 'gold'",
|
||||
inputs: { visitor: { claims: {} } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'URL-style key',
|
||||
condition: "visitor.claims['https://example.com/flag'] === true",
|
||||
inputs: { visitor: { claims: { 'https://example.com/flag': true } } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'claim array includes',
|
||||
condition: "visitor.claims.products.includes('a')",
|
||||
inputs: { visitor: { claims: { products: ['a', 'b'] } } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array literal includes a claim',
|
||||
condition: "['a', 'b'].includes(visitor.claims.org)",
|
||||
inputs: { visitor: { claims: { org: 'a' } } },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: '&& guard with a dashed key',
|
||||
condition:
|
||||
"visitor.claims.unsigned && visitor.claims.unsigned['early-access'] === true",
|
||||
inputs: { visitor: { claims: { unsigned: { 'early-access': true } } } },
|
||||
expectedResult: true,
|
||||
},
|
||||
])(
|
||||
'should properly evaluate/safeEvaluate a valid conditional expression: $scenario',
|
||||
({ condition, inputs, expectedResult }) => {
|
||||
@@ -126,11 +263,6 @@ describe('ExpressionRuntime', () => {
|
||||
condition: 'while (1) {}',
|
||||
inputs: {},
|
||||
},
|
||||
{
|
||||
scenario: 'unsafe expression',
|
||||
condition: '[1, 2, 3].map(() => { while (1) {}})',
|
||||
inputs: {},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(INVALID_EXPRESSSIONS)(
|
||||
@@ -148,6 +280,303 @@ describe('ExpressionRuntime', () => {
|
||||
expect(() => runtime.evaluate(condition, inputs)).toThrowError(ExpressionError);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ scenario: 'string method', condition: 'visitor.claims.plan.startsWith("p")' },
|
||||
{ scenario: 'array method', condition: 'visitor.claims.products.includes("a")' },
|
||||
{ scenario: 'callback method', condition: 'visitor.claims.products.some(p => p)' },
|
||||
{ scenario: 'missing parent', condition: 'visitor.claims.org.name.toLowerCase()' },
|
||||
])(
|
||||
'should give undefined when calling a method on a missing claim: $scenario',
|
||||
({ condition }) => {
|
||||
const inputs = { visitor: { claims: {} } };
|
||||
|
||||
expect(runtime.evaluate(condition, inputs)).toBeUndefined();
|
||||
expect(runtime.safeEvaluate(condition, inputs)).toEqual({ value: undefined });
|
||||
expect(runtime.evaluateBoolean(condition, inputs)).toBe(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('unsafe expressions', () => {
|
||||
// Each expression tries to call this function, which only an escape from the restrictions can reach.
|
||||
const PAYLOAD = '__expressionPayload';
|
||||
const CALL_PAYLOAD = `globalThis.${PAYLOAD}()`;
|
||||
const payload = mock(() => true);
|
||||
|
||||
beforeEach(() => {
|
||||
payload.mockClear();
|
||||
Reflect.set(globalThis, PAYLOAD, payload);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Reflect.deleteProperty(globalThis, PAYLOAD);
|
||||
});
|
||||
|
||||
const UNSAFE_EXPRESSIONS = [
|
||||
{
|
||||
scenario: 'array key bypassing the constructor check',
|
||||
condition: `({})[["constructor"]][["constructor"]]("${CALL_PAYLOAD}")()`,
|
||||
},
|
||||
{
|
||||
scenario: 'array key on an input value',
|
||||
condition: `user.role[["constructor"]][["constructor"]]("${CALL_PAYLOAD}")()`,
|
||||
},
|
||||
{
|
||||
scenario: 'array key on a method',
|
||||
condition: `"".includes[["constructor"]]("${CALL_PAYLOAD}")()`,
|
||||
},
|
||||
{
|
||||
scenario: 'global access in a callback',
|
||||
condition: `[1].some(() => ${CALL_PAYLOAD})`,
|
||||
},
|
||||
{
|
||||
scenario: 'computed constructor key in a callback',
|
||||
condition: `[1].some(x => x["constr" + "uctor"]["constr" + "uctor"]("${CALL_PAYLOAD}")())`,
|
||||
},
|
||||
{
|
||||
scenario: 'computed constructor key in a map callback',
|
||||
condition: `[1].map(x => x["constr" + "uctor"]["constr" + "uctor"]("${CALL_PAYLOAD}")())`,
|
||||
},
|
||||
{
|
||||
scenario: 'arrow function called through implicit toString',
|
||||
condition: `({ toString: () => ${CALL_PAYLOAD} }) + ""`,
|
||||
},
|
||||
{ scenario: 'inherited property', condition: 'user.constructor' },
|
||||
{
|
||||
scenario: 'inherited property with an array key',
|
||||
condition: 'user[["constructor"]]',
|
||||
},
|
||||
{
|
||||
scenario: 'inherited property of a string',
|
||||
condition: 'user.role.toString',
|
||||
runtimeOnly: true,
|
||||
},
|
||||
{ scenario: 'inherited property of a number', condition: 'count.constructor' },
|
||||
{ scenario: 'prototype access', condition: 'user.__proto__' },
|
||||
{ scenario: 'inherited variable', condition: 'toString', runtimeOnly: true },
|
||||
{ scenario: 'arrow function outside a callback', condition: '[() => 1].length' },
|
||||
{
|
||||
scenario: 'block-bodied arrow function',
|
||||
condition: '[1, 2, 3].some(() => { while (1) {} })',
|
||||
},
|
||||
{ scenario: 'function expression', condition: '[1].some(function () { return 1 })' },
|
||||
{ scenario: 'async arrow callback', condition: 'tags.some(async t => t)' },
|
||||
{
|
||||
scenario: 'async function callback',
|
||||
condition: 'tags.some(async function (t) { return t })',
|
||||
},
|
||||
{ scenario: 'immediately invoked async arrow', condition: '(async () => 1)()' },
|
||||
{ scenario: 'object literal', condition: '({ role: "admin" }).role' },
|
||||
{ scenario: 'constructor call', condition: 'new Date()' },
|
||||
{ scenario: 'eval', condition: `eval("${CALL_PAYLOAD}")` },
|
||||
{ scenario: 'eval as a method', condition: `globalThis.eval("${CALL_PAYLOAD}")` },
|
||||
{ scenario: 'new Function', condition: `new Function("${CALL_PAYLOAD}")()` },
|
||||
{ scenario: 'Function without new', condition: `Function("${CALL_PAYLOAD}")()` },
|
||||
{
|
||||
scenario: 'new on a function expression',
|
||||
condition: `new function () { ${CALL_PAYLOAD} }()`,
|
||||
},
|
||||
{ scenario: 'method outside the standard library', condition: '"a".padStart(1e9)' },
|
||||
{ scenario: 'string repeat', condition: 'user.role.repeat(1e9)' },
|
||||
{ scenario: 'string repeat in a callback', condition: 'tags.map(t => t.repeat(1e9))' },
|
||||
{ scenario: 'mutating array method', condition: 'tags.reverse()' },
|
||||
{
|
||||
scenario: 'callback for a method not taking one',
|
||||
condition: 'tags.includes(t => t)',
|
||||
},
|
||||
{
|
||||
scenario: 'inherited variable in a callback',
|
||||
condition: 'tags.some(t => toString)',
|
||||
runtimeOnly: true,
|
||||
},
|
||||
{
|
||||
scenario: 'callback parameter named __proto__',
|
||||
condition: 'tags.some(__proto__ => 1)',
|
||||
},
|
||||
{
|
||||
scenario: 'callback parameter named constructor',
|
||||
condition: 'tags.some(constructor => 1)',
|
||||
},
|
||||
{
|
||||
scenario: 'expression deeper than the library limit',
|
||||
condition: `${'!'.repeat(60)}true`,
|
||||
runtimeOnly: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array literal longer than the library limit',
|
||||
condition: `[${'1,'.repeat(10_001)}].length`,
|
||||
runtimeOnly: true,
|
||||
},
|
||||
{
|
||||
scenario: 'inherited property of a callback parameter',
|
||||
condition: 'tags.some(t => t.constructor)',
|
||||
},
|
||||
{ scenario: 'non-arrow callback', condition: 'tags.map(Function)' },
|
||||
{ scenario: 'callback with an extra argument', condition: 'tags.some(t => t, "a")' },
|
||||
{ scenario: 'missing callback', condition: 'tags.some()' },
|
||||
{
|
||||
scenario: 'too many nested callback calls',
|
||||
condition:
|
||||
'[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(a => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(b => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(c => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(d => d))))',
|
||||
runtimeOnly: true,
|
||||
},
|
||||
{ scenario: 'method on a plain object', condition: 'user.hasOwnProperty("role")' },
|
||||
{ scenario: 'method on a function', condition: '[1].some.call([1], n => n)' },
|
||||
{
|
||||
scenario: 'non-primitive argument',
|
||||
condition: '[user].includes(user)',
|
||||
runtimeOnly: true,
|
||||
},
|
||||
{ scenario: 'regex operator', condition: 'user.role =~ /^ed/' },
|
||||
{ scenario: 'regex argument', condition: 'user.role.includes(/(a+)+b/)' },
|
||||
{ scenario: 'BigInt literal', condition: '2n ** 64n' },
|
||||
{
|
||||
scenario: 'BigInt literal in a callback',
|
||||
condition: '[1, 2, 3, 4].some((n) => 9n ** 9n ** 9n)',
|
||||
},
|
||||
{ scenario: 'BigInt literal in an array', condition: '[9n][0] ** 9n' },
|
||||
{ scenario: 'BigInt literal in a template', condition: '`${9n ** 9n ** 9n}`' },
|
||||
{ scenario: 'delete', condition: 'delete user.role' },
|
||||
{ scenario: 'assignment', condition: 'user.role = "admin"' },
|
||||
{ scenario: 'update', condition: 'count++' },
|
||||
];
|
||||
|
||||
it.each(UNSAFE_EXPRESSIONS)(
|
||||
'should throw with evaluate and return the error with safeEvaluate: $scenario',
|
||||
({ condition }) => {
|
||||
const evaluated = attempt(() => runtime.evaluate(condition, getInputs()));
|
||||
const safeEvaluated = runtime.safeEvaluate(condition, getInputs());
|
||||
|
||||
expect(payload).not.toHaveBeenCalled();
|
||||
expect(evaluated).toBeInstanceOf(ExpressionError);
|
||||
expect(safeEvaluated.value).toBeUndefined();
|
||||
expect(safeEvaluated.error).toBeInstanceOf(ExpressionError);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(UNSAFE_EXPRESSIONS)(
|
||||
'should evaluate to false with evaluateBoolean/evaluateBooleanAll: $scenario',
|
||||
({ condition }) => {
|
||||
const evaluated = runtime.evaluateBoolean(condition, getInputs());
|
||||
const evaluatedAll = runtime.evaluateBooleanAll(['true', condition], getInputs());
|
||||
|
||||
expect(payload).not.toHaveBeenCalled();
|
||||
expect(evaluated).toBe(false);
|
||||
expect(evaluatedAll).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(UNSAFE_EXPRESSIONS)(
|
||||
'should throw without executing code with evaluateTemplate: $scenario',
|
||||
({ condition }) => {
|
||||
const evaluated = attempt(() =>
|
||||
runtime.evaluateTemplate(`Hello {{ ${condition} }}`, getInputs())
|
||||
);
|
||||
|
||||
expect(payload).not.toHaveBeenCalled();
|
||||
expect(evaluated).toBeInstanceOf(ExpressionError);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(UNSAFE_EXPRESSIONS)('should not mutate the inputs: $scenario', ({ condition }) => {
|
||||
const inputs = getInputs();
|
||||
attempt(() => runtime.evaluate(condition, inputs));
|
||||
expect(inputs).toEqual(getInputs());
|
||||
});
|
||||
|
||||
it.each(UNSAFE_EXPRESSIONS)(
|
||||
'should report the error when parsing, for editors: $scenario',
|
||||
({ condition, runtimeOnly }) => {
|
||||
const parsed = attempt(() => runtime.parse(condition));
|
||||
const { errors } = runtime.parseTemplate(`Hello {{ ${condition} }}`);
|
||||
|
||||
if (runtimeOnly) {
|
||||
// Depends on the values, so only enforced when evaluating.
|
||||
expect(parsed).not.toBeInstanceOf(Error);
|
||||
expect(errors).toEqual([]);
|
||||
} else {
|
||||
expect(parsed).toBeInstanceOf(ExpressionError);
|
||||
expect((parsed as ExpressionError).location).toBeDefined();
|
||||
expect(errors).toHaveLength(1);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it.each(UNSAFE_EXPRESSIONS)(
|
||||
'should not execute code when evaluating without validating first: $scenario',
|
||||
({ condition }) => {
|
||||
const expression = parseExpressionAt(condition, 0, { ecmaVersion: 'latest' });
|
||||
const evaluated = attempt(() => evaluateRestricted(expression, getInputs()));
|
||||
|
||||
expect(payload).not.toHaveBeenCalled();
|
||||
expect(
|
||||
evaluated instanceof UnsafeExpressionError || evaluated instanceof RangeError
|
||||
).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it('should point the parse error at the offending token', () => {
|
||||
const condition = 'user.role.padStart(10)';
|
||||
const error = attempt(() => runtime.parse(condition)) as ExpressionError;
|
||||
|
||||
expect(error.message).toBe('Method "padStart" is not supported');
|
||||
expect(error.location).toEqual({ line: 1, column: 10 });
|
||||
expect(condition.slice(error.token?.start, error.token?.end)).toBe('padStart');
|
||||
});
|
||||
|
||||
it('should keep the location of the error when evaluating', () => {
|
||||
const condition = 'user.role.padStart(10)';
|
||||
const error = attempt(() => runtime.evaluate(condition, getInputs()));
|
||||
|
||||
expect(error).toBeInstanceOf(UnsafeExpressionError);
|
||||
expect((error as ExpressionError).location).toEqual({ line: 1, column: 10 });
|
||||
});
|
||||
|
||||
it('should explain why async callbacks are rejected', () => {
|
||||
const error = attempt(() => runtime.parse('tags.some(async t => t)'));
|
||||
|
||||
expect((error as ExpressionError).message).toBe('Async functions are not supported');
|
||||
});
|
||||
|
||||
it('should not validate when parsing loosely for autocomplete', () => {
|
||||
expect(() => runtime.parse('user.constructor', { loose: true })).not.toThrow();
|
||||
});
|
||||
|
||||
it.each(['constructor', '__proto__', 'prototype', '__defineGetter__', '__lookupGetter__'])(
|
||||
'should not read an own "%s" key of the inputs',
|
||||
(key) => {
|
||||
// JSON.parse creates own properties, even for `__proto__`.
|
||||
const inputs = JSON.parse(`{ "data": { "${key}": "value" }, "${key}": "value" }`);
|
||||
|
||||
expect(() => runtime.evaluate(`data["${key}"]`, inputs)).toThrow(ExpressionError);
|
||||
expect(() => runtime.evaluate(key, inputs)).toThrow(ExpressionError);
|
||||
expect(runtime.evaluateBoolean(`data["${key}"] === "value"`, inputs)).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it('should limit the number of callback calls per evaluation', () => {
|
||||
const condition = 'items.map(item => item).length > 0';
|
||||
const atLimit = { items: Array.from({ length: MAX_CALLBACK_CALLS }, (_, i) => i) };
|
||||
const overLimit = { items: [...atLimit.items, MAX_CALLBACK_CALLS] };
|
||||
|
||||
expect(runtime.evaluate(condition, atLimit)).toBe(true);
|
||||
// The budget is per evaluation, not shared across evaluations.
|
||||
expect(runtime.evaluate(condition, atLimit)).toBe(true);
|
||||
expect(() => runtime.evaluate(condition, overLimit)).toThrow(ExpressionError);
|
||||
expect(runtime.evaluateBoolean(condition, overLimit)).toBe(false);
|
||||
});
|
||||
|
||||
it('should only allow the standard library offered in the editor and reviewed additions', () => {
|
||||
const stringMethods = SymbolString().methods.map((method) => method.name);
|
||||
const arrayMethods = SymbolArray({ items: SymbolString() }).methods.map(
|
||||
(method) => method.name
|
||||
);
|
||||
|
||||
// Extend only after reviewing that a method is safe (see `ALLOWED_METHODS`).
|
||||
expect(ALLOWED_METHODS.string).toEqual(new Set(stringMethods));
|
||||
expect(ALLOWED_METHODS.array).toEqual(new Set([...arrayMethods, 'filter', 'map']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
} 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';
|
||||
@@ -312,7 +311,7 @@ export class AutoComplete {
|
||||
node: BinaryExpression,
|
||||
cursorOffset: number,
|
||||
context: SymbolsTable
|
||||
): Array<AutocompleteLiteralValueSuggestion> {
|
||||
): AutocompleteLiteralValueSuggestion[] {
|
||||
const { left, right } = node;
|
||||
|
||||
if (left.type !== 'MemberExpression' && left.type !== 'Identifier') {
|
||||
@@ -431,7 +430,7 @@ export class AutoComplete {
|
||||
node: AnyNode,
|
||||
cursorOffset: number,
|
||||
context: SymbolsTable
|
||||
): Array<AutocompleteOperatorSuggestion> {
|
||||
): AutocompleteOperatorSuggestion[] {
|
||||
if (node.type === 'Literal') {
|
||||
const parent = findParentNode(node, ast);
|
||||
|
||||
@@ -496,7 +495,7 @@ export class AutoComplete {
|
||||
*/
|
||||
private getOperatorSuggestionsForSymbol(
|
||||
symbol: ExtractSymbolDef<SymbolType>
|
||||
): Array<AutocompleteOperatorSuggestion> {
|
||||
): AutocompleteOperatorSuggestion[] {
|
||||
switch (symbol.type) {
|
||||
case SymbolType.Number:
|
||||
case SymbolType.Boolean:
|
||||
@@ -526,7 +525,7 @@ export class AutoComplete {
|
||||
case SymbolType.Function:
|
||||
return this.getOperatorSuggestionsForSymbol(symbol.returns);
|
||||
case SymbolType.Union: {
|
||||
return symbol.members.reduce<Array<AutocompleteOperatorSuggestion>>((prev, cur) => {
|
||||
return symbol.members.reduce<AutocompleteOperatorSuggestion[]>((prev, cur) => {
|
||||
prev.push(...this.getOperatorSuggestionsForSymbol(cur));
|
||||
return prev;
|
||||
}, []);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
|
||||
import { filterOutNullable } from './utils';
|
||||
|
||||
type InputValuesType =
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import type {
|
||||
ArrowFunctionExpression,
|
||||
CallExpression,
|
||||
Expression,
|
||||
Identifier,
|
||||
Literal,
|
||||
MemberExpression,
|
||||
Node,
|
||||
UnaryExpression,
|
||||
} from 'acorn';
|
||||
import evalESTreeExpr from 'eval-estree-expression';
|
||||
|
||||
import {
|
||||
ALLOWED_METHODS,
|
||||
CALLBACK_METHODS,
|
||||
MAX_CALLBACK_CALLS,
|
||||
UNSAFE_KEYS,
|
||||
UnsafeExpressionError,
|
||||
getCallbackParams,
|
||||
} from './rules';
|
||||
|
||||
/**
|
||||
* Evaluate a parsed expression with the restrictions, enforcing the rules that depend on values.
|
||||
*/
|
||||
export function evaluateRestricted(expression: Expression, inputs: object): unknown {
|
||||
return evalESTreeExpr.evaluate.sync<Expression>(expression, inputs, {
|
||||
// The library's own function support compiles code with `Function()`: calls and
|
||||
// arrow functions are only handled by our visitors.
|
||||
functions: false,
|
||||
withMembers: true,
|
||||
visitors: RESTRICTED_VISITORS,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback calls so far, per evaluator (the library creates one per evaluation).
|
||||
*/
|
||||
const callbackCalls = new WeakMap<object, number>();
|
||||
|
||||
type Visit = (node: Node, context: object, parent?: Node) => unknown;
|
||||
type Evaluator = { visit: Visit };
|
||||
|
||||
/**
|
||||
* Visitors overriding eval-estree-expression's defaults so that expressions can only read own
|
||||
* properties of plain data and call the methods of the standard library.
|
||||
*/
|
||||
const RESTRICTED_VISITORS = {
|
||||
Identifier(this: Evaluator, node: Identifier, context: object): unknown {
|
||||
return readVariable(context, node.name);
|
||||
},
|
||||
|
||||
MemberExpression(this: Evaluator, node: MemberExpression, context: object): unknown {
|
||||
const object = this.visit(node.object, context, node);
|
||||
const key = node.computed
|
||||
? this.visit(node.property, context, node)
|
||||
: (node.property as Identifier).name;
|
||||
|
||||
// Reject rather than coerce, so that keys like `["constructor"]` can't slip through.
|
||||
if (typeof key !== 'string' && typeof key !== 'number') {
|
||||
throw new UnsafeExpressionError('Property keys must be strings or numbers');
|
||||
}
|
||||
return readOwnProperty(object, String(key));
|
||||
},
|
||||
|
||||
CallExpression(this: Evaluator, node: CallExpression, context: object): unknown {
|
||||
const callee = node.callee;
|
||||
if (callee.type !== 'MemberExpression' || callee.computed || callee.optional) {
|
||||
throw new UnsafeExpressionError('Only method calls are supported');
|
||||
}
|
||||
|
||||
const method = (callee.property as Identifier).name;
|
||||
const receiver = this.visit(callee.object, context, callee);
|
||||
|
||||
// Like reading a property of a missing claim, calling a method on one gives `undefined`.
|
||||
if (
|
||||
(receiver === null || receiver === undefined) &&
|
||||
(ALLOWED_METHODS.string.has(method) || ALLOWED_METHODS.array.has(method))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fn = getAllowedMethod(receiver, method);
|
||||
if (!fn) {
|
||||
throw new UnsafeExpressionError(`Method "${method}" is not supported`);
|
||||
}
|
||||
|
||||
if (CALLBACK_METHODS.has(method)) {
|
||||
const [callback] = node.arguments;
|
||||
if (node.arguments.length !== 1 || callback?.type !== 'ArrowFunctionExpression') {
|
||||
throw new UnsafeExpressionError(`"${method}" only accepts an arrow function`);
|
||||
}
|
||||
return Reflect.apply(fn, receiver, [createCallback(this, callback, context)]);
|
||||
}
|
||||
|
||||
const args = node.arguments.map((arg) => {
|
||||
if (arg.type === 'SpreadElement') {
|
||||
throw new UnsafeExpressionError('Spread arguments are not supported');
|
||||
}
|
||||
const value = this.visit(arg, context, node);
|
||||
if (!isPrimitive(value)) {
|
||||
throw new UnsafeExpressionError('Only primitive values can be arguments');
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
return Reflect.apply(fn, receiver, args);
|
||||
},
|
||||
|
||||
ArrowFunctionExpression(): never {
|
||||
throw new UnsafeExpressionError('Arrow functions are only supported as callbacks');
|
||||
},
|
||||
|
||||
FunctionExpression(): never {
|
||||
throw new UnsafeExpressionError('Functions are not supported');
|
||||
},
|
||||
|
||||
ObjectExpression(): never {
|
||||
throw new UnsafeExpressionError('Object literals are not supported');
|
||||
},
|
||||
|
||||
Literal(node: Literal): unknown {
|
||||
// Regexes are only useful to the library's `=~` operator, and BigInt exponentiation can hang.
|
||||
if (node.regex || typeof node.value === 'bigint') {
|
||||
throw new UnsafeExpressionError('Regex and BigInt literals are not supported');
|
||||
}
|
||||
return node.value;
|
||||
},
|
||||
|
||||
UnaryExpression(this: Evaluator, node: UnaryExpression, context: object): unknown {
|
||||
// The library implements `delete` by mutating the inputs.
|
||||
if (node.operator === 'delete') {
|
||||
throw new UnsafeExpressionError('"delete" is not supported');
|
||||
}
|
||||
return Object.getPrototypeOf(this).UnaryExpression.call(this, node, context);
|
||||
},
|
||||
|
||||
AssignmentExpression(): never {
|
||||
throw new UnsafeExpressionError('Assignments are not supported');
|
||||
},
|
||||
|
||||
UpdateExpression(): never {
|
||||
throw new UnsafeExpressionError('Update expressions are not supported');
|
||||
},
|
||||
|
||||
NewExpression(): never {
|
||||
throw new UnsafeExpressionError('"new" is not supported');
|
||||
},
|
||||
|
||||
TaggedTemplateExpression(): never {
|
||||
throw new UnsafeExpressionError('Tagged templates are not supported');
|
||||
},
|
||||
|
||||
ThisExpression(): never {
|
||||
throw new UnsafeExpressionError('"this" is not supported');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a callback evaluating the arrow function's body with the evaluator, instead of compiling it.
|
||||
*/
|
||||
function createCallback(evaluator: Evaluator, node: ArrowFunctionExpression, context: object) {
|
||||
const params = getCallbackParams(node);
|
||||
|
||||
return (...args: unknown[]) => {
|
||||
const calls = (callbackCalls.get(evaluator) ?? 0) + 1;
|
||||
if (calls > MAX_CALLBACK_CALLS) {
|
||||
throw new UnsafeExpressionError('Too many callback calls');
|
||||
}
|
||||
callbackCalls.set(evaluator, calls);
|
||||
|
||||
const variables = new Map(params.map((name, index) => [name, args[index]]));
|
||||
return evaluator.visit(node.body, new CallbackScope(variables, context), node);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Variables of a callback call, falling back to the enclosing scope.
|
||||
* A Map avoids copying the inputs on each call.
|
||||
*/
|
||||
class CallbackScope {
|
||||
constructor(
|
||||
readonly variables: Map<string, unknown>,
|
||||
readonly parent: object
|
||||
) {}
|
||||
}
|
||||
|
||||
function readVariable(scope: object, name: string): unknown {
|
||||
if (UNSAFE_KEYS.has(name)) {
|
||||
throw new UnsafeExpressionError(`"${name}" is not accessible`);
|
||||
}
|
||||
if (scope instanceof CallbackScope) {
|
||||
return scope.variables.has(name)
|
||||
? scope.variables.get(name)
|
||||
: readVariable(scope.parent, name);
|
||||
}
|
||||
return readOwnProperty(scope, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an own property, treating any attempt to reach an inherited one (e.g. `constructor`) as unsafe.
|
||||
*/
|
||||
function readOwnProperty(object: unknown, key: string): unknown {
|
||||
if (UNSAFE_KEYS.has(key)) {
|
||||
throw new UnsafeExpressionError(`Property "${key}" is not accessible`);
|
||||
}
|
||||
if (object === null || object === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof object === 'function') {
|
||||
throw new UnsafeExpressionError('Functions are not accessible');
|
||||
}
|
||||
|
||||
// Wrapping primitives exposes string indexes and `length` as own properties.
|
||||
const target = Object(object);
|
||||
if (Object.hasOwn(target, key)) {
|
||||
return target[key];
|
||||
}
|
||||
if (key in target) {
|
||||
throw new UnsafeExpressionError(`Property "${key}" is not accessible`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getAllowedMethod(receiver: unknown, method: string): Function | null {
|
||||
if (typeof receiver === 'string' && ALLOWED_METHODS.string.has(method)) {
|
||||
return Reflect.get(String.prototype, method);
|
||||
}
|
||||
if (Array.isArray(receiver) && ALLOWED_METHODS.array.has(method)) {
|
||||
return Reflect.get(Array.prototype, method);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isPrimitive(value: unknown): boolean {
|
||||
return value === null || (typeof value !== 'object' && typeof value !== 'function');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { evaluateRestricted } from './evaluate';
|
||||
export { ALLOWED_METHODS, MAX_CALLBACK_CALLS, UnsafeExpressionError } from './rules';
|
||||
export { validateExpression } from './validate';
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { AnyNode, ArrowFunctionExpression } from 'acorn';
|
||||
|
||||
import { ExpressionError } from '../errors';
|
||||
|
||||
/**
|
||||
* Methods that expressions are allowed to call, per receiver type.
|
||||
* Must include the standard library offered in the editor (see `symbols.ts`). Only add methods
|
||||
* after review: they must not mutate their receiver, and their output must stay bounded by their input.
|
||||
*/
|
||||
export const ALLOWED_METHODS = {
|
||||
string: new Set([
|
||||
'at',
|
||||
'endsWith',
|
||||
'includes',
|
||||
'startsWith',
|
||||
'toLowerCase',
|
||||
'toUpperCase',
|
||||
'trim',
|
||||
]),
|
||||
array: new Set(['at', 'every', 'filter', 'includes', 'map', 'some']),
|
||||
};
|
||||
|
||||
/**
|
||||
* Array methods taking a callback, the only place where arrow functions are allowed.
|
||||
*/
|
||||
export const CALLBACK_METHODS = new Set(['every', 'filter', 'map', 'some']);
|
||||
|
||||
/**
|
||||
* Maximum number of callback calls in a single evaluation, as nested callbacks multiply and
|
||||
* visitors control the size of unsigned claims. Enough for a pass over an identity provider's
|
||||
* group claim (capped around 200 entries), or a small nested pair of arrays.
|
||||
*/
|
||||
export const MAX_CALLBACK_CALLS = 250;
|
||||
|
||||
/**
|
||||
* Names never readable as variables or properties, even as own properties of the inputs.
|
||||
* Replaces (and extends) the library's own denylist, as our visitors override the ones applying it.
|
||||
*/
|
||||
export const UNSAFE_KEYS = new Set([
|
||||
'__defineGetter__',
|
||||
'__defineSetter__',
|
||||
'__lookupGetter__',
|
||||
'__lookupSetter__',
|
||||
'__proto__',
|
||||
'constructor',
|
||||
'prototype',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Thrown when an expression uses something the restrictions don't allow.
|
||||
*/
|
||||
export class UnsafeExpressionError extends ExpressionError {}
|
||||
|
||||
/**
|
||||
* Create an error pointing at the node, so that editors can highlight it.
|
||||
*/
|
||||
export function unsupported(node: AnyNode, message: string): UnsafeExpressionError {
|
||||
return new UnsafeExpressionError(message, node.loc?.start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter names of an arrow function, if it has the shape allowed for callbacks.
|
||||
*/
|
||||
export function getCallbackParams(node: ArrowFunctionExpression): string[] {
|
||||
if (node.async) {
|
||||
throw unsupported(node, 'Async functions are not supported');
|
||||
}
|
||||
if (!node.expression) {
|
||||
throw unsupported(node, 'Only single-expression arrow functions are supported');
|
||||
}
|
||||
|
||||
return node.params.map((param) => {
|
||||
if (param.type !== 'Identifier' || UNSAFE_KEYS.has(param.name)) {
|
||||
throw unsupported(param, 'Only simple arrow function parameters are supported');
|
||||
}
|
||||
return param.name;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { AnyNode, CallExpression, Identifier } from 'acorn';
|
||||
|
||||
import {
|
||||
ALLOWED_METHODS,
|
||||
CALLBACK_METHODS,
|
||||
UNSAFE_KEYS,
|
||||
getCallbackParams,
|
||||
unsupported,
|
||||
} from './rules';
|
||||
|
||||
/**
|
||||
* Check an expression against the restrictions without evaluating it, so that editors can report
|
||||
* violations. Rules depending on values (receiver types, inherited properties, limits) are only
|
||||
* enforced by `evaluateRestricted`.
|
||||
*/
|
||||
export function validateExpression(node: AnyNode): void {
|
||||
switch (node.type) {
|
||||
case 'Literal':
|
||||
if (node.regex || typeof node.value === 'bigint') {
|
||||
throw unsupported(
|
||||
node,
|
||||
'Regular expressions and BigInt literals are not supported'
|
||||
);
|
||||
}
|
||||
return;
|
||||
case 'Identifier':
|
||||
return assertSafeKey(node.name, node);
|
||||
case 'TemplateLiteral':
|
||||
return node.expressions.forEach(validateExpression);
|
||||
case 'ArrayExpression':
|
||||
for (const element of node.elements) {
|
||||
if (element) {
|
||||
validateExpression(
|
||||
element.type === 'SpreadElement' ? element.argument : element
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case 'UnaryExpression':
|
||||
if (node.operator === 'delete') {
|
||||
throw unsupported(node, '"delete" is not supported');
|
||||
}
|
||||
return validateExpression(node.argument);
|
||||
case 'BinaryExpression':
|
||||
case 'LogicalExpression':
|
||||
validateExpression(node.left);
|
||||
return validateExpression(node.right);
|
||||
case 'ConditionalExpression':
|
||||
validateExpression(node.test);
|
||||
validateExpression(node.consequent);
|
||||
return validateExpression(node.alternate);
|
||||
case 'SequenceExpression':
|
||||
return node.expressions.forEach(validateExpression);
|
||||
case 'MemberExpression':
|
||||
validateExpression(node.object);
|
||||
if (!node.computed) {
|
||||
return assertSafeKey((node.property as Identifier).name, node.property);
|
||||
}
|
||||
if (node.property.type === 'Literal') {
|
||||
return assertSafeKey(String(node.property.value), node.property);
|
||||
}
|
||||
if (node.property.type === 'ArrayExpression') {
|
||||
throw unsupported(node.property, 'Property keys must be strings or numbers');
|
||||
}
|
||||
return validateExpression(node.property);
|
||||
case 'CallExpression':
|
||||
return validateCall(node);
|
||||
case 'ArrowFunctionExpression':
|
||||
throw unsupported(node, 'Arrow functions are only supported as callbacks');
|
||||
case 'ObjectExpression':
|
||||
throw unsupported(node, 'Object literals are not supported');
|
||||
case 'AssignmentExpression':
|
||||
// `a =~ /regex/` parses as an assignment of `~/regex/`.
|
||||
throw unsupported(
|
||||
node,
|
||||
node.right.type === 'UnaryExpression' && node.right.operator === '~'
|
||||
? 'The "=~" regex operator is not supported'
|
||||
: 'Assignments are not supported'
|
||||
);
|
||||
default:
|
||||
throw unsupported(
|
||||
node,
|
||||
UNSUPPORTED_SYNTAX[node.type] ?? `${node.type} is not supported`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors shown to authors for rejected syntax.
|
||||
*/
|
||||
const UNSUPPORTED_SYNTAX: Partial<Record<AnyNode['type'], string>> = {
|
||||
AwaitExpression: '"await" is not supported',
|
||||
ChainExpression: 'Optional chaining ("?.") is not supported',
|
||||
ClassExpression: 'Classes are not supported',
|
||||
FunctionExpression: 'Functions are not supported',
|
||||
ImportExpression: '"import" is not supported',
|
||||
NewExpression: '"new" is not supported',
|
||||
TaggedTemplateExpression: 'Tagged templates are not supported',
|
||||
ThisExpression: '"this" is not supported',
|
||||
UpdateExpression: '"++" and "--" are not supported',
|
||||
YieldExpression: '"yield" is not supported',
|
||||
};
|
||||
|
||||
function validateCall(node: CallExpression): void {
|
||||
const callee = node.callee;
|
||||
if (callee.type !== 'MemberExpression' || callee.computed || node.optional) {
|
||||
throw unsupported(node, 'Only method calls are supported');
|
||||
}
|
||||
validateExpression(callee.object);
|
||||
|
||||
const method = (callee.property as Identifier).name;
|
||||
if (!ALLOWED_METHODS.string.has(method) && !ALLOWED_METHODS.array.has(method)) {
|
||||
throw unsupported(callee.property, `Method "${method}" is not supported`);
|
||||
}
|
||||
|
||||
if (CALLBACK_METHODS.has(method)) {
|
||||
const [callback] = node.arguments;
|
||||
if (node.arguments.length !== 1 || callback?.type !== 'ArrowFunctionExpression') {
|
||||
throw unsupported(node, `"${method}" only accepts an arrow function`);
|
||||
}
|
||||
getCallbackParams(callback);
|
||||
return validateExpression(callback.body);
|
||||
}
|
||||
|
||||
for (const arg of node.arguments) {
|
||||
if (arg.type === 'SpreadElement') {
|
||||
throw unsupported(arg, 'Spread arguments are not supported');
|
||||
}
|
||||
validateExpression(arg);
|
||||
}
|
||||
}
|
||||
|
||||
function assertSafeKey(key: string, node: AnyNode): void {
|
||||
if (UNSAFE_KEYS.has(key)) {
|
||||
throw unsupported(node, `"${key}" is not accessible`);
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
import { parse as parseLoose } from 'acorn-loose';
|
||||
import escodegen from 'escodegen';
|
||||
import evalESTreeExpr from 'eval-estree-expression';
|
||||
const { evaluate, variables } = evalESTreeExpr;
|
||||
const { variables } = evalESTreeExpr;
|
||||
|
||||
import { AutoComplete } from './autocomplete';
|
||||
import { ExpressionError } from './errors';
|
||||
import { UnsafeExpressionError, evaluateRestricted, validateExpression } from './restricted';
|
||||
import type { SymbolsTable } from './symbols';
|
||||
import type { TemplatePart } from './template';
|
||||
import { parseTemplate as parseTemplateParts } from './template';
|
||||
@@ -39,6 +40,7 @@ export class ExpressionRuntime {
|
||||
|
||||
/**
|
||||
* Evaluates an expression based on the given inputs/context.
|
||||
* Throws an `ExpressionError` for invalid expressions, including those the restrictions don't allow.
|
||||
*/
|
||||
public evaluate(expr: string, inputs: object): unknown {
|
||||
try {
|
||||
@@ -48,12 +50,12 @@ export class ExpressionRuntime {
|
||||
throw new ExpressionError('Invalid nodes found when parsing');
|
||||
}
|
||||
|
||||
return evaluate.sync<Expression>(parsed.result, inputs, {
|
||||
functions: true,
|
||||
withMembers: true,
|
||||
generate: escodegen.generate,
|
||||
});
|
||||
return evaluateRestricted(parsed.result, inputs);
|
||||
} catch (error) {
|
||||
// Keep the location, so that editors can point at the offending token.
|
||||
if (error instanceof UnsafeExpressionError) {
|
||||
throw error;
|
||||
}
|
||||
throw error instanceof Error
|
||||
? new ExpressionError(error.message)
|
||||
: new ExpressionError('Unexpected error');
|
||||
@@ -213,6 +215,11 @@ export class ExpressionRuntime {
|
||||
// Return information on the other nodes as invalid nodes
|
||||
const invalidNodes = ast.body.filter(filterOutModuleDeclarationStatement);
|
||||
|
||||
// Loose parsing serves autocomplete on partial expressions, which are expected to be incomplete.
|
||||
if (!options.loose) {
|
||||
validateRestrictedExpression(expr, statement.expression);
|
||||
}
|
||||
|
||||
return {
|
||||
result: statement.expression,
|
||||
invalidNodes,
|
||||
@@ -265,6 +272,25 @@ function createExpressionErrorFromSyntaxError(
|
||||
|
||||
return new ExpressionError(errorMessage, loc, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an expression against the restrictions, pointing the error at the offending token for editors.
|
||||
*/
|
||||
function validateRestrictedExpression(code: string, expression: Expression): void {
|
||||
try {
|
||||
validateExpression(expression);
|
||||
} catch (error) {
|
||||
if (error instanceof UnsafeExpressionError && error.location) {
|
||||
throw new UnsafeExpressionError(
|
||||
error.message,
|
||||
error.location,
|
||||
getTokenAtLoc(code, error.location)
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function getTokenAtLoc(code: string, errorLoc: Position): Token | undefined {
|
||||
const tokens = tokenizer(code, {
|
||||
ecmaVersion: 'latest',
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface ExpressionParserResult {
|
||||
/**
|
||||
* The information of the invalid (non-expression) nodes found from the other portions of the parsed expression.
|
||||
*/
|
||||
invalidNodes: Array<ExpressionStatement>;
|
||||
invalidNodes: ExpressionStatement[];
|
||||
}
|
||||
|
||||
export interface ExpressionAutocompleteResults {
|
||||
@@ -158,11 +158,11 @@ export interface AutocompleteSymbolSuggestion {
|
||||
symbol: SymbolInfo;
|
||||
}
|
||||
|
||||
export type AutocompleteSuggestions = Array<
|
||||
export type AutocompleteSuggestions = (
|
||||
| AutocompleteSymbolSuggestion
|
||||
| AutocompleteLiteralValueSuggestion
|
||||
| AutocompleteOperatorSuggestion
|
||||
>;
|
||||
)[];
|
||||
|
||||
type LoggerFn = (message: string, ...args: any[]) => void;
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ declare module 'eval-estree-expression' {
|
||||
* Used with the variables method to return nested variables (e.g., variables with dot notation, like foo.bar.baz). Default: undefined
|
||||
*/
|
||||
withMembers?: boolean;
|
||||
/**
|
||||
* Custom visitors by node type, taking precedence over the built-in ones. Called with the evaluator as `this`.
|
||||
*/
|
||||
visitors?: Record<string, Function>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { APIv2 } from 'google-font-metadata';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { APIv2 } from 'google-font-metadata';
|
||||
|
||||
import { CustomizationDefaultFont } from '@gitbook/api';
|
||||
|
||||
import type { FontDefinitions } from '../src/types';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user