mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13514bf6fa | |||
| 05ffd0e321 | |||
| 701eaad92a | |||
| c66ef5dd10 | |||
| f774f9b3a0 | |||
| 29dc7ccb1a | |||
| 80237c34f9 | |||
| 979233c5c0 | |||
| 2f03e6acee | |||
| c492f09a19 | |||
| 2c3af5e73c | |||
| 6597f8508d | |||
| 8beb5d6bcf | |||
| 53f5dbea26 |
@@ -22,6 +22,17 @@
|
||||
"wrangler": "^3.109.2",
|
||||
},
|
||||
},
|
||||
"packages/cache-tags": {
|
||||
"name": "@gitbook/cache-tags",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "0.96.1",
|
||||
"assert-never": "^1.2.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
},
|
||||
},
|
||||
"packages/colors": {
|
||||
"name": "@gitbook/colors",
|
||||
"version": "0.2.0",
|
||||
@@ -38,10 +49,11 @@
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.6.3",
|
||||
"version": "0.6.4",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "0.96.1",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
@@ -57,6 +69,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",
|
||||
@@ -118,11 +131,12 @@
|
||||
},
|
||||
"packages/gitbook-v2": {
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "0.96.1",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@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",
|
||||
@@ -156,7 +170,7 @@
|
||||
},
|
||||
"packages/openapi-parser": {
|
||||
"name": "@gitbook/openapi-parser",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"dependencies": {
|
||||
"@scalar/openapi-parser": "^0.10.4",
|
||||
"@scalar/openapi-types": "^0.1.6",
|
||||
@@ -181,6 +195,7 @@
|
||||
"version": "0.6.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "0.96.1",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"assert-never": "^1.2.1",
|
||||
"classnames": "^2.5.1",
|
||||
},
|
||||
@@ -210,7 +225,7 @@
|
||||
},
|
||||
"packages/react-openapi": {
|
||||
"name": "@gitbook/react-openapi",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"dependencies": {
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@scalar/api-client-react": "1.0.87",
|
||||
@@ -242,6 +257,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=="],
|
||||
@@ -608,6 +631,8 @@
|
||||
|
||||
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
|
||||
|
||||
"@gitbook/cache-tags": ["@gitbook/cache-tags@workspace:packages/cache-tags"],
|
||||
|
||||
"@gitbook/colors": ["@gitbook/colors@workspace:packages/colors"],
|
||||
|
||||
"@gitbook/emoji-codepoints": ["@gitbook/emoji-codepoints@workspace:packages/emoji-codepoints"],
|
||||
@@ -1350,6 +1375,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=="],
|
||||
@@ -1530,6 +1557,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=="],
|
||||
@@ -1772,6 +1801,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=="],
|
||||
@@ -2194,6 +2225,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=="],
|
||||
@@ -2204,6 +2237,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=="],
|
||||
@@ -2746,6 +2781,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=="],
|
||||
@@ -2856,6 +2893,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=="],
|
||||
@@ -2880,6 +2919,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=="],
|
||||
@@ -3098,10 +3139,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=="],
|
||||
@@ -4238,7 +4283,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=="],
|
||||
|
||||
@@ -5138,23 +5183,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=="],
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dist/
|
||||
@@ -0,0 +1,7 @@
|
||||
# @gitbook/cache-tags
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 05ffd0e: Initial version of the package
|
||||
@@ -0,0 +1,3 @@
|
||||
# `@gitbook/cache-tags`
|
||||
|
||||
Utility to generate cache tags for GitBook Open.
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@gitbook/cache-tags",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "0.96.1",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w"
|
||||
},
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import type { ComputedContentSource } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
|
||||
/**
|
||||
* Get a stringified cache tag for a given object.
|
||||
*/
|
||||
export function getCacheTag(
|
||||
spec: /**
|
||||
* All data related to a user
|
||||
* @deprecated - in v2, no tag as this is an immutable data
|
||||
*/
|
||||
| {
|
||||
tag: 'user';
|
||||
user: string;
|
||||
}
|
||||
/**
|
||||
* All data related to a space
|
||||
*/
|
||||
| {
|
||||
tag: 'space';
|
||||
space: string;
|
||||
}
|
||||
/**
|
||||
* All data related to an integration.
|
||||
*/
|
||||
| {
|
||||
tag: 'integration';
|
||||
integration: string;
|
||||
}
|
||||
/**
|
||||
* All data related to a change request
|
||||
*/
|
||||
| {
|
||||
tag: 'change-request';
|
||||
space: string;
|
||||
changeRequest: string;
|
||||
}
|
||||
/**
|
||||
* Immutable data related to a revision
|
||||
* @deprecated - in v2, no tag as this is an immutable data
|
||||
*/
|
||||
| {
|
||||
tag: 'revision';
|
||||
space: string;
|
||||
revision: string;
|
||||
}
|
||||
/**
|
||||
* Immutable data related to a document
|
||||
* @deprecated - in v2, no tag as this is an immutable data
|
||||
*/
|
||||
| {
|
||||
tag: 'document';
|
||||
space: string;
|
||||
document: string;
|
||||
}
|
||||
/**
|
||||
* Immutable data related to a computed document
|
||||
* @deprecated - in v2, no tag as this is an immutable data
|
||||
*/
|
||||
| {
|
||||
tag: 'computed-document';
|
||||
space: string;
|
||||
integration: string;
|
||||
}
|
||||
/**
|
||||
* All data related to the URL of a content
|
||||
*/
|
||||
| {
|
||||
tag: 'url';
|
||||
hostname: string;
|
||||
}
|
||||
/**
|
||||
* All data related to a site
|
||||
*/
|
||||
| {
|
||||
tag: 'site';
|
||||
site: string;
|
||||
}
|
||||
/**
|
||||
* All data related to an OpenAPI spec
|
||||
*/
|
||||
| {
|
||||
tag: 'openapi';
|
||||
organization: string;
|
||||
openAPISpec: string;
|
||||
}
|
||||
): string {
|
||||
switch (spec.tag) {
|
||||
case 'user':
|
||||
return `user:${spec.user}`;
|
||||
case 'url':
|
||||
return `url:${spec.hostname}`;
|
||||
case 'space':
|
||||
return `space:${spec.space}`;
|
||||
case 'change-request':
|
||||
return `space:${spec.space}:change-request:${spec.changeRequest}`;
|
||||
case 'revision':
|
||||
return `space:${spec.space}:revision:${spec.revision}`;
|
||||
case 'document':
|
||||
return `space:${spec.space}:document:${spec.document}`;
|
||||
case 'computed-document':
|
||||
return `space:${spec.space}:computed-document:${spec.integration}`;
|
||||
case 'site':
|
||||
return `site:${spec.site}`;
|
||||
case 'integration':
|
||||
return `integration:${spec.integration}`;
|
||||
case 'openapi':
|
||||
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
|
||||
default:
|
||||
assertNever(spec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tags for a computed content source.
|
||||
*/
|
||||
export function getComputedContentSourceCacheTags(
|
||||
inContext: {
|
||||
spaceId: string;
|
||||
organizationId: string;
|
||||
},
|
||||
source: ComputedContentSource
|
||||
) {
|
||||
const tags: string[] = [];
|
||||
|
||||
// We add the dependencies as tags, to ensure that the computed content is invalidated
|
||||
// when the dependencies are updated.
|
||||
const dependencies = Object.values(source.dependencies ?? {});
|
||||
if (dependencies.length > 0) {
|
||||
dependencies.forEach((dependency) => {
|
||||
switch (dependency.ref.kind) {
|
||||
case 'space':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
space: dependency.ref.space,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'openapi':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'openapi',
|
||||
organization: inContext.organizationId,
|
||||
openAPISpec: dependency.ref.spec,
|
||||
})
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Do not throw for unknown dependency types
|
||||
// as it might mean we are lacking behind the API version
|
||||
break;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Push a dummy tag, as the v1 is only using the first tag
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'computed-document',
|
||||
space: inContext.spaceId,
|
||||
integration: source.integration,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// We invalidate the computed content when a new version of the integration is deployed.
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
integration: source.integration,
|
||||
})
|
||||
);
|
||||
|
||||
return tags;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
# gitbook-v2
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 05ffd0e: Improving data cache management for computed content
|
||||
- Updated dependencies [05ffd0e]
|
||||
- @gitbook/cache-tags@0.1.0
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"next": "^15.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"@gitbook/api": "0.96.1",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"server-only": "^0.0.1"
|
||||
},
|
||||
|
||||
@@ -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<RouteLayoutParams>;
|
||||
@@ -18,6 +18,7 @@ export default async function SiteDynamicLayout({
|
||||
}: React.PropsWithChildren<SiteDynamicLayoutProps>) {
|
||||
const context = await getDynamicSiteContext(await params);
|
||||
const forcedTheme = await getThemeFromMiddleware();
|
||||
const visitorAuthToken = await getVisitorAuthTokenFromMiddleware();
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout customization={context.customization}>
|
||||
@@ -25,6 +26,7 @@ export default async function SiteDynamicLayout({
|
||||
context={context}
|
||||
forcedTheme={forcedTheme}
|
||||
withTracking={!GITBOOK_DISABLE_TRACKING}
|
||||
visitorAuthToken={visitorAuthToken}
|
||||
>
|
||||
{children}
|
||||
</SiteLayout>
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
generateSitePageMetadata,
|
||||
generateSitePageViewport,
|
||||
} from '@/components/SitePage';
|
||||
import { getCacheTag } from '@gitbook/cache-tags';
|
||||
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils';
|
||||
import { getSiteCacheTag } from '@v2/lib/cache';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { unstable_cacheTag as cacheTag } from 'next/cache';
|
||||
|
||||
@@ -21,7 +21,12 @@ export default async function Page(props: PageProps) {
|
||||
const context = await getStaticSiteContext(params);
|
||||
const pathname = getPagePathFromParams(params);
|
||||
|
||||
cacheTag(getSiteCacheTag(context.site.id));
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: context.site.id,
|
||||
})
|
||||
);
|
||||
|
||||
return <SitePage context={context} pageParams={{ pathname }} redirectOnFallback={true} />;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
generateSiteLayoutMetadata,
|
||||
generateSiteLayoutViewport,
|
||||
} from '@/components/SiteLayout';
|
||||
import { getCacheTag } from '@gitbook/cache-tags';
|
||||
import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils';
|
||||
import { getSiteCacheTag } from '@v2/lib/cache';
|
||||
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
|
||||
import { unstable_cacheTag as cacheTag } from 'next/cache';
|
||||
|
||||
@@ -21,11 +21,20 @@ export default async function SiteStaticLayout({
|
||||
|
||||
const context = await getStaticSiteContext(await params);
|
||||
|
||||
cacheTag(getSiteCacheTag(context.site.id));
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: context.site.id,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout customization={context.customization}>
|
||||
<SiteLayout context={context} withTracking={!GITBOOK_DISABLE_TRACKING}>
|
||||
<SiteLayout
|
||||
context={context}
|
||||
withTracking={!GITBOOK_DISABLE_TRACKING}
|
||||
visitorAuthToken={null}
|
||||
>
|
||||
{children}
|
||||
</SiteLayout>
|
||||
</CustomizationRootLayout>
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Get the cache tag for a site.
|
||||
*/
|
||||
export function getSiteCacheTag(siteId: string) {
|
||||
return `site:${siteId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache tag for a hostname.
|
||||
*/
|
||||
export function getHostnameCacheTag(hostname: string) {
|
||||
return `url:${hostname}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache tag for an OpenAPI spec.
|
||||
*/
|
||||
export function getOpenAPISpecCacheTag(organizationId: string, slug: string) {
|
||||
return `organization:${organizationId}:openapi:${slug}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache tag for a space.
|
||||
*/
|
||||
export function getSpaceCacheTag(spaceId: string) {
|
||||
return `space:${spaceId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache tag for a change request.
|
||||
*/
|
||||
export function getChangeRequestCacheTag(spaceId: string, changeRequestId: string) {
|
||||
return `space:${spaceId}:change-request:${changeRequestId}`;
|
||||
}
|
||||
@@ -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<GitBookSiteContext> {
|
||||
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<GitBookSiteContext> {
|
||||
const { dataFetcher } = baseContext;
|
||||
@@ -233,7 +285,6 @@ export async function fetchSiteContextByIds(
|
||||
structure: siteStructure,
|
||||
sections,
|
||||
scripts,
|
||||
visitorAuthToken: ids.visitorAuthToken,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { type ComputedContentSource, GitBookAPI } from '@gitbook/api';
|
||||
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
|
||||
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
|
||||
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
|
||||
import {
|
||||
getChangeRequestCacheTag,
|
||||
getHostnameCacheTag,
|
||||
getOpenAPISpecCacheTag,
|
||||
getSiteCacheTag,
|
||||
getSpaceCacheTag,
|
||||
} from '../cache';
|
||||
import type { GitBookDataFetcher } from './types';
|
||||
|
||||
interface DataFetcherInput {
|
||||
@@ -35,6 +29,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
|
||||
//
|
||||
@@ -114,6 +119,7 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo
|
||||
},
|
||||
getComputedDocument(params) {
|
||||
return getComputedDocument(input, {
|
||||
organizationId: params.organizationId,
|
||||
spaceId: params.spaceId,
|
||||
source: params.source,
|
||||
});
|
||||
@@ -124,6 +130,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
|
||||
@@ -169,7 +178,12 @@ async function getSpace(
|
||||
'use cache';
|
||||
|
||||
cacheLife('days');
|
||||
cacheTag(getSpaceCacheTag(params.spaceId));
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
space: params.spaceId,
|
||||
})
|
||||
);
|
||||
|
||||
const res = await getAPI(input).spaces.getSpaceById(params.spaceId, {
|
||||
shareKey: params.shareKey,
|
||||
@@ -193,7 +207,13 @@ async function getChangeRequest(
|
||||
params.spaceId,
|
||||
params.changeRequestId
|
||||
);
|
||||
cacheTag(getChangeRequestCacheTag(params.spaceId, res.data.id));
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'change-request',
|
||||
space: params.spaceId,
|
||||
changeRequest: res.data.id,
|
||||
})
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (checkHasErrorCode(error, 404)) {
|
||||
@@ -321,14 +341,24 @@ async function getComputedDocument(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
organizationId: string;
|
||||
source: ComputedContentSource;
|
||||
}
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
// TODO: we need to resolve dependencies and pass them in the cache key
|
||||
cacheLife('days');
|
||||
|
||||
cacheTag(
|
||||
...getComputedContentSourceCacheTags(
|
||||
{
|
||||
spaceId: params.spaceId,
|
||||
organizationId: params.organizationId,
|
||||
},
|
||||
params.source
|
||||
)
|
||||
);
|
||||
|
||||
const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, {
|
||||
source: params.source,
|
||||
});
|
||||
@@ -372,7 +402,13 @@ async function getLatestOpenAPISpecVersionContent(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheTag(getOpenAPISpecCacheTag(params.organizationId, params.slug));
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'openapi',
|
||||
organization: params.organizationId,
|
||||
openAPISpec: params.slug,
|
||||
})
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
try {
|
||||
@@ -403,7 +439,12 @@ async function getPublishedContentByUrl(
|
||||
const { url, visitorAuthToken, redirectOnError } = params;
|
||||
|
||||
const hostname = new URL(url).hostname;
|
||||
cacheTag(getHostnameCacheTag(hostname));
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'url',
|
||||
hostname,
|
||||
})
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await getAPI(input).urls.getPublishedContentByUrl({
|
||||
@@ -413,7 +454,12 @@ async function getPublishedContentByUrl(
|
||||
});
|
||||
|
||||
if ('site' in res.data) {
|
||||
cacheTag(getSiteCacheTag(res.data.site));
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: res.data.site,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
@@ -429,7 +475,12 @@ async function getPublishedContentSite(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheTag(getSiteCacheTag(params.siteId));
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: params.siteId,
|
||||
})
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await getAPI(input).orgs.getPublishedContentSite(
|
||||
@@ -453,7 +504,12 @@ async function getSiteRedirectBySource(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheTag(getSiteCacheTag(params.siteId));
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: params.siteId,
|
||||
})
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
try {
|
||||
@@ -498,6 +554,23 @@ async function getEmbedByUrl(
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function searchSiteContent(
|
||||
input: DataFetcherInput,
|
||||
params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
const { organizationId, siteId, query, scope } = params;
|
||||
|
||||
cacheLife('days');
|
||||
|
||||
const res = await getAPI(input).orgs.searchSiteContent(organizationId, siteId, {
|
||||
query,
|
||||
...scope,
|
||||
});
|
||||
return res.data.items;
|
||||
}
|
||||
|
||||
function getAPI(input: DataFetcherInput) {
|
||||
const { apiEndpoint, apiToken } = input;
|
||||
const api = new GitBookAPI({
|
||||
|
||||
@@ -10,6 +10,18 @@ export interface GitBookDataFetcher {
|
||||
*/
|
||||
apiEndpoint: string;
|
||||
|
||||
/**
|
||||
* Get an API client for the current context.
|
||||
*/
|
||||
api(): Promise<api.GitBookAPI>;
|
||||
|
||||
/**
|
||||
* Create a data fetcher authenticated with a specific token.
|
||||
*/
|
||||
withToken(input: {
|
||||
apiToken: string;
|
||||
}): GitBookDataFetcher;
|
||||
|
||||
/**
|
||||
* Get a user by its ID.
|
||||
*/
|
||||
@@ -91,6 +103,7 @@ export interface GitBookDataFetcher {
|
||||
* Get a computed document by its space ID and computed source.
|
||||
*/
|
||||
getComputedDocument(params: {
|
||||
organizationId: string;
|
||||
spaceId: string;
|
||||
source: api.ComputedContentSource;
|
||||
}): Promise<api.JSONDocument>;
|
||||
@@ -126,4 +139,19 @@ export interface GitBookDataFetcher {
|
||||
* Get an embed by its URL.
|
||||
*/
|
||||
getEmbedByUrl(params: { url: string; spaceId: string }): Promise<api.Embed>;
|
||||
|
||||
/**
|
||||
* 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<api.SearchSpaceResult[]>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RevisionPageDocument } from '@gitbook/api';
|
||||
import type { RevisionPageDocument, Space } from '@gitbook/api';
|
||||
import type { GitBookDataFetcher } from './types';
|
||||
|
||||
/**
|
||||
@@ -6,14 +6,18 @@ import type { GitBookDataFetcher } from './types';
|
||||
*/
|
||||
export async function getPageDocument(
|
||||
dataFetcher: GitBookDataFetcher,
|
||||
spaceId: string,
|
||||
space: Space,
|
||||
page: RevisionPageDocument
|
||||
) {
|
||||
if (page.documentId) {
|
||||
return dataFetcher.getDocument({ spaceId, documentId: page.documentId });
|
||||
return dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId });
|
||||
}
|
||||
if (page.computed) {
|
||||
return dataFetcher.getComputedDocument({ spaceId, source: page.computed });
|
||||
return dataFetcher.getComputedDocument({
|
||||
organizationId: space.organization,
|
||||
spaceId: space.id,
|
||||
source: page.computed,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -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<PublishedSiteContent> {
|
||||
const headersList = await headers();
|
||||
const siteURLData = headersList.get(MiddlewareHeaders.SiteURLData);
|
||||
|
||||
if (!siteURLData) {
|
||||
throw new Error(
|
||||
'Site URL data is not set by the middleware. This should only be called in a server action or a dynamic route.'
|
||||
);
|
||||
}
|
||||
|
||||
return JSON.parse(siteURLData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL from the middleware headers.
|
||||
* This function should only be called in a server action or a dynamic route.
|
||||
*/
|
||||
export async function getSiteURLFromMiddleware(): Promise<string> {
|
||||
const headersList = await headers();
|
||||
const siteURL = headersList.get(MiddlewareHeaders.SiteURL);
|
||||
if (!siteURL) {
|
||||
throw new Error('URL mode is not set by the middleware');
|
||||
}
|
||||
|
||||
return siteURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* For preview, the theme can be set via query string (?theme=light).
|
||||
* This function should only be called in a dynamic route.
|
||||
*/
|
||||
export async function getThemeFromMiddleware() {
|
||||
const headersList = await headers();
|
||||
@@ -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<string | null> {
|
||||
const headersList = await headers();
|
||||
const visitorAuthToken = headersList.get(MiddlewareHeaders.VisitorAuthToken);
|
||||
if (!visitorAuthToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return visitorAuthToken;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -64,26 +64,38 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass a x-forwarded-host and origin that are equal to ensure Next doesn't block server actions when proxied
|
||||
requestHeaders.set('x-forwarded-host', request.nextUrl.host);
|
||||
requestHeaders.set('origin', request.nextUrl.origin);
|
||||
|
||||
const route = [
|
||||
'sites',
|
||||
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,
|
||||
const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
|
||||
const response = NextResponse.rewrite(rewrittenURL, {
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
},
|
||||
});
|
||||
|
||||
// Add Content Security Policy header
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# gitbook
|
||||
|
||||
## 0.6.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 05ffd0e: Improving data cache management for computed content
|
||||
- 8beb5d6: Add input elements to ContentKit
|
||||
- Updated dependencies [53f5dbe]
|
||||
- Updated dependencies [05ffd0e]
|
||||
- Updated dependencies [8beb5d6]
|
||||
- @gitbook/openapi-parser@2.0.2
|
||||
- @gitbook/cache-tags@0.1.0
|
||||
- @gitbook/react-contentkit@0.6.1
|
||||
- @gitbook/react-openapi@1.0.5
|
||||
|
||||
## 0.6.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -225,11 +225,6 @@ const testCases: TestsCase[] = [
|
||||
baseUrl: 'https://docs.raydium.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.solv.finance',
|
||||
baseUrl: 'https://docs.solv.finance',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.fluentbit.io',
|
||||
baseUrl: 'https://docs.fluentbit.io',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "env-cmd --silent -f ../../.env.local next dev",
|
||||
@@ -18,6 +18,7 @@
|
||||
"dependencies": {
|
||||
"@gitbook/api": "0.96.1",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
@@ -62,7 +63,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",
|
||||
|
||||
@@ -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 (
|
||||
<SiteLayout
|
||||
context={context}
|
||||
forcedTheme={queryStringTheme}
|
||||
withTracking={await shouldTrackEvents()}
|
||||
visitorAuthToken={visitorAuthToken}
|
||||
>
|
||||
{children}
|
||||
</SiteLayout>
|
||||
|
||||
@@ -231,7 +231,7 @@ async function PDFPageDocument(props: {
|
||||
}) {
|
||||
const { page, context } = props;
|
||||
const { space } = context;
|
||||
const document = await getPageDocument(context.dataFetcher, space.id, page);
|
||||
const document = await getPageDocument(context.dataFetcher, space, page);
|
||||
|
||||
return (
|
||||
<PrintPage id={getPagePDFContainerId(page)}>
|
||||
|
||||
@@ -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) ? (
|
||||
<AdClassicRendering ad={ad} insightsAd={insightsAd} context={baseContext} />
|
||||
<AdClassicRendering ad={ad} insightsAd={insightsAd} context={context} />
|
||||
) : (
|
||||
<AdCoverRendering ad={ad} insightsAd={insightsAd} context={baseContext} />
|
||||
<AdCoverRendering ad={ad} insightsAd={insightsAd} context={context} />
|
||||
)}
|
||||
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
|
||||
</>
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import type { DocumentBlockCode } from '@gitbook/api';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useHasBeenInViewport } from '@/components/hooks/useHasBeenInViewport';
|
||||
|
||||
import { useInViewportListener } from '@/components/hooks/useInViewportListener';
|
||||
import { useScrollListener } from '@/components/hooks/useScrollListener';
|
||||
import { useDebounceCallback, useEventCallback } from 'usehooks-ts';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { CodeBlockRenderer } from './CodeBlockRenderer';
|
||||
import type { HighlightLine, RenderedInline } from './highlight';
|
||||
@@ -21,6 +22,8 @@ type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> &
|
||||
export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
const { block, style, inlines } = props;
|
||||
const blockRef = useRef<HTMLDivElement>(null);
|
||||
const processedRef = useRef(false);
|
||||
const isInViewportRef = useRef<boolean | null>(null);
|
||||
const [lines, setLines] = useState<HighlightLine[]>(() => plainHighlight(block, []));
|
||||
|
||||
// Preload the highlighter when the block is mounted.
|
||||
@@ -28,39 +31,44 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block));
|
||||
}, [block]);
|
||||
|
||||
// Check if the block is in the viewport to start highlighting it.
|
||||
const hasBeenInViewport = useHasBeenInViewport(blockRef, {
|
||||
rootMargin: '200px',
|
||||
});
|
||||
|
||||
// Highlight the block when it's in the viewport.
|
||||
useEffect(() => {
|
||||
if (hasBeenInViewport) {
|
||||
let canceled = false;
|
||||
import('./highlight').then(({ highlight }) => {
|
||||
// We use requestIdleCallback to avoid blocking the main thread
|
||||
// when scrolling.
|
||||
if (typeof requestIdleCallback === 'function') {
|
||||
requestIdleCallback(() =>
|
||||
highlight(block, inlines).then((result) => {
|
||||
if (!canceled) {
|
||||
setLines(result);
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
highlight(block, inlines).then((result) => {
|
||||
if (!canceled) {
|
||||
setLines(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
const runHighlight = useEventCallback(() => {
|
||||
if (processedRef.current) {
|
||||
return;
|
||||
}
|
||||
}, [hasBeenInViewport, block, inlines]);
|
||||
if (typeof window !== 'undefined') {
|
||||
import('./highlight').then(({ highlight }) => {
|
||||
highlight(block, inlines).then((lines) => {
|
||||
setLines(lines);
|
||||
processedRef.current = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
const debouncedRunHighlight = useDebounceCallback(runHighlight, 1000);
|
||||
|
||||
useInViewportListener(
|
||||
blockRef,
|
||||
(isInViewport, disconnect) => {
|
||||
// Disconnect once in viewport
|
||||
if (isInViewport) {
|
||||
disconnect();
|
||||
// If it's initially in viewport, we need to run the highlight
|
||||
if (isInViewportRef.current === null) {
|
||||
runHighlight();
|
||||
}
|
||||
}
|
||||
isInViewportRef.current = isInViewport;
|
||||
},
|
||||
{ rootMargin: '200px' }
|
||||
);
|
||||
|
||||
const handleScroll = useDebounceCallback(() => {
|
||||
if (isInViewportRef.current) {
|
||||
debouncedRunHighlight();
|
||||
}
|
||||
}, 80);
|
||||
|
||||
useScrollListener(handleScroll, useRef(typeof window !== 'undefined' ? window : null));
|
||||
|
||||
return <CodeBlockRenderer ref={blockRef} block={block} style={style} lines={lines} />;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
@apply flex gap-4;
|
||||
}
|
||||
|
||||
/** Add margin bottom to all stacks that are not the last child and have a form */
|
||||
.contentkit-stack:not(:last-child):has(.contentkit-form) {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
.contentkit-hstack {
|
||||
@apply flex-row gap-4 items-center;
|
||||
@apply flex-row gap-3 items-center;
|
||||
}
|
||||
.contentkit-hstack.contentkit-stack-align-center {
|
||||
@apply justify-center;
|
||||
@@ -88,17 +93,23 @@
|
||||
|
||||
/** Buttons */
|
||||
.contentkit-button {
|
||||
@apply flex gap-2 items-center justify-center px-2 py-1 rounded
|
||||
border border-tint hover:border-tint-hover
|
||||
text-tint hover:text-tint-strong;
|
||||
@apply text-sm px-3 h-8 text-center inline-block py-1.5 rounded-md straight-corners:rounded-none place-self-start ring-1 ring-tint hover:ring-tint-hover shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12 hover:scale-105 active:scale-100 transition-all grow-0 shrink-0 truncate w-full;
|
||||
}
|
||||
|
||||
.contentkit-button-style-primary {
|
||||
@apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover ring-0 contrast-more:ring-1;
|
||||
}
|
||||
|
||||
.contentkit-button-style-secondary {
|
||||
@apply bg-tint text-tint hover:bg-tint-hover hover:text-primary contrast-more:bg-tint-subtle;
|
||||
}
|
||||
|
||||
.contentkit-button .contentkit-icon {
|
||||
@apply w-4 h-4;
|
||||
}
|
||||
|
||||
.contentkit-button .contentkit-button-label {
|
||||
@apply text-sm font-medium text-tint-strong;
|
||||
.contentkit-button-loading {
|
||||
@apply size-5 flex mx-auto animate-spin;
|
||||
}
|
||||
|
||||
/** Modals */
|
||||
@@ -143,7 +154,21 @@
|
||||
|
||||
/** Text input */
|
||||
.contentkit-textinput {
|
||||
@apply w-full px-2 py-1 rounded border border-tint text-sm text-tint;
|
||||
@apply w-full rounded border border-tint text-tint-strong placeholder:text-tint flex resize-none flex-1 px-2 py-1.5 text-sm bg-transparent whitespace-pre-line;
|
||||
@apply focus:outline-primary focus:border-primary;
|
||||
}
|
||||
|
||||
/** Form */
|
||||
.contentkit-form {
|
||||
@apply flex flex-col gap-1;
|
||||
}
|
||||
|
||||
.contentkit-form:not(:first-of-type) {
|
||||
@apply mt-4;
|
||||
}
|
||||
|
||||
.contentkit-label {
|
||||
@apply text-sm font-medium text-tint-strong;
|
||||
}
|
||||
|
||||
/** Dividers */
|
||||
|
||||
@@ -92,6 +92,7 @@ const getBlockHeight = memoize((block: DocumentBlock | undefined): number => {
|
||||
case 'images':
|
||||
case 'image':
|
||||
case 'swagger':
|
||||
case 'openapi-operation':
|
||||
return 768 / (16 / 9);
|
||||
case 'file':
|
||||
case 'expandable':
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<SearchResultsRef>(null);
|
||||
@@ -299,17 +287,13 @@ function SearchModalBody(
|
||||
{!state.ask || !withAsk ? (
|
||||
<SearchResults
|
||||
ref={resultsRef}
|
||||
pointer={pointer}
|
||||
revisionId={revisionId}
|
||||
global={isMultiVariants && state.global}
|
||||
query={state.query}
|
||||
withAsk={withAsk}
|
||||
onSwitchToAsk={onSwitchToAsk}
|
||||
/>
|
||||
) : null}
|
||||
{state.query && state.ask && withAsk ? (
|
||||
<SearchAskAnswer pointer={pointer} query={state.query} />
|
||||
) : null}
|
||||
{state.query && state.ask && withAsk ? <SearchAskAnswer query={state.query} /> : null}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<SearchResultsRef>
|
||||
) {
|
||||
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) {
|
||||
|
||||
@@ -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<OrderedComputedResult[]> {
|
||||
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<OrderedComputedResult[]> {
|
||||
return await searchSiteContent({
|
||||
pointer,
|
||||
export async function searchAllSiteContent(query: string): Promise<OrderedComputedResult[]> {
|
||||
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<OrderedComputedResult[]> {
|
||||
const siteSpaceId = pointer.siteSpaceId;
|
||||
assert(siteSpaceId, 'Expected siteSpaceId for searchSiteSpaceContent');
|
||||
export async function searchSiteSpaceContent(query: string): Promise<OrderedComputedResult[]> {
|
||||
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<AskAnswerResult | undefined>();
|
||||
|
||||
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<string, Promise<RevisionPage[]>>();
|
||||
for await (const chunk of stream) {
|
||||
const answer = chunk.answer;
|
||||
const spacePromises = new Map<string, Promise<RevisionPage[]>>();
|
||||
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<string, RevisionPage[]>());
|
||||
// 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<string, RevisionPage[]>());
|
||||
});
|
||||
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<SearchAIRecommendedQuestionStream | undefined>();
|
||||
|
||||
(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<OrderedComputedResult[]> {
|
||||
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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
>
|
||||
<SpaceLayout context={context} withTracking={withTracking}>
|
||||
<SpaceLayout
|
||||
context={context}
|
||||
withTracking={withTracking}
|
||||
visitorAuthToken={visitorAuthToken}
|
||||
>
|
||||
{children}
|
||||
</SpaceLayout>
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export async function SitePage(props: SitePageProps) {
|
||||
const withSections = Boolean(sections && sections.list.length > 0);
|
||||
const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
|
||||
|
||||
const document = await getPageDocument(context.dataFetcher, context.space.id, page);
|
||||
const document = await getPageDocument(context.dataFetcher, context.space, page);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -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: {
|
||||
<InsightsProvider
|
||||
enabled={withTracking}
|
||||
apiHost={context.dataFetcher.apiEndpoint}
|
||||
visitorAuthToken={context.visitorAuthToken}
|
||||
visitorAuthToken={visitorAuthToken}
|
||||
organizationId={context.organizationId}
|
||||
siteId={context.site.id}
|
||||
siteSectionId={context.sections?.current?.id ?? null}
|
||||
@@ -129,11 +131,9 @@ export function SpaceLayout(props: {
|
||||
|
||||
<React.Suspense fallback={null}>
|
||||
<SearchModal
|
||||
revisionId={context.revisionId}
|
||||
spaceTitle={siteSpace.title}
|
||||
withAsk={customization.aiSearch.enabled}
|
||||
isMultiVariants={isMultiVariants}
|
||||
pointer={getSitePointerFromContext(context)}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</InsightsProvider>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client';
|
||||
import { useEffect, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
const HAS_INTERSECTION_OBSERVER = typeof IntersectionObserver !== 'undefined';
|
||||
|
||||
/**
|
||||
* Watch an element to know when it is in the viewport.
|
||||
*/
|
||||
export function useInViewportListener(
|
||||
containerRef: React.RefObject<HTMLElement>,
|
||||
listener: (isIntersecting: boolean, disconnect: () => void) => void,
|
||||
options?: Pick<IntersectionObserverInit, 'root' | 'rootMargin' | 'threshold'>
|
||||
) {
|
||||
const listenerRef = useRef(listener);
|
||||
useLayoutEffect(() => {
|
||||
listenerRef.current = listener;
|
||||
});
|
||||
const isIntersectingRef = useRef(false);
|
||||
useEffect(() => {
|
||||
// We set the element as visible if the IntersectionObserver API is not available.
|
||||
// we have to do it in the `useEffect` to be SSR compatible.
|
||||
if (!HAS_INTERSECTION_OBSERVER) {
|
||||
listenerRef.current(true, () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
isIntersectingRef.current = entry.isIntersecting;
|
||||
listenerRef.current(entry.isIntersecting, () => {
|
||||
observer.disconnect();
|
||||
});
|
||||
},
|
||||
{
|
||||
root: options?.root,
|
||||
rootMargin: options?.rootMargin,
|
||||
threshold: options?.threshold,
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(containerRef.current);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [containerRef, options?.root, options?.rootMargin, options?.threshold]);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
export type StreamResponseChunk<T> = {
|
||||
iteratorResult: IteratorResult<T>;
|
||||
next?: Promise<StreamResponseChunk<T>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<T, P extends any[]>(
|
||||
createGenerator: (...args: P) => AsyncGenerator<T>
|
||||
) {
|
||||
return async (...args: Parameters<typeof createGenerator>) => {
|
||||
const generator = createGenerator(...args);
|
||||
return streamChunk<T>(generator);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over the response of an async generator function.
|
||||
*/
|
||||
export function iterateStreamResponse<T>(streamResponse: Promise<StreamResponseChunk<T>>) {
|
||||
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<T>(generator: AsyncGenerator<T>) {
|
||||
const next = generator.next();
|
||||
return new Promise<StreamResponseChunk<T>>((resolve, reject) => {
|
||||
next.then((res) => {
|
||||
if (res.done) resolve({ iteratorResult: res });
|
||||
else resolve({ iteratorResult: res, next: streamChunk(generator) });
|
||||
});
|
||||
next.catch((error) => reject(error));
|
||||
});
|
||||
}
|
||||
+33
-119
@@ -1,4 +1,5 @@
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
|
||||
import 'server-only';
|
||||
|
||||
import {
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
type RevisionReusableContent,
|
||||
} from '@gitbook/api';
|
||||
import type { GitBookDataFetcher } from '@v2/lib/data/types';
|
||||
import assertNever from 'assert-never';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import { batch } from './async';
|
||||
@@ -171,7 +171,7 @@ export type PublishedContentWithCache =
|
||||
export const getUserById = cache({
|
||||
name: 'api.getUserById',
|
||||
tag: (userId) =>
|
||||
getAPICacheTag({
|
||||
getCacheTag({
|
||||
tag: 'user',
|
||||
user: userId,
|
||||
}),
|
||||
@@ -204,7 +204,7 @@ export const getUserById = cache({
|
||||
export const getLatestOpenAPISpecVersionContent = cache({
|
||||
name: 'api.getLatestOpenApiSpecVersionContent',
|
||||
tag: (organization, openAPISpec) =>
|
||||
getAPICacheTag({
|
||||
getCacheTag({
|
||||
tag: 'openapi',
|
||||
organization,
|
||||
openAPISpec,
|
||||
@@ -240,7 +240,7 @@ export const getLatestOpenAPISpecVersionContent = cache({
|
||||
export const getPublishedContentByUrl = cache({
|
||||
name: 'api.getPublishedContentByUrl.v4',
|
||||
tag: (url) =>
|
||||
getAPICacheTag({
|
||||
getCacheTag({
|
||||
tag: 'url',
|
||||
hostname: new URL(url).hostname,
|
||||
}),
|
||||
@@ -301,7 +301,7 @@ export const getPublishedContentByUrl = cache({
|
||||
*/
|
||||
export const getSpace = cache({
|
||||
name: 'api.getSpace',
|
||||
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
|
||||
tag: (spaceId) => getCacheTag({ tag: 'space', space: spaceId }),
|
||||
get: async (spaceId: string, shareKey: string | undefined, options: CacheFunctionOptions) => {
|
||||
const apiCtx = await api();
|
||||
const response = await apiCtx.client.spaces.getSpaceById(
|
||||
@@ -330,7 +330,7 @@ function checkHasErrorCode(error: unknown, code: number) {
|
||||
export const getChangeRequest = cache({
|
||||
name: 'api.getChangeRequest',
|
||||
tag: (spaceId, changeRequestId) =>
|
||||
getAPICacheTag({ tag: 'change-request', space: spaceId, changeRequest: changeRequestId }),
|
||||
getCacheTag({ tag: 'change-request', space: spaceId, changeRequest: changeRequestId }),
|
||||
get: async (spaceId: string, changeRequestId: string, options: CacheFunctionOptions) => {
|
||||
const apiCtx = await api();
|
||||
try {
|
||||
@@ -379,7 +379,7 @@ const getAPIContextId = async () => {
|
||||
export const getRevision = cache({
|
||||
name: 'api.getRevision.v2',
|
||||
tag: (spaceId, revisionId) =>
|
||||
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (
|
||||
spaceId: string,
|
||||
@@ -411,7 +411,7 @@ export const getRevision = cache({
|
||||
export const getRevisionPages = cache({
|
||||
name: 'api.getRevisionPages.v4',
|
||||
tag: (spaceId, revisionId) =>
|
||||
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (
|
||||
spaceId: string,
|
||||
@@ -446,7 +446,7 @@ export const getRevisionPages = cache({
|
||||
export const getRevisionPageByPath = cache({
|
||||
name: 'api.getRevisionPageByPath.v3',
|
||||
tag: (spaceId, revisionId) =>
|
||||
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (
|
||||
spaceId: string,
|
||||
@@ -492,7 +492,7 @@ export const getRevisionPageByPath = cache({
|
||||
const getRevisionFileById = cache({
|
||||
name: 'api.getRevisionFile.v3',
|
||||
tag: (spaceId, revisionId) =>
|
||||
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
get: async (
|
||||
spaceId: string,
|
||||
revisionId: string,
|
||||
@@ -528,7 +528,7 @@ const getRevisionFileById = cache({
|
||||
const getRevisionReusableContentById = cache({
|
||||
name: 'api.getRevisionReusableContentById.v1',
|
||||
tag: (spaceId, revisionId) =>
|
||||
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (
|
||||
spaceId: string,
|
||||
@@ -569,7 +569,7 @@ const getRevisionReusableContentById = cache({
|
||||
const getRevisionAllFiles = cache({
|
||||
name: 'api.getRevisionAllFiles.v2',
|
||||
tag: (spaceId, revisionId) =>
|
||||
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
|
||||
get: async (spaceId: string, revisionId: string, options: CacheFunctionOptions) => {
|
||||
const response = await getAll(
|
||||
async (params) => {
|
||||
@@ -684,7 +684,7 @@ export const getReusableContent = async (
|
||||
export const getDocument = cache({
|
||||
name: 'api.getDocument.v2',
|
||||
tag: (spaceId, documentId) =>
|
||||
getAPICacheTag({ tag: 'document', space: spaceId, document: documentId }),
|
||||
getCacheTag({ tag: 'document', space: spaceId, document: documentId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (spaceId: string, documentId: string, options: CacheFunctionOptions) => {
|
||||
const apiCtx = await api();
|
||||
@@ -712,14 +712,21 @@ export const getDocument = cache({
|
||||
*/
|
||||
export const getComputedDocument = cache({
|
||||
name: 'api.getComputedDocument',
|
||||
tag: (spaceId, source) =>
|
||||
getAPICacheTag({
|
||||
tag: 'computed-document',
|
||||
space: spaceId,
|
||||
integration: source.integration,
|
||||
}),
|
||||
tag: (organizationId, spaceId, source) =>
|
||||
getComputedContentSourceCacheTags(
|
||||
{
|
||||
organizationId,
|
||||
spaceId,
|
||||
},
|
||||
source
|
||||
)[0],
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (spaceId: string, source: ComputedContentSource, options: CacheFunctionOptions) => {
|
||||
get: async (
|
||||
_organizationId: string,
|
||||
spaceId: string,
|
||||
source: ComputedContentSource,
|
||||
options: CacheFunctionOptions
|
||||
) => {
|
||||
const apiCtx = await api();
|
||||
const response = await apiCtx.client.spaces.getComputedDocument(
|
||||
spaceId,
|
||||
@@ -750,7 +757,7 @@ function validateSiteRedirectSource(source: string) {
|
||||
*/
|
||||
export const getSiteRedirectBySource = cache({
|
||||
name: 'api.getSiteRedirectBySource',
|
||||
tag: ({ siteId }) => getAPICacheTag({ tag: 'site', site: siteId }),
|
||||
tag: ({ siteId }) => getCacheTag({ tag: 'site', site: siteId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (
|
||||
args: {
|
||||
@@ -811,7 +818,7 @@ export const getSiteRedirectBySource = cache({
|
||||
*/
|
||||
export const getSite = cache({
|
||||
name: 'api.getSite',
|
||||
tag: (_organizationId, siteId) => getAPICacheTag({ tag: 'site', site: siteId }),
|
||||
tag: (_organizationId, siteId) => getCacheTag({ tag: 'site', site: siteId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (organizationId: string, siteId: string, options: CacheFunctionOptions) => {
|
||||
const apiCtx = await api();
|
||||
@@ -830,7 +837,7 @@ export const getSite = cache({
|
||||
*/
|
||||
export const getPublishedContentSite = cache({
|
||||
name: 'api.getPublishedContentSite',
|
||||
tag: ({ siteId }) => getAPICacheTag({ tag: 'site', site: siteId }),
|
||||
tag: ({ siteId }) => getCacheTag({ tag: 'site', site: siteId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (
|
||||
args: {
|
||||
@@ -907,7 +914,7 @@ export async function getSpaceContentData(
|
||||
*/
|
||||
export const searchSiteContent = cache({
|
||||
name: 'api.searchSiteContent',
|
||||
tag: (_organizationId, siteId) => getAPICacheTag({ tag: 'site', site: siteId }),
|
||||
tag: (_organizationId, siteId) => getCacheTag({ tag: 'site', site: siteId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (
|
||||
organizationId: string,
|
||||
@@ -947,7 +954,7 @@ export const searchSiteContent = cache({
|
||||
*/
|
||||
export const renderIntegrationUi = cache({
|
||||
name: 'api.renderIntegrationUi',
|
||||
tag: (integrationName) => getAPICacheTag({ tag: 'integration', integration: integrationName }),
|
||||
tag: (integrationName) => getCacheTag({ tag: 'integration', integration: integrationName }),
|
||||
get: async (
|
||||
integrationName: string,
|
||||
request: RequestRenderIntegrationUI,
|
||||
@@ -971,7 +978,7 @@ export const renderIntegrationUi = cache({
|
||||
*/
|
||||
export const getEmbedByUrlInSpace = cache({
|
||||
name: 'api.getEmbedByUrlInSpace',
|
||||
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
|
||||
tag: (spaceId) => getCacheTag({ tag: 'space', space: spaceId }),
|
||||
get: async (spaceId: string, url: string, options: CacheFunctionOptions) => {
|
||||
const apiCtx = await api();
|
||||
const response = await apiCtx.client.spaces.getEmbedByUrlInSpace(
|
||||
@@ -986,99 +993,6 @@ export const getEmbedByUrlInSpace = cache({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a cache tag for the API.
|
||||
*/
|
||||
export function getAPICacheTag(
|
||||
spec: // All data related to a user
|
||||
| {
|
||||
tag: 'user';
|
||||
user: string;
|
||||
}
|
||||
// All data related to a space
|
||||
| {
|
||||
tag: 'space';
|
||||
space: string;
|
||||
}
|
||||
// All data related to an integration
|
||||
| {
|
||||
tag: 'integration';
|
||||
integration: string;
|
||||
}
|
||||
// All data related to a change request
|
||||
| {
|
||||
tag: 'change-request';
|
||||
space: string;
|
||||
changeRequest: string;
|
||||
}
|
||||
// Immutable data related to a revision
|
||||
| {
|
||||
tag: 'revision';
|
||||
space: string;
|
||||
revision: string;
|
||||
}
|
||||
// Immutable data related to a document
|
||||
| {
|
||||
tag: 'document';
|
||||
space: string;
|
||||
document: string;
|
||||
}
|
||||
// Immutable data related to a computed document
|
||||
| {
|
||||
tag: 'computed-document';
|
||||
space: string;
|
||||
integration: string;
|
||||
}
|
||||
// All data related to the URL of a content
|
||||
| {
|
||||
tag: 'url';
|
||||
hostname: string;
|
||||
}
|
||||
// All data related to a collection
|
||||
| {
|
||||
tag: 'collection';
|
||||
collection: string;
|
||||
}
|
||||
// All data related to a site
|
||||
| {
|
||||
tag: 'site';
|
||||
site: string;
|
||||
}
|
||||
// All data related to an OpenAPI spec
|
||||
| {
|
||||
tag: 'openapi';
|
||||
organization: string;
|
||||
openAPISpec: string;
|
||||
}
|
||||
): string {
|
||||
switch (spec.tag) {
|
||||
case 'user':
|
||||
return `user:${spec.user}`;
|
||||
case 'url':
|
||||
return `url:${spec.hostname}`;
|
||||
case 'space':
|
||||
return `space:${spec.space}`;
|
||||
case 'change-request':
|
||||
return `space:${spec.space}:change-request:${spec.changeRequest}`;
|
||||
case 'revision':
|
||||
return `space:${spec.space}:revision:${spec.revision}`;
|
||||
case 'document':
|
||||
return `space:${spec.space}:document:${spec.document}`;
|
||||
case 'computed-document':
|
||||
return `space:${spec.space}:computed-document:${spec.integration}`;
|
||||
case 'collection':
|
||||
return `collection:${spec.collection}`;
|
||||
case 'site':
|
||||
return `site:${spec.site}`;
|
||||
case 'integration':
|
||||
return `integration:${spec.integration}`;
|
||||
case 'openapi':
|
||||
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
|
||||
default:
|
||||
assertNever(spec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user agent to use for API requests.
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function getDocumentSections(
|
||||
});
|
||||
}
|
||||
|
||||
if (block.type === 'swagger' && block.meta?.id) {
|
||||
if ((block.type === 'swagger' || block.type === 'openapi-operation') && block.meta?.id) {
|
||||
const { data: operation } = await resolveOpenAPIBlock({
|
||||
block,
|
||||
context,
|
||||
|
||||
@@ -22,7 +22,7 @@ export function hasFullWidthBlock(document: JSONDocument): boolean {
|
||||
if (node.data && 'fullWidth' in node.data && node.data.fullWidth) {
|
||||
return true;
|
||||
}
|
||||
if (node.type === 'swagger') {
|
||||
if (node.type === 'swagger' || node.type === 'openapi-operation') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,8 @@ export function getBlockTitle(block: DocumentBlock): string {
|
||||
return block.data.title ?? '';
|
||||
}
|
||||
|
||||
case 'swagger': {
|
||||
case 'swagger':
|
||||
case 'openapi-operation': {
|
||||
return `${block.data.method?.toUpperCase()} ${block.data.path}`;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -130,7 +130,7 @@ export async function resolveContentRef(
|
||||
text = `#${anchor}`;
|
||||
|
||||
if (resolveAnchorText) {
|
||||
const document = await getPageDocument(dataFetcher, space.id, page);
|
||||
const document = await getPageDocument(dataFetcher, space, page);
|
||||
if (document) {
|
||||
const block = getBlockById(document, anchor);
|
||||
if (block) {
|
||||
|
||||
@@ -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<GitBookBaseContext> {
|
||||
async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
|
||||
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);
|
||||
},
|
||||
@@ -117,7 +128,7 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
|
||||
},
|
||||
|
||||
getComputedDocument(params) {
|
||||
return getComputedDocument(params.spaceId, params.source);
|
||||
return getComputedDocument(params.organizationId, params.spaceId, params.source);
|
||||
},
|
||||
|
||||
getRevisionPages(params) {
|
||||
@@ -145,7 +156,15 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
|
||||
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<GitBookDataFetcher> {
|
||||
*/
|
||||
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);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/openapi-parser
|
||||
|
||||
## 2.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 53f5dbe: Fix typing of `shouldIgnoreEntity`
|
||||
|
||||
## 2.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@scalar/openapi-parser": "^0.10.4",
|
||||
|
||||
@@ -4,10 +4,7 @@ import type { OpenAPIV3 } from '@scalar/openapi-types';
|
||||
* Check if an entity should be ignored
|
||||
*/
|
||||
export function shouldIgnoreEntity(
|
||||
data:
|
||||
| undefined
|
||||
| Pick<OpenAPIV3.TagObject, 'x-internal' | 'x-gitbook-ignore'>
|
||||
| Pick<OpenAPIV3.OperationObject, 'x-internal' | 'x-gitbook-ignore'>
|
||||
data: undefined | OpenAPIV3.TagObject | OpenAPIV3.OperationObject
|
||||
) {
|
||||
return data?.['x-internal'] === true || data?.['x-gitbook-ignore'] === true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/react-contentkit
|
||||
|
||||
## 0.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8beb5d6: Add input elements to ContentKit
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -11,7 +11,8 @@
|
||||
"dependencies": {
|
||||
"classnames": "^2.5.1",
|
||||
"@gitbook/api": "0.96.1",
|
||||
"assert-never": "^1.2.1"
|
||||
"assert-never": "^1.2.1",
|
||||
"@gitbook/icons": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ElementCodeBlock } from './ElementCodeBlock';
|
||||
import { ElementDivider } from './ElementDivider';
|
||||
import { ElementIcon } from './ElementIcon';
|
||||
import { ElementImage } from './ElementImage';
|
||||
import { ElementInput } from './ElementInput';
|
||||
import { ElementMarkdown } from './ElementMarkdown';
|
||||
import { ElementModal } from './ElementModal';
|
||||
import { ElementStack } from './ElementStack';
|
||||
@@ -152,6 +153,8 @@ export function Element(props: {
|
||||
return <ElementCodeBlock element={element} context={context} state={state} />;
|
||||
case 'webframe':
|
||||
return <ElementWebframe element={element} />;
|
||||
case 'input':
|
||||
return <ElementInput element={element} context={context} state={state} />;
|
||||
default:
|
||||
return (
|
||||
<pre
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type { ContentKitButton } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -19,16 +20,14 @@ export function ElementButton(
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
// TODO:
|
||||
// - loading
|
||||
// - confirm
|
||||
// - confirm dialog
|
||||
|
||||
return (
|
||||
<button
|
||||
title={element.tooltip}
|
||||
className={classNames(
|
||||
'contentkit-button',
|
||||
`contentkit-button-style-${element.style ?? 'secondary'}`,
|
||||
loading ? 'contentkit-button-loading' : null
|
||||
`contentkit-button-style-${element.style ?? 'secondary'}`
|
||||
)}
|
||||
onClick={(event) => {
|
||||
if (element.disabled || loading) {
|
||||
@@ -43,11 +42,17 @@ export function ElementButton(
|
||||
});
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{element.label ? (
|
||||
<span className="contentkit-button-label">{element.label}</span>
|
||||
) : null}
|
||||
{trailingIcon}
|
||||
{loading ? (
|
||||
<Icon icon="spinner" className="contentkit-button-loading" />
|
||||
) : (
|
||||
<>
|
||||
{icon}
|
||||
{element.label ? (
|
||||
<span className="contentkit-button-label">{element.label}</span>
|
||||
) : null}
|
||||
{trailingIcon}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type {
|
||||
ContentKitButton,
|
||||
ContentKitCheckbox,
|
||||
ContentKitCodeBlock,
|
||||
ContentKitInput,
|
||||
ContentKitRadio,
|
||||
ContentKitSelect,
|
||||
ContentKitSwitch,
|
||||
ContentKitTextInput,
|
||||
} from '@gitbook/api';
|
||||
import classNames from 'classnames';
|
||||
import { ElementButton } from './ElementButton';
|
||||
import { ElementCodeBlock } from './ElementCodeBlock';
|
||||
import { ElementIcon } from './ElementIcon';
|
||||
import { ElementTextInput } from './ElementTextInput';
|
||||
import type { ContentKitServerContext } from './types';
|
||||
|
||||
export function ElementInput(props: {
|
||||
element: ContentKitInput;
|
||||
context: ContentKitServerContext;
|
||||
state: object;
|
||||
}) {
|
||||
const { element, context, state } = props;
|
||||
const { label, element: childElement } = element;
|
||||
|
||||
return (
|
||||
<div className={classNames('contentkit-form')}>
|
||||
{label ? <label className="contentkit-label">{label}</label> : null}
|
||||
{getInputElement({ element: childElement, context, state })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getInputElement(props: {
|
||||
element:
|
||||
| ContentKitTextInput
|
||||
| ContentKitSelect
|
||||
| ContentKitSwitch
|
||||
| ContentKitRadio
|
||||
| ContentKitCheckbox
|
||||
| ContentKitButton
|
||||
| ContentKitCodeBlock;
|
||||
context: ContentKitServerContext;
|
||||
state: object;
|
||||
}) {
|
||||
const { element, context, state } = props;
|
||||
|
||||
//!!TODO add support for:
|
||||
// - select
|
||||
// - switch
|
||||
// - radio
|
||||
// - checkbox
|
||||
|
||||
switch (element.type) {
|
||||
case 'textinput':
|
||||
return <ElementTextInput element={element} />;
|
||||
case 'button':
|
||||
return (
|
||||
<ElementButton
|
||||
element={element}
|
||||
icon={
|
||||
element.icon ? <ElementIcon icon={element.icon} context={context} /> : null
|
||||
}
|
||||
trailingIcon={
|
||||
element.trailingIcon ? (
|
||||
<ElementIcon icon={element.trailingIcon} context={context} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'codeblock':
|
||||
return <ElementCodeBlock element={element} context={context} state={state} />;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ export function ElementTextInput(props: ContentKitClientElementProps<ContentKitT
|
||||
value={value}
|
||||
placeholder={element.placeholder}
|
||||
onChange={onChange}
|
||||
rows={4}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @gitbook/react-openapi
|
||||
|
||||
## 1.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [53f5dbe]
|
||||
- @gitbook/openapi-parser@2.0.2
|
||||
|
||||
## 1.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
|
||||
@@ -4,6 +4,7 @@ import clsx from 'clsx';
|
||||
import { useRef, useState } from 'react';
|
||||
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
||||
import { useDisclosureState } from 'react-stately';
|
||||
import { Section, SectionBody, SectionHeader, SectionHeaderContent } from './StaticSection';
|
||||
|
||||
interface InteractiveSectionTab {
|
||||
key: string;
|
||||
@@ -63,7 +64,7 @@ export function InteractiveSection(props: {
|
||||
const { isFocusVisible, focusProps } = useFocusRing();
|
||||
|
||||
return (
|
||||
<div
|
||||
<Section
|
||||
id={id}
|
||||
className={clsx(
|
||||
'openapi-section',
|
||||
@@ -73,20 +74,15 @@ export function InteractiveSection(props: {
|
||||
)}
|
||||
>
|
||||
{header ? (
|
||||
<div
|
||||
<SectionHeader
|
||||
onClick={() => {
|
||||
if (toggeable) {
|
||||
state.toggle();
|
||||
}
|
||||
}}
|
||||
className={clsx('openapi-section-header', `${className}-header`)}
|
||||
className={className}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'openapi-section-header-content',
|
||||
`${className}-header-content`
|
||||
)}
|
||||
>
|
||||
<SectionHeaderContent className={className}>
|
||||
{(children || selectedTab?.body) && toggeable ? (
|
||||
<button
|
||||
{...mergeProps(buttonProps, focusProps)}
|
||||
@@ -102,7 +98,7 @@ export function InteractiveSection(props: {
|
||||
</button>
|
||||
) : null}
|
||||
{header}
|
||||
</div>
|
||||
</SectionHeaderContent>
|
||||
<div
|
||||
className={clsx(
|
||||
'openapi-section-header-controls',
|
||||
@@ -133,23 +129,19 @@ export function InteractiveSection(props: {
|
||||
</select>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
) : null}
|
||||
{(!toggeable || state.isExpanded) && (children || selectedTab?.body) ? (
|
||||
<div
|
||||
ref={panelRef}
|
||||
{...panelProps}
|
||||
className={clsx('openapi-section-body', `${className}-body`)}
|
||||
>
|
||||
<SectionBody ref={panelRef} {...panelProps} className={className}>
|
||||
{children}
|
||||
{selectedTab?.body}
|
||||
</div>
|
||||
</SectionBody>
|
||||
) : null}
|
||||
{overlay ? (
|
||||
<div className={clsx('openapi-section-overlay', `${className}-overlay`)}>
|
||||
{overlay}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
|
||||
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
@@ -117,9 +117,9 @@ export function OpenAPICodeSample(props: {
|
||||
|
||||
return (
|
||||
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
|
||||
<InteractiveSection header={<OpenAPITabsList />} className="openapi-codesample">
|
||||
<StaticSection header={<OpenAPITabsList />} className="openapi-codesample">
|
||||
<OpenAPITabsPanels />
|
||||
</InteractiveSection>
|
||||
</StaticSection>
|
||||
</OpenAPITabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import type React from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useRef, useState } from 'react';
|
||||
import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
|
||||
import {
|
||||
type DisclosureGroupProps,
|
||||
type DisclosureGroupState,
|
||||
useDisclosureGroupState,
|
||||
useDisclosureState,
|
||||
} from 'react-stately';
|
||||
|
||||
interface Props {
|
||||
groups: TDisclosureGroup[];
|
||||
icon?: React.ReactNode;
|
||||
@@ -13,15 +24,6 @@ type TDisclosureGroup = {
|
||||
}[];
|
||||
};
|
||||
|
||||
import { createContext, useContext, useRef, useState } from 'react';
|
||||
import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
|
||||
import {
|
||||
type DisclosureGroupProps,
|
||||
type DisclosureGroupState,
|
||||
useDisclosureGroupState,
|
||||
useDisclosureState,
|
||||
} from 'react-stately';
|
||||
|
||||
const DisclosureGroupStateContext = createContext<DisclosureGroupState | null>(null);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import { generateSchemaExample } from './generateSchemaExample';
|
||||
import { json2xml } from './json2xml';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
@@ -84,9 +84,9 @@ export function OpenAPIResponseExample(props: {
|
||||
|
||||
return (
|
||||
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
|
||||
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
|
||||
<StaticSection header={<OpenAPITabsList />} className="openapi-response-example">
|
||||
<OpenAPITabsPanels />
|
||||
</InteractiveSection>
|
||||
</StaticSection>
|
||||
</OpenAPITabs>
|
||||
);
|
||||
}
|
||||
@@ -134,12 +134,9 @@ function OpenAPIResponse(props: {
|
||||
|
||||
return (
|
||||
<OpenAPITabs stateKey={createStateKey('response-media-types')} items={tabs}>
|
||||
<InteractiveSection
|
||||
header={<OpenAPITabsList />}
|
||||
className="openapi-response-media-types"
|
||||
>
|
||||
<StaticSection header={<OpenAPITabsList />} className="openapi-response-media-types">
|
||||
<OpenAPITabsPanels />
|
||||
</InteractiveSection>
|
||||
</StaticSection>
|
||||
</OpenAPITabs>
|
||||
);
|
||||
}
|
||||
@@ -173,23 +170,19 @@ function OpenAPIResponseMediaType(props: {
|
||||
key: example.key,
|
||||
label: example.example.summary || example.key,
|
||||
body: (
|
||||
<OpenAPIExample
|
||||
example={firstExample.example}
|
||||
context={props.context}
|
||||
syntax={syntax}
|
||||
/>
|
||||
<OpenAPIExample example={example.example} context={props.context} syntax={syntax} />
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<OpenAPITabs stateKey={createStateKey('response-media-type-examples')} items={tabs}>
|
||||
<InteractiveSection
|
||||
<StaticSection
|
||||
header={<OpenAPITabsList />}
|
||||
className="openapi-response-media-type-examples"
|
||||
>
|
||||
<OpenAPITabsPanels />
|
||||
</InteractiveSection>
|
||||
</StaticSection>
|
||||
</OpenAPITabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
|
||||
import { OpenAPIResponse } from './OpenAPIResponse';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,7 @@ export function OpenAPIResponses(props: {
|
||||
const { responses, context } = props;
|
||||
|
||||
return (
|
||||
<InteractiveSection header="Responses" className="openapi-responses">
|
||||
<StaticSection header="Responses" className="openapi-responses">
|
||||
<OpenAPIDisclosureGroup
|
||||
allowsMultipleExpanded
|
||||
icon={context.icons.chevronRight}
|
||||
@@ -58,6 +58,6 @@ export function OpenAPIResponses(props: {
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</InteractiveSection>
|
||||
</StaticSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,9 +119,9 @@ export function OpenAPISchemaProperties(props: {
|
||||
|
||||
return (
|
||||
<div id={id} className="openapi-schema-properties">
|
||||
{properties.map((property) => (
|
||||
{properties.map((property, index) => (
|
||||
<OpenAPISchemaProperty
|
||||
key={property.propertyName}
|
||||
key={index}
|
||||
circularRefs={circularRefs}
|
||||
{...property}
|
||||
context={context}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import type { OpenAPI } from '@gitbook/openapi-parser';
|
||||
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
import { OpenAPIRequestBody } from './OpenAPIRequestBody';
|
||||
import { OpenAPIResponses } from './OpenAPIResponses';
|
||||
import { OpenAPISchemaProperties } from './OpenAPISchema';
|
||||
import { OpenAPISecurities } from './OpenAPISecurities';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
|
||||
import { parameterToProperty } from './utils';
|
||||
|
||||
@@ -32,7 +30,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
|
||||
|
||||
{parameterGroups.map((group) => {
|
||||
return (
|
||||
<InteractiveSection
|
||||
<StaticSection
|
||||
key={group.key}
|
||||
className="openapi-parameters"
|
||||
header={group.label}
|
||||
@@ -41,7 +39,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
|
||||
properties={group.parameters.map(parameterToProperty)}
|
||||
context={context}
|
||||
/>
|
||||
</InteractiveSection>
|
||||
</StaticSection>
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
|
||||
import { useIntersectionObserver } from 'usehooks-ts';
|
||||
import { useEventCallback } from 'usehooks-ts';
|
||||
import { Markdown } from './Markdown';
|
||||
import { useSyncedTabsGlobalState } from './useSyncedTabsGlobalState';
|
||||
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
||||
|
||||
export type TabItem = {
|
||||
key: Key;
|
||||
@@ -15,7 +15,7 @@ export type TabItem = {
|
||||
|
||||
type OpenAPITabsContextData = {
|
||||
items: TabItem[];
|
||||
selectedTab: TabItem;
|
||||
selectedTab: TabItem | null;
|
||||
};
|
||||
|
||||
const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null);
|
||||
@@ -35,68 +35,54 @@ export function OpenAPITabs(
|
||||
props: React.PropsWithChildren<TabsProps & { items: TabItem[]; stateKey?: string }>
|
||||
) {
|
||||
const { children, items, stateKey } = props;
|
||||
const [ref, isIntersectionVisible] = useIntersectionObserver({
|
||||
threshold: 0.1,
|
||||
rootMargin: '200px',
|
||||
});
|
||||
const isVisible = stateKey ? isIntersectionVisible : true;
|
||||
const defaultTab = items[0] as TabItem;
|
||||
const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState<TabItem>();
|
||||
const [selectedTabKey, setSelectedTabKey] = useState(() => {
|
||||
if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
|
||||
const tabFromState = syncedTabs.get(stateKey);
|
||||
return tabFromState?.key ?? items[0]?.key;
|
||||
}
|
||||
return items[0]?.key;
|
||||
});
|
||||
const [selectedTab, setSelectedTab] = useState<TabItem>(defaultTab);
|
||||
|
||||
const handleSelectionChange = (key: Key) => {
|
||||
setSelectedTabKey(key);
|
||||
if (stateKey) {
|
||||
const tab = items.find((item) => item.key === key);
|
||||
|
||||
if (!tab) {
|
||||
return;
|
||||
const [tabKey, setTabKey] = useState<Key | null>(() => {
|
||||
if (stateKey && typeof window !== 'undefined') {
|
||||
const store = getOrCreateTabStoreByKey(stateKey);
|
||||
const tabKey = store.getState().tabKey;
|
||||
if (tabKey) {
|
||||
return tabKey;
|
||||
}
|
||||
|
||||
setSyncedTabs((state) => {
|
||||
const newState = new Map(state);
|
||||
newState.set(stateKey, tab);
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return items[0]?.key ?? null;
|
||||
});
|
||||
const selectTab = useEventCallback((key: Key | null) => {
|
||||
if (!key || key === tabKey) {
|
||||
return;
|
||||
}
|
||||
const tab = items.find((item) => item.key === key);
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
setTabKey(key);
|
||||
});
|
||||
const selectedTab = items.find((item) => item.key === tabKey) ?? items[0] ?? null;
|
||||
const cancelDeferRef = useRef<(() => void) | null>(null);
|
||||
useEffect(() => {
|
||||
if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
|
||||
const tabFromState = syncedTabs.get(stateKey);
|
||||
|
||||
if (!items.some((item) => item.key === tabFromState?.key)) {
|
||||
return setSelectedTab(defaultTab);
|
||||
}
|
||||
|
||||
if (tabFromState && tabFromState?.key !== selectedTab?.key) {
|
||||
const tabFromItems = items.find((item) => item.key === tabFromState.key);
|
||||
|
||||
if (!tabFromItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedTab(tabFromItems);
|
||||
}
|
||||
if (!stateKey) {
|
||||
return undefined;
|
||||
}
|
||||
}, [isVisible, stateKey, syncedTabs, selectedTabKey]);
|
||||
|
||||
const store = getOrCreateTabStoreByKey(stateKey);
|
||||
return store.subscribe((state) => {
|
||||
cancelDeferRef.current?.();
|
||||
cancelDeferRef.current = defer(() => selectTab(state.tabKey));
|
||||
});
|
||||
}, [stateKey, selectTab]);
|
||||
useEffect(() => {
|
||||
return () => cancelDeferRef.current?.();
|
||||
}, []);
|
||||
const contextValue = useMemo(() => ({ items, selectedTab }), [items, selectedTab]);
|
||||
|
||||
return (
|
||||
<OpenAPITabsContext.Provider value={contextValue}>
|
||||
<Tabs
|
||||
ref={ref}
|
||||
className="openapi-tabs"
|
||||
onSelectionChange={handleSelectionChange}
|
||||
selectedKey={selectedTab?.key}
|
||||
onSelectionChange={(tabKey) => {
|
||||
selectTab(tabKey);
|
||||
if (stateKey) {
|
||||
const store = getOrCreateTabStoreByKey(stateKey);
|
||||
store.setState({ tabKey });
|
||||
}
|
||||
}}
|
||||
selectedKey={tabKey}
|
||||
>
|
||||
{children}
|
||||
</Tabs>
|
||||
@@ -104,6 +90,11 @@ export function OpenAPITabs(
|
||||
);
|
||||
}
|
||||
|
||||
const defer = (fn: () => void) => {
|
||||
const id = setTimeout(fn, 0);
|
||||
return () => clearTimeout(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* The OpenAPI Tabs list component.
|
||||
* This component should be used as a child of the OpenAPITabs component.
|
||||
@@ -116,14 +107,14 @@ export function OpenAPITabsList() {
|
||||
<TabList className="openapi-tabs-list">
|
||||
{items.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
id={tab.key}
|
||||
style={({ isFocusVisible }) => ({
|
||||
outline: isFocusVisible
|
||||
? '2px solid rgb(var(--primary-color-500)/0.4)'
|
||||
: 'none',
|
||||
})}
|
||||
className="openapi-tabs-tab"
|
||||
key={`Tab-${tab.key}`}
|
||||
id={tab.key}
|
||||
>
|
||||
{tab.label}
|
||||
</Tab>
|
||||
@@ -144,12 +135,10 @@ export function OpenAPITabsPanels() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = selectedTab.key.toString();
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
key={`TabPanel-${selectedTab.key}`}
|
||||
id={selectedTab.key.toString()}
|
||||
className="openapi-tabs-panel"
|
||||
>
|
||||
<TabPanel key={key} id={key} className="openapi-tabs-panel">
|
||||
{selectedTab.body}
|
||||
{selectedTab.description ? (
|
||||
<Markdown source={selectedTab.description} className="openapi-tabs-footer" />
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import clsx from 'clsx';
|
||||
import { type ComponentPropsWithoutRef, forwardRef } from 'react';
|
||||
|
||||
export function Section(props: ComponentPropsWithoutRef<'div'>) {
|
||||
return <div {...props} className={clsx('openapi-section', props.className)} />;
|
||||
}
|
||||
|
||||
export function SectionHeader(props: ComponentPropsWithoutRef<'div'>) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={clsx(
|
||||
'openapi-section-header',
|
||||
props.className && `${props.className}-header`
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SectionHeaderContent(props: ComponentPropsWithoutRef<'div'>) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={clsx(
|
||||
'openapi-section-header-content',
|
||||
props.className && `${props.className}-header-content`
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const SectionBody = forwardRef(function SectionBody(
|
||||
props: ComponentPropsWithoutRef<'div'>,
|
||||
ref: React.ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={clsx('openapi-section-body', props.className && `${props.className}-body`)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export function StaticSection(props: {
|
||||
className: string;
|
||||
header: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { className, header, children } = props;
|
||||
return (
|
||||
<Section className={className}>
|
||||
<SectionHeader className={className}>
|
||||
<SectionHeaderContent className={className}>{header}</SectionHeaderContent>
|
||||
</SectionHeader>
|
||||
<SectionBody className={className}>{children}</SectionBody>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { createStore } from 'zustand';
|
||||
|
||||
interface SyncedTabsState<T> {
|
||||
tabs: Map<string, T>;
|
||||
setTabs: (updater: (tabs: Map<string, T>) => Map<string, T>) => void;
|
||||
}
|
||||
type Key = string | number;
|
||||
|
||||
const useSyncedTabsStore = create<SyncedTabsState<any>>()((set) => ({
|
||||
tabs: new Map<string, any>(),
|
||||
setTabs: (updater) =>
|
||||
set((state) => ({
|
||||
tabs: updater(new Map(state.tabs)), // Ensure a new Map is created for reactivity
|
||||
})),
|
||||
}));
|
||||
type TabState = {
|
||||
tabKey: Key | null;
|
||||
};
|
||||
|
||||
// Selector for better performance - only re-renders when tabs change
|
||||
export function useSyncedTabsGlobalState<T>() {
|
||||
const tabs = useSyncedTabsStore((state) => state.tabs as Map<string, T>);
|
||||
const setTabs = useSyncedTabsStore((state) => state.setTabs as SyncedTabsState<T>['setTabs']);
|
||||
return [tabs, setTabs] as const;
|
||||
}
|
||||
type TabActions = { setTabKey: (tab: Key | null) => void };
|
||||
|
||||
type TabStore = TabState & TabActions;
|
||||
|
||||
const createTabStore = (initialTab?: Key) => {
|
||||
return createStore<TabStore>()((set) => ({
|
||||
tabKey: initialTab ?? null,
|
||||
setTabKey: (tabKey) => {
|
||||
set(() => ({ tabKey }));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const defaultTabStores = new Map<string, ReturnType<typeof createTabStore>>();
|
||||
|
||||
const createTabStoreFactory = (stores: typeof defaultTabStores) => {
|
||||
return (storeKey: string, initialKey?: Key) => {
|
||||
if (!stores.has(storeKey)) {
|
||||
stores.set(storeKey, createTabStore(initialKey));
|
||||
}
|
||||
return stores.get(storeKey)!;
|
||||
};
|
||||
};
|
||||
|
||||
export const getOrCreateTabStoreByKey = createTabStoreFactory(defaultTabStores);
|
||||
|
||||
Reference in New Issue
Block a user