diff --git a/bun.lock b/bun.lock index 6db506b05..a983f15be 100644 --- a/bun.lock +++ b/bun.lock @@ -57,6 +57,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/typography": "^0.5.16", "@upstash/redis": "^1.27.1", + "ai": "^4.1.46", "ajv": "^8.12.0", "assert-never": "^1.2.1", "bun-types": "^1.1.20", @@ -122,7 +123,7 @@ "dependencies": { "@gitbook/api": "0.96.1", "@sindresorhus/fnv1a": "^3.1.0", - "next": "canary", + "next": "^15.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", "server-only": "^0.0.1", @@ -138,7 +139,7 @@ "name": "@gitbook/icons", "version": "0.1.0", "bin": { - "gitbook-icons": "./bin/gitbook-icons.js" + "gitbook-icons": "./bin/gitbook-icons.js", }, "dependencies": { "@fortawesome/fontawesome-free": "^6.6.0", @@ -196,7 +197,7 @@ "name": "@gitbook/react-math", "version": "0.6.0", "bin": { - "gitbook-math": "./bin/gitbook-math.js" + "gitbook-math": "./bin/gitbook-math.js", }, "dependencies": { "object-hash": "^3.0.0", @@ -243,6 +244,14 @@ "react-dom": "18.3.1", }, "packages": { + "@ai-sdk/provider": ["@ai-sdk/provider@1.0.9", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.1.10", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "eventsource-parser": "^3.0.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-4GZ8GHjOFxePFzkl3q42AU0DQOtTQ5w09vmaWUf/pKFXJPizlnzKSUkF0f+VkapIUfDugyMqPMT1ge8XQzVI7Q=="], + + "@ai-sdk/react": ["@ai-sdk/react@1.1.19", "", { "dependencies": { "@ai-sdk/provider-utils": "2.1.10", "@ai-sdk/ui-utils": "1.1.16", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-zqSOWmJxpB45ZrwZ04+Q7Uo3xeGM0tva2eUYz2T4gv9Yk6MQAfOBA6+scsKg8CyIuUy4M4/C4pCY3eWQf7sfQg=="], + + "@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.1.16", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "@ai-sdk/provider-utils": "2.1.10", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-jfblR2yZVISmNK2zyNzJZFtkgX57WDAUQXcmn3XUBJyo8LFsADu+/vYMn5AOyBi9qJT0RBk11PEtIxIqvByw3Q=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], @@ -1351,6 +1360,8 @@ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="], + "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], @@ -1531,6 +1542,8 @@ "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + "ai": ["ai@4.1.47", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "@ai-sdk/provider-utils": "2.1.10", "@ai-sdk/react": "1.1.19", "@ai-sdk/ui-utils": "1.1.16", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-9UZ8Mkv1HlprCJfQ0Kq+rgKbfkrkDtJjslr1WOHBRzvC70jDJJYp8r0Qq4vlF7zs9VBkGyy8Rhm5zQJJIUjvgw=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -1773,6 +1786,8 @@ "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], + "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], @@ -2195,6 +2210,8 @@ "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-to-ts": ["json-schema-to-ts@1.6.4", "", { "dependencies": { "@types/json-schema": "^7.0.6", "ts-toolbelt": "^6.15.5" } }, "sha512-pR4yQ9DHz6itqswtHCm26mw45FSNfQ9rEQjosaZErhn5J3J2sIViQiz8rDaezjKAhFGpmsoczYVBgGHzFw/stA=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -2205,6 +2222,8 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], @@ -2747,6 +2766,8 @@ "schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], + "semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], "semver-diff": ["semver-diff@3.1.1", "", { "dependencies": { "semver": "^6.3.0" } }, "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg=="], @@ -2857,6 +2878,8 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "swr": ["swr@2.3.2", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA=="], + "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="], "tailwind-merge": ["tailwind-merge@2.5.5", "", {}, "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA=="], @@ -2881,6 +2904,8 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], + "time-span": ["time-span@4.0.0", "", { "dependencies": { "convert-hrtime": "^3.0.0" } }, "sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g=="], "timers-ext": ["timers-ext@0.1.8", "", { "dependencies": { "es5-ext": "^0.10.64", "next-tick": "^1.1.0" } }, "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww=="], @@ -3099,10 +3124,14 @@ "zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], + "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], + "zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + "@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@argos-ci/core/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -4239,7 +4268,7 @@ "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "gitbook-v2/next": ["next@15.2.1-canary.2", "", { "dependencies": { "@next/env": "15.2.1-canary.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.1-canary.2", "@next/swc-darwin-x64": "15.2.1-canary.2", "@next/swc-linux-arm64-gnu": "15.2.1-canary.2", "@next/swc-linux-arm64-musl": "15.2.1-canary.2", "@next/swc-linux-x64-gnu": "15.2.1-canary.2", "@next/swc-linux-x64-musl": "15.2.1-canary.2", "@next/swc-win32-arm64-msvc": "15.2.1-canary.2", "@next/swc-win32-x64-msvc": "15.2.1-canary.2", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Vrl8pSN3I7fuUWYWSqbzRsItKYm0X0OXPwm6/DuX7Pgi6uyYT6uwpvEGiPHSGV5Up1MOOtlsCjIbEQlNJ3t1pg=="], + "gitbook-v2/next": ["next@15.2.0", "", { "dependencies": { "@next/env": "15.2.0", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0", "@next/swc-darwin-x64": "15.2.0", "@next/swc-linux-arm64-gnu": "15.2.0", "@next/swc-linux-arm64-musl": "15.2.0", "@next/swc-linux-x64-gnu": "15.2.0", "@next/swc-linux-x64-musl": "15.2.0", "@next/swc-win32-arm64-msvc": "15.2.0", "@next/swc-win32-x64-msvc": "15.2.0", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ=="], "google-auth-library/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], @@ -5139,23 +5168,23 @@ "gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "gitbook-v2/next/@next/env": ["@next/env@15.2.1-canary.2", "", {}, "sha512-8bs9W8rN9ln19PA3IdglEkuRxNI+VAjY/Gxkkp24qpSO19uKHbORFz3I5DGyRzHzTRDpCRGHQDKNbcy2b0buOQ=="], + "gitbook-v2/next/@next/env": ["@next/env@15.2.0", "", {}, "sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA=="], - "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.1-canary.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PvoxT5tucaeREa7uhTxEaSW9sL4Sr8WfluVfSS+bj2axZpQ567gB/I+Eias67FPRW80cZaPMGsorNjyaq+cJuA=="], + "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA=="], - "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.1-canary.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-tPg9F3bT5MMalZpLfd+paiWWaWLvUaWAFZfIGEBgozYgIBd66pROkb+dXsJVd73IlYoBDh3mYcK7me2uYImBOA=="], + "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA=="], - "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.1-canary.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-LmnZN/psunvUXofINdGENFNDv7yRIET2szi2FREMr1epKL3NsAQD0k4hv3pwRLwkVb8z6Rj+TXzX0pFChHOlhg=="], + "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ=="], - "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.1-canary.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Ma1ATOCdgei1LRAbziBYciunyuG0uUASoiz7DDw40Ox5YuDD4Tznbg3re5ltWRwMAl9NNQa8ns3u+Q3Zs1NsQ=="], + "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw=="], - "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.1-canary.2", "", { "os": "linux", "cpu": "x64" }, "sha512-R8bbqvKkVtzjN/KWogQQvWXgBTyWXcOEVgiPMXKgx7/zfybWeL+WjSXeepw4xYyDlQ1vC//HwEqLfFX41Jht4Q=="], + "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q=="], - "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.1-canary.2", "", { "os": "linux", "cpu": "x64" }, "sha512-wWr41f7g3H5FcLguoAS7bvqSqaVkhK97/fk7ANG1l5yW13F9qVPboVZvgVFrRsPFZgqANxs4AmhKAjjjT+mL1g=="], + "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ=="], - "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.1-canary.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-ogzvioGSaJ9F2VWZXiLQR4K4G6fbBUdlZfKowOJyFKfBhPK+WuwJlEnzWfPAe5YjtiQU2H+eMdZdC25DXDXo6g=="], + "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw=="], - "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.1-canary.2", "", { "os": "win32", "cpu": "x64" }, "sha512-6sss7xGWck4DrJC7h/Gc2LFN3xh2dxnXxYmkxOD9gOq/yFdX7NSrNdCfNvXg5+6EFcTPURRg9gS9VgkzOCxp8Q=="], + "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw=="], "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 8470e4422..95097532c 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -3,7 +3,7 @@ "version": "0.1.1", "private": true, "dependencies": { - "next": "canary", + "next": "^15.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", "@gitbook/api": "0.96.1", diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/layout.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/layout.tsx index 67c5767f4..ae7727abf 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/layout.tsx +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/layout.tsx @@ -6,7 +6,7 @@ import { } from '@/components/SiteLayout'; import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils'; import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; -import { getThemeFromMiddleware } from '@v2/lib/middleware'; +import { getThemeFromMiddleware, getVisitorAuthTokenFromMiddleware } from '@v2/lib/middleware'; interface SiteDynamicLayoutProps { params: Promise; @@ -18,6 +18,7 @@ export default async function SiteDynamicLayout({ }: React.PropsWithChildren) { const context = await getDynamicSiteContext(await params); const forcedTheme = await getThemeFromMiddleware(); + const visitorAuthToken = await getVisitorAuthTokenFromMiddleware(); return ( @@ -25,6 +26,7 @@ export default async function SiteDynamicLayout({ context={context} forcedTheme={forcedTheme} withTracking={!GITBOOK_DISABLE_TRACKING} + visitorAuthToken={visitorAuthToken} > {children} diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx index 08f83222f..9e8cb36aa 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx @@ -25,7 +25,11 @@ export default async function SiteStaticLayout({ return ( - + {children} diff --git a/packages/gitbook-v2/src/app/utils.ts b/packages/gitbook-v2/src/app/utils.ts index bb6826d6e..805d1c0f6 100644 --- a/packages/gitbook-v2/src/app/utils.ts +++ b/packages/gitbook-v2/src/app/utils.ts @@ -1,9 +1,9 @@ -import { fetchSiteContextByURL } from '@v2/lib/context'; -import { createDataFetcher } from '@v2/lib/data'; -import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from '@v2/lib/env'; -import { createImageResizer } from '@v2/lib/images'; -import { createLinker } from '@v2/lib/links'; -import { headers } from 'next/headers'; +import { + fetchSiteContextByURL, + fetchSiteContextByURLLookup, + getBaseContext, +} from '@v2/lib/context'; +import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware'; export type RouteParamMode = 'url-host' | 'url'; @@ -21,29 +21,19 @@ export type RouteParams = RouteLayoutParams & { /** * Get the static context when rendering statically a site. */ -export async function getStaticSiteContext(params: RouteLayoutParams) { - const url = getSiteURLFromParams(params); - - const dataFetcher = createDataFetcher(); - const { linker, host } = createLinkerFromParams(params); - const context = await fetchSiteContextByURL( +export function getStaticSiteContext(params: RouteLayoutParams) { + const siteURL = getSiteURLFromParams(params); + return fetchSiteContextByURL( + getBaseContext({ + siteURL, + urlMode: getModeFromParams(params.mode), + }), { - dataFetcher, - linker, - }, - { - url: url.toString(), + url: siteURL.toString(), visitorAuthToken: null, redirectOnError: false, } ); - - context.imageResizer = createImageResizer({ - host, - linker: context.linker, - }); - - return context; } /** @@ -51,36 +41,16 @@ export async function getStaticSiteContext(params: RouteLayoutParams) { * The context will depend on the request. */ export async function getDynamicSiteContext(params: RouteLayoutParams) { - const url = getSiteURLFromParams(params); - const headersSet = await headers(); + const siteURL = getSiteURLFromParams(params); + const siteURLData = await getSiteURLDataFromMiddleware(); - const dataFetcher = createDataFetcher({ - apiToken: headersSet.get('x-gitbook-token') ?? GITBOOK_API_TOKEN, - apiEndpoint: headersSet.get('x-gitbook-api') ?? GITBOOK_API_URL, - }); - - const { linker, host } = createLinkerFromParams(params); - - const context = await fetchSiteContextByURL( - { - dataFetcher, - linker, - }, - { - url: url.toString(), - visitorAuthToken: headersSet.get('x-gitbook-visitor-token'), - - // TODO: set it only when the token comes from the cookies. - redirectOnError: true, - } + return fetchSiteContextByURLLookup( + getBaseContext({ + siteURL, + urlMode: getModeFromParams(params.mode), + }), + siteURLData ); - - context.imageResizer = createImageResizer({ - host, - linker: context.linker, - }); - - return context; } /** @@ -91,39 +61,6 @@ export function getPagePathFromParams(params: RouteParams) { return decoded; } -function createLinkerFromParams(params: RouteLayoutParams) { - const url = getSiteURLFromParams(params); - const mode = getModeFromParams(params.mode); - - if (mode === 'url-host') { - return { - linker: createLinker({ - host: url.host, - pathname: '/', - }), - host: url.host, - }; - } - - const gitbookURL = new URL(GITBOOK_URL); - const linker = createLinker({ - protocol: gitbookURL.protocol, - host: gitbookURL.host, - pathname: `/url/${url.host}`, - }); - - // Create link in the same format for links to other sites/sections. - linker.toLinkForContent = (rawURL: string) => { - const urlObject = new URL(rawURL); - return `/url/${urlObject.host}${urlObject.pathname}`; - }; - - return { - linker, - host: gitbookURL.host, - }; -} - function getSiteURLFromParams(params: RouteLayoutParams) { const decoded = decodeURIComponent(params.siteURL); const url = new URL(`https://${decoded}`); diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts index 4b73d6d81..79aaf598c 100644 --- a/packages/gitbook-v2/src/lib/context.ts +++ b/packages/gitbook-v2/src/lib/context.ts @@ -1,6 +1,7 @@ import { getSiteStructureSections } from '@/lib/sites'; import type { ChangeRequest, + PublishedSiteContentLookup, RevisionPage, RevisionPageDocument, Site, @@ -15,8 +16,9 @@ import type { import { type GitBookDataFetcher, createDataFetcher } from '@v2/lib/data'; import { redirect } from 'next/navigation'; import { assert } from 'ts-essentials'; -import type { ImageResizer } from './images'; -import { type GitBookSpaceLinker, appendBasePathToLinker } from './links'; +import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from './env'; +import { type ImageResizer, createImageResizer } from './images'; +import { type GitBookSpaceLinker, createLinker } from './links'; /** * Generic context when rendering content. @@ -90,9 +92,6 @@ export type GitBookSiteContext = GitBookSpaceContext & { /** Scripts to load for the site. */ scripts: SiteIntegrationScript[]; - - /** Visitor token used to fetch the site */ - visitorAuthToken: string | null; }; /** @@ -102,6 +101,58 @@ export type GitBookPageContext = (GitBookSpaceContext | GitBookSiteContext) & { page: RevisionPageDocument; }; +/** + * Get the base context for a request. + */ +export function getBaseContext(input: { + siteURL: URL | string; + urlMode: 'url' | 'url-host'; + apiToken?: string | null; +}) { + const url = typeof input.siteURL === 'string' ? new URL(input.siteURL) : input.siteURL; + const urlMode = input.urlMode; + + const dataFetcher = createDataFetcher({ + apiToken: input.apiToken ?? GITBOOK_API_TOKEN, + apiEndpoint: GITBOOK_API_URL, + }); + const gitbookURL = new URL(GITBOOK_URL); + + const linker = + urlMode === 'url-host' + ? createLinker({ + host: url.host, + pathname: url.pathname, + }) + : createLinker({ + protocol: gitbookURL.protocol, + host: gitbookURL.host, + pathname: `/url/${url.host}${url.pathname}`, + }); + + if (urlMode === 'url') { + // Create link in the same format for links to other sites/sections. + linker.toLinkForContent = (rawURL: string) => { + const urlObject = new URL(rawURL); + return `/url/${urlObject.host}${urlObject.pathname}`; + }; + } + + const imageResizer = createImageResizer({ + host: urlMode === 'url-host' ? url.host : gitbookURL.host, + + // To ensure image resizing work for proxied sites, + // we serve images from the root of the site. + linker: linker, + }); + + return { + dataFetcher, + linker, + imageResizer, + }; +} + /** * Fetch the context of a site for a given URL and a base context. */ @@ -120,15 +171,25 @@ export async function fetchSiteContextByURL( redirectOnError: input.redirectOnError, }); + return fetchSiteContextByURLLookup(baseContext, data); +} + +/** + * Fetch the context of a site using the resolution of a URL + */ +export async function fetchSiteContextByURLLookup( + baseContext: GitBookBaseContext, + data: PublishedSiteContentLookup +): Promise { + const { dataFetcher } = baseContext; if ('redirect' in data) { redirect(data.redirect); } - const context = await fetchSiteContextByIds( + return await fetchSiteContextByIds( { ...baseContext, - dataFetcher: createDataFetcher({ - apiEndpoint: dataFetcher.apiEndpoint, + dataFetcher: dataFetcher.withToken({ apiToken: data.apiToken, }), }, @@ -141,16 +202,8 @@ export async function fetchSiteContextByURL( shareKey: data.shareKey, changeRequest: data.changeRequest, revision: data.revision, - visitorAuthToken: input.visitorAuthToken, } ); - - const siteContext = { - ...context, - linker: appendBasePathToLinker(context.linker, data.basePath), - }; - - return siteContext; } /** @@ -167,7 +220,6 @@ export async function fetchSiteContextByIds( shareKey: string | undefined; changeRequest: string | undefined; revision: string | undefined; - visitorAuthToken: string | null; } ): Promise { const { dataFetcher } = baseContext; @@ -233,7 +285,6 @@ export async function fetchSiteContextByIds( structure: siteStructure, sections, scripts, - visitorAuthToken: ids.visitorAuthToken, }; } diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 738eeec50..72698b430 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -35,6 +35,17 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo return { apiEndpoint: input.apiEndpoint, + async api() { + return getAPI(input); + }, + + withToken({ apiToken }) { + return createDataFetcher({ + ...input, + apiToken, + }); + }, + // // API that are tied to the token // @@ -124,6 +135,9 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo spaceId: params.spaceId, }); }, + searchSiteContent(params) { + return searchSiteContent(input, params); + }, // // API that are not tied to the token @@ -498,6 +512,23 @@ async function getEmbedByUrl( return res.data; } +async function searchSiteContent( + input: DataFetcherInput, + params: Parameters[0] +) { + 'use cache'; + + const { organizationId, siteId, query, scope } = params; + + cacheLife('days'); + + const res = await getAPI(input).orgs.searchSiteContent(organizationId, siteId, { + query, + ...scope, + }); + return res.data.items; +} + function getAPI(input: DataFetcherInput) { const { apiEndpoint, apiToken } = input; const api = new GitBookAPI({ diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts index f52a1310a..0a0bbe2a0 100644 --- a/packages/gitbook-v2/src/lib/data/types.ts +++ b/packages/gitbook-v2/src/lib/data/types.ts @@ -10,6 +10,18 @@ export interface GitBookDataFetcher { */ apiEndpoint: string; + /** + * Get an API client for the current context. + */ + api(): Promise; + + /** + * Create a data fetcher authenticated with a specific token. + */ + withToken(input: { + apiToken: string; + }): GitBookDataFetcher; + /** * Get a user by its ID. */ @@ -126,4 +138,19 @@ export interface GitBookDataFetcher { * Get an embed by its URL. */ getEmbedByUrl(params: { url: string; spaceId: string }): Promise; + + /** + * Search content in a site. + */ + searchSiteContent(params: { + organizationId: string; + siteId: string; + query: string; + scope: + | { mode: 'all' } + | { mode: 'current'; siteSpaceId: string } + | { mode: 'specific'; siteSpaceIds: string[] }; + /** Cache bust to ensure the search results are fresh when the space is updated. */ + cacheBust?: string; + }): Promise; } diff --git a/packages/gitbook-v2/src/lib/middleware.ts b/packages/gitbook-v2/src/lib/middleware.ts index d5e8f7cef..f2bbd6cc7 100644 --- a/packages/gitbook-v2/src/lib/middleware.ts +++ b/packages/gitbook-v2/src/lib/middleware.ts @@ -1,14 +1,51 @@ -import { CustomizationThemeMode } from '@gitbook/api'; +import { CustomizationThemeMode, type PublishedSiteContent } from '@gitbook/api'; import { headers } from 'next/headers'; export enum MiddlewareHeaders { + /** + * Type of the route (static or dynamic) + */ + RouteType = 'x-gitbook-route-type', + + /** + * The URL of the site (without the pathname) + */ + SiteURL = 'x-gitbook-site-url', + + /** + * The data associated with the URL. + */ + SiteURLData = 'x-gitbook-site-url-data', + + /** + * The mode of the URL (url or url-host) + */ URLMode = 'x-gitbook-url-mode', + + /** + * The theme of the page (light or dark) + */ Theme = 'x-gitbook-theme', + + /** + * The customization override to apply. + */ Customization = 'x-gitbook-customization', + + /** + * Token to use for the API. + */ + APIToken = 'x-gitbook-token', + + /** + * The visitor token used to access this content + */ + VisitorAuthToken = 'x-gitbook-visitor-token', } /** * Get the URL mode from the middleware headers. + * This function should only be called in a server action or a dynamic route. */ export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> { const headersList = await headers(); @@ -20,8 +57,40 @@ export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> { return mode as 'url' | 'url-host'; } +/** + * Get the site URL data from the middleware headers. + * This function should only be called in a server action or a dynamic route. + */ +export async function getSiteURLDataFromMiddleware(): Promise { + const headersList = await headers(); + const siteURLData = headersList.get(MiddlewareHeaders.SiteURLData); + + if (!siteURLData) { + throw new Error( + 'Site URL data is not set by the middleware. This should only be called in a server action or a dynamic route.' + ); + } + + return JSON.parse(siteURLData); +} + +/** + * Get the URL from the middleware headers. + * This function should only be called in a server action or a dynamic route. + */ +export async function getSiteURLFromMiddleware(): Promise { + const headersList = await headers(); + const siteURL = headersList.get(MiddlewareHeaders.SiteURL); + if (!siteURL) { + throw new Error('URL mode is not set by the middleware'); + } + + return siteURL; +} + /** * For preview, the theme can be set via query string (?theme=light). + * This function should only be called in a dynamic route. */ export async function getThemeFromMiddleware() { const headersList = await headers(); @@ -34,3 +103,17 @@ export async function getThemeFromMiddleware() { ? CustomizationThemeMode.Light : CustomizationThemeMode.Dark; } + +/** + * Get the visitor auth token from the middleware headers. + * This function should only be called in a dynamic route. + */ +export async function getVisitorAuthTokenFromMiddleware(): Promise { + const headersList = await headers(); + const visitorAuthToken = headersList.get(MiddlewareHeaders.VisitorAuthToken); + if (!visitorAuthToken) { + return null; + } + + return visitorAuthToken; +} diff --git a/packages/gitbook-v2/src/lib/server-actions.ts b/packages/gitbook-v2/src/lib/server-actions.ts new file mode 100644 index 000000000..08e6f0a91 --- /dev/null +++ b/packages/gitbook-v2/src/lib/server-actions.ts @@ -0,0 +1,31 @@ +import { type GitBookBaseContext, fetchSiteContextByURLLookup, getBaseContext } from './context'; +import { + getSiteURLDataFromMiddleware, + getSiteURLFromMiddleware, + getURLModeFromMiddleware, +} from './middleware'; + +/** + * Get the base context for a server action. + * This function should only be called in a server action. + */ +export async function getServerActionBaseContext() { + const siteURL = await getSiteURLFromMiddleware(); + const siteURLData = await getSiteURLDataFromMiddleware(); + const urlMode = await getURLModeFromMiddleware(); + + return getBaseContext({ + siteURL, + urlMode, + apiToken: siteURLData.apiToken, + }); +} + +/** + * Fetch the context for a site in a server action. + * The server action is always dynamic and the request is passed through the middleware. + */ +export async function fetchServerActionSiteContext(baseContext: GitBookBaseContext) { + const siteURLData = await getSiteURLDataFromMiddleware(); + return fetchSiteContextByURLLookup(baseContext, siteURLData); +} diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index 2fa9d4677..60e5abe8d 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -64,8 +64,13 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) { return NextResponse.redirect(data.redirect); } + const routeType = dynamicHeaders ? 'dynamic' : 'static'; + const requestHeaders = new Headers(request.headers); + requestHeaders.set(MiddlewareHeaders.RouteType, routeType); requestHeaders.set(MiddlewareHeaders.URLMode, mode); + requestHeaders.set(MiddlewareHeaders.SiteURL, `${url.origin}${data.basePath}`); + requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(data)); if (dynamicHeaders) { for (const [key, value] of Object.entries(dynamicHeaders)) { requestHeaders.set(key, value); @@ -74,16 +79,18 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) { const route = [ 'sites', - dynamicHeaders ? 'dynamic' : 'static', + routeType, mode, encodeURIComponent(url.host + data.basePath), encodePathInSiteContent(data.pathname), ].join('/'); - console.log('route', route); + console.log(`rewriting to ${route}`); const response = NextResponse.rewrite(new URL(`/${route}`, request.url), { - headers: requestHeaders, + request: { + headers: requestHeaders, + }, }); // Add Content Security Policy header diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index 6c8e8fb97..3c6f3d435 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -155,6 +155,21 @@ export function runTestCases(testCases: TestsCase[]) { ); } + // Set the header to disable the Vercel toolbar + // But only on the main document as it'd cause CORS issues on other resources + await page.route('**/*', async (route, request) => { + if (request.resourceType() === 'document') { + await route.continue({ + headers: { + ...request.headers(), + 'x-vercel-skip-toolbar': '1', + }, + }); + } else { + await route.continue(); + } + }); + await page.goto(url); if (testEntry.run) { await testEntry.run(page); diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index fb31dafea..bbbcf467b 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -62,7 +62,8 @@ "tailwind-merge": "^2.2.0", "tailwind-shades": "^1.1.2", "url-join": "^5.0.0", - "usehooks-ts": "^3.1.0" + "usehooks-ts": "^3.1.0", + "ai": "^4.1.46" }, "devDependencies": { "@argos-ci/playwright": "^3.10.0", diff --git a/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx b/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx index 94a1ddb85..3ac8b8427 100644 --- a/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx +++ b/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx @@ -1,4 +1,4 @@ -import { getThemeFromMiddleware } from '@v2/lib/middleware'; +import { getThemeFromMiddleware, getVisitorAuthTokenFromMiddleware } from '@v2/lib/middleware'; import type { Metadata, Viewport } from 'next'; import type React from 'react'; @@ -22,12 +22,14 @@ export default async function ContentLayout(props: { children: React.ReactNode } const context = await fetchLayoutData(); const queryStringTheme = await getThemeFromMiddleware(); + const visitorAuthToken = await getVisitorAuthTokenFromMiddleware(); return ( {children} diff --git a/packages/gitbook/src/components/Ads/renderAd.tsx b/packages/gitbook/src/components/Ads/renderAd.tsx index 9ea4d92f2..146f0d253 100644 --- a/packages/gitbook/src/components/Ads/renderAd.tsx +++ b/packages/gitbook/src/components/Ads/renderAd.tsx @@ -5,6 +5,8 @@ import { headers } from 'next/headers'; import { getV1BaseContext } from '@/lib/v1'; +import { isV2 } from '@/lib/v2'; +import { getServerActionBaseContext } from '@v2/lib/server-actions'; import { AdClassicRendering } from './AdClassicRendering'; import { AdCoverRendering } from './AdCoverRendering'; import { AdPixels } from './AdPixels'; @@ -41,7 +43,7 @@ interface FetchPlaceholderAdOptions { * and properly access user-agent and IP. */ export async function renderAd(options: FetchAdOptions) { - const baseContext = await getV1BaseContext(); + const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext(); const mode = options.source === 'live' ? options.mode : 'classic'; const result = options.source === 'live' ? await fetchAd(options) : await getPlaceholderAd(); @@ -64,9 +66,9 @@ export async function renderAd(options: FetchAdOptions) { children: ( <> {mode === 'classic' || !('callToAction' in ad) ? ( - + ) : ( - + )} {ad.pixel ? : null} diff --git a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx index 785da49be..f63f0ca8e 100644 --- a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx +++ b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx @@ -1,14 +1,13 @@ 'use client'; import { Icon } from '@gitbook/icons'; +import { readStreamableValue } from 'ai/rsc'; import React from 'react'; import { Loading } from '@/components/primitives'; import { useLanguage } from '@/intl/client'; import { t } from '@/intl/translate'; import type { TranslationLanguage } from '@/intl/translations'; -import { iterateStreamResponse } from '@/lib/actions'; -import type { SiteContentPointer } from '@/lib/api'; import { tcls } from '@/lib/tailwind'; import { useTrackEvent } from '../Insights'; @@ -32,14 +31,13 @@ export type SearchAskState = /** * Fetch and render the answers to a question. */ -export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: string }) { - const { pointer, query } = props; +export function SearchAskAnswer(props: { query: string }) { + const { query } = props; const language = useLanguage(); const trackEvent = useTrackEvent(); const [, setSearchState] = useSearch(); const [askState, setAskState] = useSearchAskContext(); - const { organizationId, siteId, siteSpaceId } = pointer; React.useEffect(() => { let cancelled = false; @@ -52,19 +50,19 @@ export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: str query, }); - const response = streamAskQuestion({ pointer, question: query }); - const stream = iterateStreamResponse(response); - // When we pass in "ask" mode, the query could still be updated by the client // we ensure that the query is up-to-date before starting the stream. setSearchState((prev) => (prev ? { ...prev, query, ask: true } : null)); - for await (const chunk of stream) { + const { stream } = await streamAskQuestion({ question: query }); + for await (const chunk of readStreamableValue(stream)) { if (cancelled) { return; } - setAskState({ type: 'answer', answer: chunk }); + if (chunk) { + setAskState({ type: 'answer', answer: chunk }); + } } })().catch(() => { if (cancelled) { @@ -81,7 +79,7 @@ export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: str cancelled = true; } }; - }, [organizationId, siteId, siteSpaceId, query, setAskState, setSearchState, trackEvent]); + }, [query, setAskState, setSearchState, trackEvent]); React.useEffect(() => { return () => { diff --git a/packages/gitbook/src/components/Search/SearchModal.tsx b/packages/gitbook/src/components/Search/SearchModal.tsx index 58ddc3dee..80956f3e3 100644 --- a/packages/gitbook/src/components/Search/SearchModal.tsx +++ b/packages/gitbook/src/components/Search/SearchModal.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { tString, useLanguage } from '@/intl/client'; -import type { SiteContentPointer } from '@/lib/api'; import { tcls } from '@/lib/tailwind'; import { LoadingPane } from '../primitives/LoadingPane'; @@ -18,11 +17,9 @@ import { SearchScopeToggle } from './SearchScopeToggle'; import { type SearchState, type UpdateSearchState, useSearch } from './useSearch'; interface SearchModalProps { - revisionId: string; spaceTitle: string; isMultiVariants: boolean; withAsk: boolean; - pointer: SiteContentPointer; } /** @@ -144,16 +141,7 @@ function SearchModalBody( onClose: (to?: string) => void; } ) { - const { - pointer, - revisionId, - spaceTitle, - withAsk, - isMultiVariants, - state, - setSearchState, - onClose, - } = props; + const { spaceTitle, withAsk, isMultiVariants, state, setSearchState, onClose } = props; const language = useLanguage(); const resultsRef = React.useRef(null); @@ -299,17 +287,13 @@ function SearchModalBody( {!state.ask || !withAsk ? ( ) : null} - {state.query && state.ask && withAsk ? ( - - ) : null} + {state.query && state.ask && withAsk ? : null} ); } diff --git a/packages/gitbook/src/components/Search/SearchResults.tsx b/packages/gitbook/src/components/Search/SearchResults.tsx index cebe577ca..6c9f7502f 100644 --- a/packages/gitbook/src/components/Search/SearchResults.tsx +++ b/packages/gitbook/src/components/Search/SearchResults.tsx @@ -1,12 +1,11 @@ 'use client'; import { captureException } from '@sentry/nextjs'; +import { readStreamableValue } from 'ai/rsc'; import assertNever from 'assert-never'; import React from 'react'; import { t, useLanguage } from '@/intl/client'; -import { iterateStreamResponse } from '@/lib/actions'; -import type { SiteContentPointer } from '@/lib/api'; import { tcls } from '@/lib/tailwind'; import { useTrackEvent } from '../Insights'; @@ -48,15 +47,13 @@ export const SearchResults = React.forwardRef(function SearchResults( props: { children?: React.ReactNode; query: string; - revisionId: string; global: boolean; withAsk: boolean; - pointer: SiteContentPointer; onSwitchToAsk: () => void; }, ref: React.Ref ) { - const { children, query, pointer, revisionId, withAsk, global, onSwitchToAsk } = props; + const { children, query, withAsk, global, onSwitchToAsk } = props; const language = useLanguage(); const trackEvent = useTrackEvent(); @@ -93,10 +90,13 @@ export const SearchResults = React.forwardRef(function SearchResults( return; } - const response = streamRecommendedQuestions(pointer.organizationId, pointer.siteId); - const stream = iterateStreamResponse(response); + const response = await streamRecommendedQuestions(); + for await (const entry of readStreamableValue(response.stream)) { + if (!entry) { + continue; + } - for await (const { question } of stream) { + const { question } = entry; if (questions.has(question)) { continue; } @@ -124,8 +124,8 @@ export const SearchResults = React.forwardRef(function SearchResults( let cancelled = false; const timeout = setTimeout(async () => { const results = await (global - ? searchAllSiteContent(query, pointer) - : searchSiteSpaceContent(query, pointer, revisionId)); + ? searchAllSiteContent(query) + : searchSiteSpaceContent(query)); if (cancelled) { return; @@ -154,7 +154,7 @@ export const SearchResults = React.forwardRef(function SearchResults( cancelled = true; clearTimeout(timeout); }; - }, [query, global, pointer, revisionId, withAsk, trackEvent]); + }, [query, global, withAsk, trackEvent]); const results: ResultType[] = React.useMemo(() => { if (!withAsk) { diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx index 6b5614463..5d2d85f0c 100644 --- a/packages/gitbook/src/components/Search/server-actions.tsx +++ b/packages/gitbook/src/components/Search/server-actions.tsx @@ -3,22 +3,24 @@ import type { RevisionPage, SearchAIAnswer, + SearchAIRecommendedQuestionStream, SearchPageResult, SiteSpace, Space, } from '@gitbook/api'; -import type { GitBookSiteContext } from '@v2/lib/context'; +import type { GitBookBaseContext, GitBookSiteContext } from '@v2/lib/context'; +import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/lib/server-actions'; +import { createStreamableValue } from 'ai/rsc'; import type * as React from 'react'; -import { assert } from 'ts-essentials'; -import { streamResponse } from '@/lib/actions'; -import * as api from '@/lib/api'; import { getAbsoluteHref } from '@/lib/links'; import { resolvePageId } from '@/lib/pages'; import { findSiteSpaceById } from '@/lib/sites'; import { filterOutNullable } from '@/lib/typescript'; -import { fetchV1ContextForSitePointer } from '@/lib/v1'; +import { getV1BaseContext } from '@/lib/v1'; +import { isV2 } from '@/lib/v2'; +import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware'; import { DocumentView } from '../DocumentView'; export type OrderedComputedResult = ComputedPageResult | ComputedSectionResult; @@ -54,70 +56,13 @@ export interface AskAnswerResult { sources: AskAnswerSource[]; } -/** - * Search for content in a site by scoping the search to all content, a specific spaces or current space. - */ -async function searchSiteContent(args: { - pointer: api.SiteContentPointer; - query: string; - scope: - | { mode: 'all' } - | { mode: 'current'; siteSpaceId: string } - | { mode: 'specific'; siteSpaceIds: string[] }; - cacheBust?: string; -}): Promise { - const { pointer, scope, query, cacheBust } = args; - - if (query.length <= 1) { - return []; - } - - const needsStructure = - scope.mode === 'all' || - scope.mode === 'current' || - (scope.mode === 'specific' && scope.siteSpaceIds.length > 1); - - const [searchResults, siteData] = await Promise.all([ - api.searchSiteContent(pointer.organizationId, pointer.siteId, query, scope, cacheBust), - needsStructure ? fetchV1ContextForSitePointer(pointer) : null, - ]); - const siteStructure = siteData?.structure; - - if (siteStructure) { - // We are searching all of this Site's content - return ( - await Promise.all( - searchResults.items.map(async (spaceItem) => { - const siteSpace = findSiteSpaceById(siteStructure, spaceItem.id); - - return Promise.all( - spaceItem.pages.map((item) => - transformSitePageResult(item, siteSpace ?? undefined) - ) - ); - }) - ) - ).flat(2); - } - - return ( - await Promise.all( - searchResults.items.map((spaceItem) => { - return Promise.all(spaceItem.pages.map((item) => transformPageResult(item))); - }) - ) - ).flat(2); -} - /** * Server action to search content in the entire site. */ -export async function searchAllSiteContent( - query: string, - pointer: api.SiteContentPointer -): Promise { - return await searchSiteContent({ - pointer, +export async function searchAllSiteContent(query: string): Promise { + const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext(); + + return await searchSiteContent(context, { query, scope: { mode: 'all' }, }); @@ -126,118 +71,191 @@ export async function searchAllSiteContent( /** * Server action to search content in a space. */ -export async function searchSiteSpaceContent( - query: string, - pointer: api.SiteContentPointer, - revisionId: string -): Promise { - const siteSpaceId = pointer.siteSpaceId; - assert(siteSpaceId, 'Expected siteSpaceId for searchSiteSpaceContent'); +export async function searchSiteSpaceContent(query: string): Promise { + const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext(); + const siteURLData = await getSiteURLDataFromMiddleware(); - return await searchSiteContent({ - pointer, + return await searchSiteContent(context, { query, // If we have a siteSectionId that means its a sections site use `current` mode // which searches in the current space + all default spaces of sections - scope: pointer.siteSectionId - ? { mode: 'current', siteSpaceId } - : { mode: 'specific', siteSpaceIds: [siteSpaceId] }, - // We want to break cache for this specific space if the revisionId is different so use it as a cache busting key - cacheBust: revisionId, + scope: siteURLData.siteSection + ? { mode: 'current', siteSpaceId: siteURLData.siteSpace } + : { mode: 'specific', siteSpaceIds: [siteURLData.siteSpace] }, }); } /** * Server action to ask a question in a space. */ -export const streamAskQuestion = streamResponse(async function* ({ - pointer, +export async function streamAskQuestion({ question, }: { - pointer: api.SiteContentPointer; question: string; }) { - const { organizationId, siteId, siteSpaceId } = pointer; - const [apiCtx, context] = await Promise.all([api.api(), fetchV1ContextForSitePointer(pointer)]); + const responseStream = createStreamableValue(); - const stream = apiCtx.client.orgs.streamAskInSite( - organizationId, - siteId, - { - question, - context: siteSpaceId - ? { - siteSpaceId, - } - : undefined, - scope: { - mode: 'default', + (async () => { + const context = await fetchServerActionSiteContext( + isV2() ? await getServerActionBaseContext() : await getV1BaseContext() + ); - // Include the current site space regardless. - includedSiteSpaces: siteSpaceId ? [siteSpaceId] : undefined, + const apiClient = await context.dataFetcher.api(); + + const stream = apiClient.orgs.streamAskInSite( + context.organizationId, + context.site.id, + { + question, + context: { + siteSpaceId: context.siteSpace.id, + }, + scope: { + mode: 'default', + // Include the current site space regardless. + includedSiteSpaces: [context.siteSpace.id], + }, }, - }, - { format: 'document' } - ); + { format: 'document' } + ); - const spacePromises = new Map>(); - for await (const chunk of stream) { - const answer = chunk.answer; + const spacePromises = new Map>(); + for await (const chunk of stream) { + const answer = chunk.answer; - // Register the space of each page source into the promise queue. - const spaces = answer.sources - .map((source) => { - if (source.type !== 'page') { - return null; - } + // Register the space of each page source into the promise queue. + const spaces = answer.sources + .map((source) => { + if (source.type !== 'page') { + return null; + } - if (!spacePromises.has(source.space)) { - spacePromises.set( - source.space, - context.dataFetcher.getRevisionPages({ - spaceId: source.space, - revisionId: source.revision, - metadata: false, - }) - ); - } + if (!spacePromises.has(source.space)) { + spacePromises.set( + source.space, + context.dataFetcher.getRevisionPages({ + spaceId: source.space, + revisionId: source.revision, + metadata: false, + }) + ); + } - return source.space; - }) - .filter(filterOutNullable); + return source.space; + }) + .filter(filterOutNullable); - // Get the pages for all spaces referenced by this answer. - const pages = await Promise.all( - spaces.map(async (space) => { - const pages = await spacePromises.get(space); - return { space, pages }; - }) - ).then((results) => { - return results.reduce((map, result) => { - if (result.pages) { - map.set(result.space, result.pages); - } - return map; - }, new Map()); + // Get the pages for all spaces referenced by this answer. + const pages = await Promise.all( + spaces.map(async (space) => { + const pages = await spacePromises.get(space); + return { space, pages }; + }) + ).then((results) => { + return results.reduce((map, result) => { + if (result.pages) { + map.set(result.space, result.pages); + } + return map; + }, new Map()); + }); + responseStream.update( + await transformAnswer(context, { answer: chunk.answer, spacePages: pages }) + ); + } + })() + .then(() => { + responseStream.done(); + }) + .catch((error) => { + responseStream.error(error); }); - yield await transformAnswer(context, { answer: chunk.answer, spacePages: pages }); - } -}); + + return { + stream: responseStream.value, + }; +} /** * Stream a list of suggested questions for the site. */ -export const streamRecommendedQuestions = streamResponse(async function* ( - organizationId: string, - siteId: string -) { - const apiCtx = await api.api(); - const stream = apiCtx.client.orgs.streamRecommendedQuestionsInSite(organizationId, siteId); +export async function streamRecommendedQuestions() { + const siteURLData = await getSiteURLDataFromMiddleware(); + const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext(); - for await (const chunk of stream) { - yield chunk; + const responseStream = createStreamableValue(); + + (async () => { + const apiClient = await context.dataFetcher.api(); + const apiStream = apiClient.orgs.streamRecommendedQuestionsInSite( + siteURLData.organization, + siteURLData.site + ); + + for await (const chunk of apiStream) { + responseStream.update(chunk); + } + })() + .then(() => { + responseStream.done(); + }) + .catch((error) => { + responseStream.error(error); + }); + + return { stream: responseStream.value }; +} + +/** + * Search for content in a site by scoping the search to all content, a specific spaces or current space. + */ +async function searchSiteContent( + context: GitBookBaseContext, + args: { + query: string; + scope: + | { mode: 'all' } + | { mode: 'current'; siteSpaceId: string } + | { mode: 'specific'; siteSpaceIds: string[] }; } -}); +): Promise { + const { dataFetcher } = context; + const siteURLData = await getSiteURLDataFromMiddleware(); + + const { scope, query } = args; + + if (query.length <= 1) { + return []; + } + + const [searchResults, { structure }] = await Promise.all([ + dataFetcher.searchSiteContent({ + organizationId: siteURLData.organization, + siteId: siteURLData.site, + query, + scope, + }), + dataFetcher.getPublishedContentSite({ + organizationId: siteURLData.organization, + siteId: siteURLData.site, + siteShareKey: siteURLData.shareKey, + }), + ]); + + return ( + await Promise.all( + searchResults.map(async (spaceItem) => { + const siteSpace = findSiteSpaceById(structure, spaceItem.id); + + return Promise.all( + spaceItem.pages.map((item) => + transformSitePageResult(item, siteSpace ?? undefined) + ) + ); + }) + ) + ).flat(2); +} async function transformAnswer( context: GitBookSiteContext, @@ -343,16 +361,6 @@ async function transformSitePageResult(item: SearchPageResult, siteSpace?: SiteS return [page, ...sections]; } -async function transformPageResult(item: SearchPageResult, space?: Space) { - const [page, sections] = await transformSectionsAndPage({ - item, - space, - spaceURL: space?.urls.published ?? space?.urls.app, - }); - - return [page, ...sections]; -} - // Resolve a relative path to an absolute URL // if the search result is relative to another space, we use the space URL async function getURLWithSections(path: string, spaceURL?: string) { diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx index c15c3d45c..671c64619 100644 --- a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx +++ b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx @@ -24,9 +24,10 @@ export async function SiteLayout(props: { context: GitBookSiteContext; forcedTheme?: CustomizationThemeMode | null; withTracking: boolean; + visitorAuthToken: string | null; children: React.ReactNode; }) { - const { context, nonce, forcedTheme, withTracking, children } = props; + const { context, nonce, forcedTheme, withTracking, visitorAuthToken, children } = props; const { scripts, customization } = context; @@ -51,7 +52,11 @@ export async function SiteLayout(props: { (customization.themes.toggeable ? undefined : customization.themes.default) } > - + {children} diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index 3360a89c8..75d705d19 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -10,7 +10,6 @@ import { CONTAINER_STYLE } from '@/components/layout'; import { getSpaceLanguage } from '@/intl/server'; import { t } from '@/intl/translate'; import { tcls } from '@/lib/tailwind'; -import { getSitePointerFromContext } from '@/lib/v1'; import { SpacesDropdown } from '../Header/SpacesDropdown'; import { InsightsProvider } from '../Insights'; @@ -25,10 +24,13 @@ export function SpaceLayout(props: { /** Whether to enable tracking of events into site insights. */ withTracking: boolean; + /** The visitor token used to access this content */ + visitorAuthToken: string | null; + /** The children of the layout. */ children: React.ReactNode; }) { - const { context, withTracking, children } = props; + const { context, withTracking, visitorAuthToken, children } = props; const { siteSpace, customization, sections, siteSpaces } = context; const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None; @@ -46,7 +48,7 @@ export function SpaceLayout(props: { diff --git a/packages/gitbook/src/lib/actions.ts b/packages/gitbook/src/lib/actions.ts deleted file mode 100644 index c0e7b577b..000000000 --- a/packages/gitbook/src/lib/actions.ts +++ /dev/null @@ -1,49 +0,0 @@ -export type StreamResponseChunk = { - iteratorResult: IteratorResult; - next?: Promise>; -}; - -/** - * Stream the response of an async generator function. - * This function should be used to wrap an async generator server action. - * - * See https://github.com/vercel/next.js/discussions/51282 - */ -export function streamResponse( - createGenerator: (...args: P) => AsyncGenerator -) { - return async (...args: Parameters) => { - const generator = createGenerator(...args); - return streamChunk(generator); - }; -} - -/** - * Iterate over the response of an async generator function. - */ -export function iterateStreamResponse(streamResponse: Promise>) { - return { - [Symbol.asyncIterator]: () => ({ - current: streamResponse, - async next() { - const { iteratorResult, next } = await this.current; - - if (next) this.current = next; - else iteratorResult.done = true; - - return iteratorResult; - }, - }), - }; -} - -async function streamChunk(generator: AsyncGenerator) { - const next = generator.next(); - return new Promise>((resolve, reject) => { - next.then((res) => { - if (res.done) resolve({ iteratorResult: res }); - else resolve({ iteratorResult: res, next: streamChunk(generator) }); - }); - next.catch((error) => reject(error)); - }); -} diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts index 82781ebd2..cc0900613 100644 --- a/packages/gitbook/src/lib/v1.ts +++ b/packages/gitbook/src/lib/v1.ts @@ -7,7 +7,6 @@ import { import type { GitBookDataFetcher } from '@v2/lib/data/types'; import { createImageResizer } from '@v2/lib/images'; import { createLinker } from '@v2/lib/links'; -import { headers } from 'next/headers'; import { type SiteContentPointer, @@ -28,6 +27,7 @@ import { getSiteRedirectBySource, getSpace, getUserById, + searchSiteContent, } from './api'; import { getDynamicCustomizationSettings } from './customization'; import { getBasePath, getHost } from './links'; @@ -74,9 +74,20 @@ export async function getV1BaseContext(): Promise { async function getDataFetcherV1(): Promise { const apiClient = await api(); - return { + const dataFetcher: GitBookDataFetcher = { apiEndpoint: apiClient.client.endpoint, + async api() { + const result = await api(); + return result.client; + }, + + withToken() { + // In v1, the token is global and controlled by the middleware. + // We don't need to do anything special here. + return dataFetcher; + }, + getUserById(userId) { return getUserById(userId); }, @@ -145,7 +156,15 @@ async function getDataFetcherV1(): Promise { getEmbedByUrl(params) { return getEmbedByUrlInSpace(params.spaceId, params.url); }, + + async searchSiteContent(params) { + const { organizationId, siteId, query, cacheBust, scope } = params; + const result = await searchSiteContent(organizationId, siteId, query, scope, cacheBust); + return result.items; + }, }; + + return dataFetcher; } /** @@ -153,7 +172,6 @@ async function getDataFetcherV1(): Promise { */ export async function fetchV1ContextForSitePointer(pointer: SiteContentPointer) { const baseContext = await getV1BaseContext(); - const headersList = await headers(); const context = await fetchSiteContextByIds(baseContext, { organization: pointer.organizationId, @@ -164,7 +182,6 @@ export async function fetchV1ContextForSitePointer(pointer: SiteContentPointer) shareKey: pointer.siteShareKey, changeRequest: pointer.changeRequestId, revision: pointer.revisionId, - visitorAuthToken: headersList.get('x-gitbook-visitor-token'), }); context.customization = await getDynamicCustomizationSettings(context.customization); diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index b5b8da5a7..f7b994ae5 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -32,6 +32,7 @@ import { import { joinPath, normalizePathname } from '@/lib/paths'; import { getProxyModeBasePath } from '@/lib/proxy'; +import { MiddlewareHeaders } from '@v2/lib/middleware'; export const config = { matcher: @@ -83,6 +84,8 @@ export type LookupResult = PublishedContentWithCache & { cookies?: LookupCookies; /** Visitor authentication token */ visitorToken?: string; + /** URL of the site */ + siteURL?: string; }; /** @@ -199,6 +202,13 @@ export async function middleware(request: NextRequest) { if (resolved.shareKey) { headers.set('x-gitbook-content-site-share-key', resolved.shareKey); } + + // For server actions that use v2 code + headers.set(MiddlewareHeaders.SiteURLData, JSON.stringify(resolved)); + } + + if (resolved.visitorToken) { + headers.set(MiddlewareHeaders.VisitorAuthToken, resolved.visitorToken); } // For tests, we make it possible to enable search indexation @@ -231,10 +241,6 @@ export async function middleware(request: NextRequest) { headers.set('x-gitbook-api', apiEndpoint); } - if (resolved.visitorToken) { - headers.set('x-gitbook-visitor-token', resolved.visitorToken); - } - const target = new URL(joinPath('/middleware', rewritePathname), request.nextUrl.toString()); target.search = url.search; @@ -761,14 +767,23 @@ async function lookupSiteByAPI( return null; }); - return ( - result ?? { - error: { - code: 404, - message: 'No content found', - }, + if (result) { + if ('site' in result) { + return { + ...result, + siteURL: `${lookupURL.origin}${result.basePath}`, + }; } - ); + + return result; + } + + return { + error: { + code: 404, + message: 'No content found', + }, + }; } /**