Compare commits

..

14 Commits

Author SHA1 Message Date
Samy Pessé 13514bf6fa Version Packages (#2907)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-04 13:42:32 +01:00
Samy Pessé 05ffd0e321 Proper data cache tagging for computed content (#2915) 2025-03-04 12:26:09 +01:00
Samy Pessé 701eaad92a Fix use of server actions with proxied domains by using middleware (#2913) 2025-03-04 10:00:08 +01:00
Greg Bergé c66ef5dd10 Fix OpenAPI tabs selection (#2914) 2025-03-03 21:10:24 +00:00
Greg Bergé f774f9b3a0 Fix all tabs behaviour in OpenAPI blocks (#2912) 2025-03-03 17:11:54 +00:00
Samy Pessé 29dc7ccb1a Fix next.js configuration for serverActions 2025-03-03 17:05:46 +01:00
Samy Pessé 80237c34f9 Allow all origins (including sub-domains) for server actions 2025-03-03 16:54:16 +01:00
Samy Pessé 979233c5c0 Allow all origins for serverActions in v2 2025-03-03 16:21:05 +01:00
Samy Pessé 2f03e6acee Define GITBOOK_URL as serverActions.allowedOrigins (#2911) 2025-03-03 15:24:31 +01:00
Greg Bergé c492f09a19 Fix outline not visible on OpenAPI blocks (#2910) 2025-03-03 14:56:19 +01:00
Greg Bergé 2c3af5e73c Improve scrolling with lot of highlighted blocks (#2909) 2025-03-03 13:47:30 +00:00
Samy Pessé 6597f8508d V2: search and ask AI (#2889) 2025-03-03 11:53:32 +01:00
Nolann B. 8beb5d6bcf Add input elements to ContentKit (#2882) 2025-02-28 17:50:20 +00:00
Greg Bergé 53f5dbea26 Fix typing of shouldIgnoreEntity (#2906) 2025-02-28 17:38:58 +00:00
68 changed files with 1428 additions and 786 deletions
+60 -15
View File
@@ -22,6 +22,17 @@
"wrangler": "^3.109.2", "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": { "packages/colors": {
"name": "@gitbook/colors", "name": "@gitbook/colors",
"version": "0.2.0", "version": "0.2.0",
@@ -38,10 +49,11 @@
}, },
"packages/gitbook": { "packages/gitbook": {
"name": "gitbook", "name": "gitbook",
"version": "0.6.3", "version": "0.6.4",
"dependencies": { "dependencies": {
"@gitbook/api": "0.96.1", "@gitbook/api": "0.96.1",
"@gitbook/cache-do": "workspace:*", "@gitbook/cache-do": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*", "@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*", "@gitbook/icons": "workspace:*",
@@ -57,6 +69,7 @@
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@upstash/redis": "^1.27.1", "@upstash/redis": "^1.27.1",
"ai": "^4.1.46",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"assert-never": "^1.2.1", "assert-never": "^1.2.1",
"bun-types": "^1.1.20", "bun-types": "^1.1.20",
@@ -118,11 +131,12 @@
}, },
"packages/gitbook-v2": { "packages/gitbook-v2": {
"name": "gitbook-v2", "name": "gitbook-v2",
"version": "0.1.0", "version": "0.1.1",
"dependencies": { "dependencies": {
"@gitbook/api": "0.96.1", "@gitbook/api": "0.96.1",
"@gitbook/cache-tags": "workspace:*",
"@sindresorhus/fnv1a": "^3.1.0", "@sindresorhus/fnv1a": "^3.1.0",
"next": "canary", "next": "^15.2.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"server-only": "^0.0.1", "server-only": "^0.0.1",
@@ -156,7 +170,7 @@
}, },
"packages/openapi-parser": { "packages/openapi-parser": {
"name": "@gitbook/openapi-parser", "name": "@gitbook/openapi-parser",
"version": "2.0.0", "version": "2.0.1",
"dependencies": { "dependencies": {
"@scalar/openapi-parser": "^0.10.4", "@scalar/openapi-parser": "^0.10.4",
"@scalar/openapi-types": "^0.1.6", "@scalar/openapi-types": "^0.1.6",
@@ -181,6 +195,7 @@
"version": "0.6.0", "version": "0.6.0",
"dependencies": { "dependencies": {
"@gitbook/api": "0.96.1", "@gitbook/api": "0.96.1",
"@gitbook/icons": "workspace:*",
"assert-never": "^1.2.1", "assert-never": "^1.2.1",
"classnames": "^2.5.1", "classnames": "^2.5.1",
}, },
@@ -210,7 +225,7 @@
}, },
"packages/react-openapi": { "packages/react-openapi": {
"name": "@gitbook/react-openapi", "name": "@gitbook/react-openapi",
"version": "1.0.3", "version": "1.0.4",
"dependencies": { "dependencies": {
"@gitbook/openapi-parser": "workspace:*", "@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "1.0.87", "@scalar/api-client-react": "1.0.87",
@@ -242,6 +257,14 @@
"react-dom": "18.3.1", "react-dom": "18.3.1",
}, },
"packages": { "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=="], "@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=="], "@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-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/colors": ["@gitbook/colors@workspace:packages/colors"],
"@gitbook/emoji-codepoints": ["@gitbook/emoji-codepoints@workspace:packages/emoji-codepoints"], "@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/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": ["@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=="], "@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=="], "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": ["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=="], "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": ["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=="], "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], "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-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-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=="], "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=="], "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=="], "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], "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=="], "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": ["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=="], "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=="], "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=="], "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
"tailwind-merge": ["tailwind-merge@2.5.5", "", {}, "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA=="], "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=="], "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=="], "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=="], "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": ["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=="], "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=="], "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=="], "@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=="], "@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=="], "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=="], "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=="], "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=="], "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=="],
+1
View File
@@ -0,0 +1 @@
dist/
+7
View File
@@ -0,0 +1,7 @@
# @gitbook/cache-tags
## 0.1.0
### Minor Changes
- 05ffd0e: Initial version of the package
+3
View File
@@ -0,0 +1,3 @@
# `@gitbook/cache-tags`
Utility to generate cache tags for GitBook Open.
+25
View File
@@ -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"]
}
+175
View File
@@ -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;
}
+24
View File
@@ -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"]
}
+8
View File
@@ -1,5 +1,13 @@
# gitbook-v2 # 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 ## 0.1.1
### Patch Changes ### Patch Changes
+3 -2
View File
@@ -1,12 +1,13 @@
{ {
"name": "gitbook-v2", "name": "gitbook-v2",
"version": "0.1.1", "version": "0.1.2",
"private": true, "private": true,
"dependencies": { "dependencies": {
"next": "canary", "next": "^15.2.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"@gitbook/api": "0.96.1", "@gitbook/api": "0.96.1",
"@gitbook/cache-tags": "workspace:*",
"@sindresorhus/fnv1a": "^3.1.0", "@sindresorhus/fnv1a": "^3.1.0",
"server-only": "^0.0.1" "server-only": "^0.0.1"
}, },
@@ -6,7 +6,7 @@ import {
} from '@/components/SiteLayout'; } from '@/components/SiteLayout';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils'; import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
import { getThemeFromMiddleware } from '@v2/lib/middleware'; import { getThemeFromMiddleware, getVisitorAuthTokenFromMiddleware } from '@v2/lib/middleware';
interface SiteDynamicLayoutProps { interface SiteDynamicLayoutProps {
params: Promise<RouteLayoutParams>; params: Promise<RouteLayoutParams>;
@@ -18,6 +18,7 @@ export default async function SiteDynamicLayout({
}: React.PropsWithChildren<SiteDynamicLayoutProps>) { }: React.PropsWithChildren<SiteDynamicLayoutProps>) {
const context = await getDynamicSiteContext(await params); const context = await getDynamicSiteContext(await params);
const forcedTheme = await getThemeFromMiddleware(); const forcedTheme = await getThemeFromMiddleware();
const visitorAuthToken = await getVisitorAuthTokenFromMiddleware();
return ( return (
<CustomizationRootLayout customization={context.customization}> <CustomizationRootLayout customization={context.customization}>
@@ -25,6 +26,7 @@ export default async function SiteDynamicLayout({
context={context} context={context}
forcedTheme={forcedTheme} forcedTheme={forcedTheme}
withTracking={!GITBOOK_DISABLE_TRACKING} withTracking={!GITBOOK_DISABLE_TRACKING}
visitorAuthToken={visitorAuthToken}
> >
{children} {children}
</SiteLayout> </SiteLayout>
@@ -3,8 +3,8 @@ import {
generateSitePageMetadata, generateSitePageMetadata,
generateSitePageViewport, generateSitePageViewport,
} from '@/components/SitePage'; } from '@/components/SitePage';
import { getCacheTag } from '@gitbook/cache-tags';
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils'; import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils';
import { getSiteCacheTag } from '@v2/lib/cache';
import type { Metadata, Viewport } from 'next'; import type { Metadata, Viewport } from 'next';
import { unstable_cacheTag as cacheTag } from 'next/cache'; 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 context = await getStaticSiteContext(params);
const pathname = getPagePathFromParams(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} />; return <SitePage context={context} pageParams={{ pathname }} redirectOnFallback={true} />;
} }
@@ -4,8 +4,8 @@ import {
generateSiteLayoutMetadata, generateSiteLayoutMetadata,
generateSiteLayoutViewport, generateSiteLayoutViewport,
} from '@/components/SiteLayout'; } from '@/components/SiteLayout';
import { getCacheTag } from '@gitbook/cache-tags';
import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils'; import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils';
import { getSiteCacheTag } from '@v2/lib/cache';
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
import { unstable_cacheTag as cacheTag } from 'next/cache'; import { unstable_cacheTag as cacheTag } from 'next/cache';
@@ -21,11 +21,20 @@ export default async function SiteStaticLayout({
const context = await getStaticSiteContext(await params); const context = await getStaticSiteContext(await params);
cacheTag(getSiteCacheTag(context.site.id)); cacheTag(
getCacheTag({
tag: 'site',
site: context.site.id,
})
);
return ( return (
<CustomizationRootLayout customization={context.customization}> <CustomizationRootLayout customization={context.customization}>
<SiteLayout context={context} withTracking={!GITBOOK_DISABLE_TRACKING}> <SiteLayout
context={context}
withTracking={!GITBOOK_DISABLE_TRACKING}
visitorAuthToken={null}
>
{children} {children}
</SiteLayout> </SiteLayout>
</CustomizationRootLayout> </CustomizationRootLayout>
+22 -85
View File
@@ -1,9 +1,9 @@
import { fetchSiteContextByURL } from '@v2/lib/context'; import {
import { createDataFetcher } from '@v2/lib/data'; fetchSiteContextByURL,
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from '@v2/lib/env'; fetchSiteContextByURLLookup,
import { createImageResizer } from '@v2/lib/images'; getBaseContext,
import { createLinker } from '@v2/lib/links'; } from '@v2/lib/context';
import { headers } from 'next/headers'; import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
export type RouteParamMode = 'url-host' | 'url'; export type RouteParamMode = 'url-host' | 'url';
@@ -21,29 +21,19 @@ export type RouteParams = RouteLayoutParams & {
/** /**
* Get the static context when rendering statically a site. * Get the static context when rendering statically a site.
*/ */
export async function getStaticSiteContext(params: RouteLayoutParams) { export function getStaticSiteContext(params: RouteLayoutParams) {
const url = getSiteURLFromParams(params); const siteURL = getSiteURLFromParams(params);
return fetchSiteContextByURL(
const dataFetcher = createDataFetcher(); getBaseContext({
const { linker, host } = createLinkerFromParams(params); siteURL,
const context = await fetchSiteContextByURL( urlMode: getModeFromParams(params.mode),
}),
{ {
dataFetcher, url: siteURL.toString(),
linker,
},
{
url: url.toString(),
visitorAuthToken: null, visitorAuthToken: null,
redirectOnError: false, 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. * The context will depend on the request.
*/ */
export async function getDynamicSiteContext(params: RouteLayoutParams) { export async function getDynamicSiteContext(params: RouteLayoutParams) {
const url = getSiteURLFromParams(params); const siteURL = getSiteURLFromParams(params);
const headersSet = await headers(); const siteURLData = await getSiteURLDataFromMiddleware();
const dataFetcher = createDataFetcher({ return fetchSiteContextByURLLookup(
apiToken: headersSet.get('x-gitbook-token') ?? GITBOOK_API_TOKEN, getBaseContext({
apiEndpoint: headersSet.get('x-gitbook-api') ?? GITBOOK_API_URL, siteURL,
}); urlMode: getModeFromParams(params.mode),
}),
const { linker, host } = createLinkerFromParams(params); siteURLData
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,
}
); );
context.imageResizer = createImageResizer({
host,
linker: context.linker,
});
return context;
} }
/** /**
@@ -91,39 +61,6 @@ export function getPagePathFromParams(params: RouteParams) {
return decoded; 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) { function getSiteURLFromParams(params: RouteLayoutParams) {
const decoded = decodeURIComponent(params.siteURL); const decoded = decodeURIComponent(params.siteURL);
const url = new URL(`https://${decoded}`); const url = new URL(`https://${decoded}`);
-34
View File
@@ -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}`;
}
+69 -18
View File
@@ -1,6 +1,7 @@
import { getSiteStructureSections } from '@/lib/sites'; import { getSiteStructureSections } from '@/lib/sites';
import type { import type {
ChangeRequest, ChangeRequest,
PublishedSiteContentLookup,
RevisionPage, RevisionPage,
RevisionPageDocument, RevisionPageDocument,
Site, Site,
@@ -15,8 +16,9 @@ import type {
import { type GitBookDataFetcher, createDataFetcher } from '@v2/lib/data'; import { type GitBookDataFetcher, createDataFetcher } from '@v2/lib/data';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { assert } from 'ts-essentials'; import { assert } from 'ts-essentials';
import type { ImageResizer } from './images'; import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from './env';
import { type GitBookSpaceLinker, appendBasePathToLinker } from './links'; import { type ImageResizer, createImageResizer } from './images';
import { type GitBookSpaceLinker, createLinker } from './links';
/** /**
* Generic context when rendering content. * Generic context when rendering content.
@@ -90,9 +92,6 @@ export type GitBookSiteContext = GitBookSpaceContext & {
/** Scripts to load for the site. */ /** Scripts to load for the site. */
scripts: SiteIntegrationScript[]; scripts: SiteIntegrationScript[];
/** Visitor token used to fetch the site */
visitorAuthToken: string | null;
}; };
/** /**
@@ -102,6 +101,58 @@ export type GitBookPageContext = (GitBookSpaceContext | GitBookSiteContext) & {
page: RevisionPageDocument; 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. * 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, 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) { if ('redirect' in data) {
redirect(data.redirect); redirect(data.redirect);
} }
const context = await fetchSiteContextByIds( return await fetchSiteContextByIds(
{ {
...baseContext, ...baseContext,
dataFetcher: createDataFetcher({ dataFetcher: dataFetcher.withToken({
apiEndpoint: dataFetcher.apiEndpoint,
apiToken: data.apiToken, apiToken: data.apiToken,
}), }),
}, },
@@ -141,16 +202,8 @@ export async function fetchSiteContextByURL(
shareKey: data.shareKey, shareKey: data.shareKey,
changeRequest: data.changeRequest, changeRequest: data.changeRequest,
revision: data.revision, 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; shareKey: string | undefined;
changeRequest: string | undefined; changeRequest: string | undefined;
revision: string | undefined; revision: string | undefined;
visitorAuthToken: string | null;
} }
): Promise<GitBookSiteContext> { ): Promise<GitBookSiteContext> {
const { dataFetcher } = baseContext; const { dataFetcher } = baseContext;
@@ -233,7 +285,6 @@ export async function fetchSiteContextByIds(
structure: siteStructure, structure: siteStructure,
sections, sections,
scripts, scripts,
visitorAuthToken: ids.visitorAuthToken,
}; };
} }
+88 -15
View File
@@ -1,13 +1,7 @@
import { type ComputedContentSource, GitBookAPI } from '@gitbook/api'; 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 { 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 { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
import {
getChangeRequestCacheTag,
getHostnameCacheTag,
getOpenAPISpecCacheTag,
getSiteCacheTag,
getSpaceCacheTag,
} from '../cache';
import type { GitBookDataFetcher } from './types'; import type { GitBookDataFetcher } from './types';
interface DataFetcherInput { interface DataFetcherInput {
@@ -35,6 +29,17 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo
return { return {
apiEndpoint: input.apiEndpoint, apiEndpoint: input.apiEndpoint,
async api() {
return getAPI(input);
},
withToken({ apiToken }) {
return createDataFetcher({
...input,
apiToken,
});
},
// //
// API that are tied to the token // API that are tied to the token
// //
@@ -114,6 +119,7 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo
}, },
getComputedDocument(params) { getComputedDocument(params) {
return getComputedDocument(input, { return getComputedDocument(input, {
organizationId: params.organizationId,
spaceId: params.spaceId, spaceId: params.spaceId,
source: params.source, source: params.source,
}); });
@@ -124,6 +130,9 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo
spaceId: params.spaceId, spaceId: params.spaceId,
}); });
}, },
searchSiteContent(params) {
return searchSiteContent(input, params);
},
// //
// API that are not tied to the token // API that are not tied to the token
@@ -169,7 +178,12 @@ async function getSpace(
'use cache'; 'use cache';
cacheLife('days'); cacheLife('days');
cacheTag(getSpaceCacheTag(params.spaceId)); cacheTag(
getCacheTag({
tag: 'space',
space: params.spaceId,
})
);
const res = await getAPI(input).spaces.getSpaceById(params.spaceId, { const res = await getAPI(input).spaces.getSpaceById(params.spaceId, {
shareKey: params.shareKey, shareKey: params.shareKey,
@@ -193,7 +207,13 @@ async function getChangeRequest(
params.spaceId, params.spaceId,
params.changeRequestId params.changeRequestId
); );
cacheTag(getChangeRequestCacheTag(params.spaceId, res.data.id)); cacheTag(
getCacheTag({
tag: 'change-request',
space: params.spaceId,
changeRequest: res.data.id,
})
);
return res.data; return res.data;
} catch (error) { } catch (error) {
if (checkHasErrorCode(error, 404)) { if (checkHasErrorCode(error, 404)) {
@@ -321,14 +341,24 @@ async function getComputedDocument(
input: DataFetcherInput, input: DataFetcherInput,
params: { params: {
spaceId: string; spaceId: string;
organizationId: string;
source: ComputedContentSource; source: ComputedContentSource;
} }
) { ) {
'use cache'; 'use cache';
// TODO: we need to resolve dependencies and pass them in the cache key
cacheLife('days'); cacheLife('days');
cacheTag(
...getComputedContentSourceCacheTags(
{
spaceId: params.spaceId,
organizationId: params.organizationId,
},
params.source
)
);
const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, { const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, {
source: params.source, source: params.source,
}); });
@@ -372,7 +402,13 @@ async function getLatestOpenAPISpecVersionContent(
) { ) {
'use cache'; 'use cache';
cacheTag(getOpenAPISpecCacheTag(params.organizationId, params.slug)); cacheTag(
getCacheTag({
tag: 'openapi',
organization: params.organizationId,
openAPISpec: params.slug,
})
);
cacheLife('days'); cacheLife('days');
try { try {
@@ -403,7 +439,12 @@ async function getPublishedContentByUrl(
const { url, visitorAuthToken, redirectOnError } = params; const { url, visitorAuthToken, redirectOnError } = params;
const hostname = new URL(url).hostname; const hostname = new URL(url).hostname;
cacheTag(getHostnameCacheTag(hostname)); cacheTag(
getCacheTag({
tag: 'url',
hostname,
})
);
cacheLife('days'); cacheLife('days');
const res = await getAPI(input).urls.getPublishedContentByUrl({ const res = await getAPI(input).urls.getPublishedContentByUrl({
@@ -413,7 +454,12 @@ async function getPublishedContentByUrl(
}); });
if ('site' in res.data) { if ('site' in res.data) {
cacheTag(getSiteCacheTag(res.data.site)); cacheTag(
getCacheTag({
tag: 'site',
site: res.data.site,
})
);
} }
return res.data; return res.data;
@@ -429,7 +475,12 @@ async function getPublishedContentSite(
) { ) {
'use cache'; 'use cache';
cacheTag(getSiteCacheTag(params.siteId)); cacheTag(
getCacheTag({
tag: 'site',
site: params.siteId,
})
);
cacheLife('days'); cacheLife('days');
const res = await getAPI(input).orgs.getPublishedContentSite( const res = await getAPI(input).orgs.getPublishedContentSite(
@@ -453,7 +504,12 @@ async function getSiteRedirectBySource(
) { ) {
'use cache'; 'use cache';
cacheTag(getSiteCacheTag(params.siteId)); cacheTag(
getCacheTag({
tag: 'site',
site: params.siteId,
})
);
cacheLife('days'); cacheLife('days');
try { try {
@@ -498,6 +554,23 @@ async function getEmbedByUrl(
return res.data; 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) { function getAPI(input: DataFetcherInput) {
const { apiEndpoint, apiToken } = input; const { apiEndpoint, apiToken } = input;
const api = new GitBookAPI({ const api = new GitBookAPI({
+28
View File
@@ -10,6 +10,18 @@ export interface GitBookDataFetcher {
*/ */
apiEndpoint: string; 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. * Get a user by its ID.
*/ */
@@ -91,6 +103,7 @@ export interface GitBookDataFetcher {
* Get a computed document by its space ID and computed source. * Get a computed document by its space ID and computed source.
*/ */
getComputedDocument(params: { getComputedDocument(params: {
organizationId: string;
spaceId: string; spaceId: string;
source: api.ComputedContentSource; source: api.ComputedContentSource;
}): Promise<api.JSONDocument>; }): Promise<api.JSONDocument>;
@@ -126,4 +139,19 @@ export interface GitBookDataFetcher {
* Get an embed by its URL. * Get an embed by its URL.
*/ */
getEmbedByUrl(params: { url: string; spaceId: string }): Promise<api.Embed>; 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[]>;
} }
+8 -4
View File
@@ -1,4 +1,4 @@
import type { RevisionPageDocument } from '@gitbook/api'; import type { RevisionPageDocument, Space } from '@gitbook/api';
import type { GitBookDataFetcher } from './types'; import type { GitBookDataFetcher } from './types';
/** /**
@@ -6,14 +6,18 @@ import type { GitBookDataFetcher } from './types';
*/ */
export async function getPageDocument( export async function getPageDocument(
dataFetcher: GitBookDataFetcher, dataFetcher: GitBookDataFetcher,
spaceId: string, space: Space,
page: RevisionPageDocument page: RevisionPageDocument
) { ) {
if (page.documentId) { if (page.documentId) {
return dataFetcher.getDocument({ spaceId, documentId: page.documentId }); return dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId });
} }
if (page.computed) { if (page.computed) {
return dataFetcher.getComputedDocument({ spaceId, source: page.computed }); return dataFetcher.getComputedDocument({
organizationId: space.organization,
spaceId: space.id,
source: page.computed,
});
} }
return null; return null;
+84 -1
View File
@@ -1,14 +1,51 @@
import { CustomizationThemeMode } from '@gitbook/api'; import { CustomizationThemeMode, type PublishedSiteContent } from '@gitbook/api';
import { headers } from 'next/headers'; import { headers } from 'next/headers';
export enum MiddlewareHeaders { 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', URLMode = 'x-gitbook-url-mode',
/**
* The theme of the page (light or dark)
*/
Theme = 'x-gitbook-theme', Theme = 'x-gitbook-theme',
/**
* The customization override to apply.
*/
Customization = 'x-gitbook-customization', 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. * 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'> { export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> {
const headersList = await headers(); const headersList = await headers();
@@ -20,8 +57,40 @@ export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> {
return mode as '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). * 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() { export async function getThemeFromMiddleware() {
const headersList = await headers(); const headersList = await headers();
@@ -34,3 +103,17 @@ export async function getThemeFromMiddleware() {
? CustomizationThemeMode.Light ? CustomizationThemeMode.Light
: CustomizationThemeMode.Dark; : 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);
}
+16 -4
View File
@@ -64,26 +64,38 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
return NextResponse.redirect(data.redirect); return NextResponse.redirect(data.redirect);
} }
const routeType = dynamicHeaders ? 'dynamic' : 'static';
const requestHeaders = new Headers(request.headers); const requestHeaders = new Headers(request.headers);
requestHeaders.set(MiddlewareHeaders.RouteType, routeType);
requestHeaders.set(MiddlewareHeaders.URLMode, mode); requestHeaders.set(MiddlewareHeaders.URLMode, mode);
requestHeaders.set(MiddlewareHeaders.SiteURL, `${url.origin}${data.basePath}`);
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(data));
if (dynamicHeaders) { if (dynamicHeaders) {
for (const [key, value] of Object.entries(dynamicHeaders)) { for (const [key, value] of Object.entries(dynamicHeaders)) {
requestHeaders.set(key, value); 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 = [ const route = [
'sites', 'sites',
dynamicHeaders ? 'dynamic' : 'static', routeType,
mode, mode,
encodeURIComponent(url.host + data.basePath), encodeURIComponent(url.host + data.basePath),
encodePathInSiteContent(data.pathname), encodePathInSiteContent(data.pathname),
].join('/'); ].join('/');
console.log('route', route); console.log(`rewriting to ${route}`);
const response = NextResponse.rewrite(new URL(`/${route}`, request.url), { const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
headers: requestHeaders, const response = NextResponse.rewrite(rewrittenURL, {
request: {
headers: requestHeaders,
},
}); });
// Add Content Security Policy header // Add Content Security Policy header
+14
View File
@@ -1,5 +1,19 @@
# gitbook # 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 ## 0.6.4
### Patch Changes ### Patch Changes
-5
View File
@@ -225,11 +225,6 @@ const testCases: TestsCase[] = [
baseUrl: 'https://docs.raydium.io', baseUrl: 'https://docs.raydium.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{
name: 'docs.solv.finance',
baseUrl: 'https://docs.solv.finance',
tests: [{ name: 'Home', url: '/' }],
},
{ {
name: 'docs.fluentbit.io', name: 'docs.fluentbit.io',
baseUrl: 'https://docs.fluentbit.io', baseUrl: 'https://docs.fluentbit.io',
+15
View File
@@ -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); await page.goto(url);
if (testEntry.run) { if (testEntry.run) {
await testEntry.run(page); await testEntry.run(page);
+4 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "gitbook", "name": "gitbook",
"version": "0.6.4", "version": "0.6.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev", "dev": "env-cmd --silent -f ../../.env.local next dev",
@@ -18,6 +18,7 @@
"dependencies": { "dependencies": {
"@gitbook/api": "0.96.1", "@gitbook/api": "0.96.1",
"@gitbook/cache-do": "workspace:*", "@gitbook/cache-do": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*", "@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*", "@gitbook/icons": "workspace:*",
@@ -62,7 +63,8 @@
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2", "tailwind-shades": "^1.1.2",
"url-join": "^5.0.0", "url-join": "^5.0.0",
"usehooks-ts": "^3.1.0" "usehooks-ts": "^3.1.0",
"ai": "^4.1.46"
}, },
"devDependencies": { "devDependencies": {
"@argos-ci/playwright": "^3.10.0", "@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 { Metadata, Viewport } from 'next';
import type React from 'react'; import type React from 'react';
@@ -22,12 +22,14 @@ export default async function ContentLayout(props: { children: React.ReactNode }
const context = await fetchLayoutData(); const context = await fetchLayoutData();
const queryStringTheme = await getThemeFromMiddleware(); const queryStringTheme = await getThemeFromMiddleware();
const visitorAuthToken = await getVisitorAuthTokenFromMiddleware();
return ( return (
<SiteLayout <SiteLayout
context={context} context={context}
forcedTheme={queryStringTheme} forcedTheme={queryStringTheme}
withTracking={await shouldTrackEvents()} withTracking={await shouldTrackEvents()}
visitorAuthToken={visitorAuthToken}
> >
{children} {children}
</SiteLayout> </SiteLayout>
@@ -231,7 +231,7 @@ async function PDFPageDocument(props: {
}) { }) {
const { page, context } = props; const { page, context } = props;
const { space } = context; const { space } = context;
const document = await getPageDocument(context.dataFetcher, space.id, page); const document = await getPageDocument(context.dataFetcher, space, page);
return ( return (
<PrintPage id={getPagePDFContainerId(page)}> <PrintPage id={getPagePDFContainerId(page)}>
@@ -5,6 +5,8 @@ import { headers } from 'next/headers';
import { getV1BaseContext } from '@/lib/v1'; import { getV1BaseContext } from '@/lib/v1';
import { isV2 } from '@/lib/v2';
import { getServerActionBaseContext } from '@v2/lib/server-actions';
import { AdClassicRendering } from './AdClassicRendering'; import { AdClassicRendering } from './AdClassicRendering';
import { AdCoverRendering } from './AdCoverRendering'; import { AdCoverRendering } from './AdCoverRendering';
import { AdPixels } from './AdPixels'; import { AdPixels } from './AdPixels';
@@ -41,7 +43,7 @@ interface FetchPlaceholderAdOptions {
* and properly access user-agent and IP. * and properly access user-agent and IP.
*/ */
export async function renderAd(options: FetchAdOptions) { 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 mode = options.source === 'live' ? options.mode : 'classic';
const result = options.source === 'live' ? await fetchAd(options) : await getPlaceholderAd(); const result = options.source === 'live' ? await fetchAd(options) : await getPlaceholderAd();
@@ -64,9 +66,9 @@ export async function renderAd(options: FetchAdOptions) {
children: ( children: (
<> <>
{mode === 'classic' || !('callToAction' in ad) ? ( {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} {ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
</> </>
@@ -3,8 +3,9 @@
import type { DocumentBlockCode } from '@gitbook/api'; import type { DocumentBlockCode } from '@gitbook/api';
import { useEffect, useRef, useState } from 'react'; 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 type { BlockProps } from '../Block';
import { CodeBlockRenderer } from './CodeBlockRenderer'; import { CodeBlockRenderer } from './CodeBlockRenderer';
import type { HighlightLine, RenderedInline } from './highlight'; import type { HighlightLine, RenderedInline } from './highlight';
@@ -21,6 +22,8 @@ type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> &
export function ClientCodeBlock(props: ClientBlockProps) { export function ClientCodeBlock(props: ClientBlockProps) {
const { block, style, inlines } = props; const { block, style, inlines } = props;
const blockRef = useRef<HTMLDivElement>(null); const blockRef = useRef<HTMLDivElement>(null);
const processedRef = useRef(false);
const isInViewportRef = useRef<boolean | null>(null);
const [lines, setLines] = useState<HighlightLine[]>(() => plainHighlight(block, [])); const [lines, setLines] = useState<HighlightLine[]>(() => plainHighlight(block, []));
// Preload the highlighter when the block is mounted. // Preload the highlighter when the block is mounted.
@@ -28,39 +31,44 @@ export function ClientCodeBlock(props: ClientBlockProps) {
import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block)); import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block));
}, [block]); }, [block]);
// Check if the block is in the viewport to start highlighting it. const runHighlight = useEventCallback(() => {
const hasBeenInViewport = useHasBeenInViewport(blockRef, { if (processedRef.current) {
rootMargin: '200px', return;
});
// 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;
};
} }
}, [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} />; return <CodeBlockRenderer ref={blockRef} block={block} style={style} lines={lines} />;
} }
@@ -3,8 +3,13 @@
@apply flex gap-4; @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 { .contentkit-hstack {
@apply flex-row gap-4 items-center; @apply flex-row gap-3 items-center;
} }
.contentkit-hstack.contentkit-stack-align-center { .contentkit-hstack.contentkit-stack-align-center {
@apply justify-center; @apply justify-center;
@@ -88,17 +93,23 @@
/** Buttons */ /** Buttons */
.contentkit-button { .contentkit-button {
@apply flex gap-2 items-center justify-center px-2 py-1 rounded @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;
border border-tint hover:border-tint-hover }
text-tint hover:text-tint-strong;
.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 { .contentkit-button .contentkit-icon {
@apply w-4 h-4; @apply w-4 h-4;
} }
.contentkit-button .contentkit-button-label { .contentkit-button-loading {
@apply text-sm font-medium text-tint-strong; @apply size-5 flex mx-auto animate-spin;
} }
/** Modals */ /** Modals */
@@ -143,7 +154,21 @@
/** Text input */ /** Text input */
.contentkit-textinput { .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 */ /** Dividers */
@@ -92,6 +92,7 @@ const getBlockHeight = memoize((block: DocumentBlock | undefined): number => {
case 'images': case 'images':
case 'image': case 'image':
case 'swagger': case 'swagger':
case 'openapi-operation':
return 768 / (16 / 9); return 768 / (16 / 9);
case 'file': case 'file':
case 'expandable': case 'expandable':
@@ -1,14 +1,13 @@
'use client'; 'use client';
import { Icon } from '@gitbook/icons'; import { Icon } from '@gitbook/icons';
import { readStreamableValue } from 'ai/rsc';
import React from 'react'; import React from 'react';
import { Loading } from '@/components/primitives'; import { Loading } from '@/components/primitives';
import { useLanguage } from '@/intl/client'; import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate'; import { t } from '@/intl/translate';
import type { TranslationLanguage } from '@/intl/translations'; import type { TranslationLanguage } from '@/intl/translations';
import { iterateStreamResponse } from '@/lib/actions';
import type { SiteContentPointer } from '@/lib/api';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { useTrackEvent } from '../Insights'; import { useTrackEvent } from '../Insights';
@@ -32,14 +31,13 @@ export type SearchAskState =
/** /**
* Fetch and render the answers to a question. * Fetch and render the answers to a question.
*/ */
export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: string }) { export function SearchAskAnswer(props: { query: string }) {
const { pointer, query } = props; const { query } = props;
const language = useLanguage(); const language = useLanguage();
const trackEvent = useTrackEvent(); const trackEvent = useTrackEvent();
const [, setSearchState] = useSearch(); const [, setSearchState] = useSearch();
const [askState, setAskState] = useSearchAskContext(); const [askState, setAskState] = useSearchAskContext();
const { organizationId, siteId, siteSpaceId } = pointer;
React.useEffect(() => { React.useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -52,19 +50,19 @@ export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: str
query, 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 // 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. // we ensure that the query is up-to-date before starting the stream.
setSearchState((prev) => (prev ? { ...prev, query, ask: true } : null)); 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) { if (cancelled) {
return; return;
} }
setAskState({ type: 'answer', answer: chunk }); if (chunk) {
setAskState({ type: 'answer', answer: chunk });
}
} }
})().catch(() => { })().catch(() => {
if (cancelled) { if (cancelled) {
@@ -81,7 +79,7 @@ export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: str
cancelled = true; cancelled = true;
} }
}; };
}, [organizationId, siteId, siteSpaceId, query, setAskState, setSearchState, trackEvent]); }, [query, setAskState, setSearchState, trackEvent]);
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
@@ -7,7 +7,6 @@ import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { tString, useLanguage } from '@/intl/client'; import { tString, useLanguage } from '@/intl/client';
import type { SiteContentPointer } from '@/lib/api';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { LoadingPane } from '../primitives/LoadingPane'; import { LoadingPane } from '../primitives/LoadingPane';
@@ -18,11 +17,9 @@ import { SearchScopeToggle } from './SearchScopeToggle';
import { type SearchState, type UpdateSearchState, useSearch } from './useSearch'; import { type SearchState, type UpdateSearchState, useSearch } from './useSearch';
interface SearchModalProps { interface SearchModalProps {
revisionId: string;
spaceTitle: string; spaceTitle: string;
isMultiVariants: boolean; isMultiVariants: boolean;
withAsk: boolean; withAsk: boolean;
pointer: SiteContentPointer;
} }
/** /**
@@ -144,16 +141,7 @@ function SearchModalBody(
onClose: (to?: string) => void; onClose: (to?: string) => void;
} }
) { ) {
const { const { spaceTitle, withAsk, isMultiVariants, state, setSearchState, onClose } = props;
pointer,
revisionId,
spaceTitle,
withAsk,
isMultiVariants,
state,
setSearchState,
onClose,
} = props;
const language = useLanguage(); const language = useLanguage();
const resultsRef = React.useRef<SearchResultsRef>(null); const resultsRef = React.useRef<SearchResultsRef>(null);
@@ -299,17 +287,13 @@ function SearchModalBody(
{!state.ask || !withAsk ? ( {!state.ask || !withAsk ? (
<SearchResults <SearchResults
ref={resultsRef} ref={resultsRef}
pointer={pointer}
revisionId={revisionId}
global={isMultiVariants && state.global} global={isMultiVariants && state.global}
query={state.query} query={state.query}
withAsk={withAsk} withAsk={withAsk}
onSwitchToAsk={onSwitchToAsk} onSwitchToAsk={onSwitchToAsk}
/> />
) : null} ) : null}
{state.query && state.ask && withAsk ? ( {state.query && state.ask && withAsk ? <SearchAskAnswer query={state.query} /> : null}
<SearchAskAnswer pointer={pointer} query={state.query} />
) : null}
</motion.div> </motion.div>
); );
} }
@@ -1,12 +1,11 @@
'use client'; 'use client';
import { captureException } from '@sentry/nextjs'; import { captureException } from '@sentry/nextjs';
import { readStreamableValue } from 'ai/rsc';
import assertNever from 'assert-never'; import assertNever from 'assert-never';
import React from 'react'; import React from 'react';
import { t, useLanguage } from '@/intl/client'; import { t, useLanguage } from '@/intl/client';
import { iterateStreamResponse } from '@/lib/actions';
import type { SiteContentPointer } from '@/lib/api';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { useTrackEvent } from '../Insights'; import { useTrackEvent } from '../Insights';
@@ -48,15 +47,13 @@ export const SearchResults = React.forwardRef(function SearchResults(
props: { props: {
children?: React.ReactNode; children?: React.ReactNode;
query: string; query: string;
revisionId: string;
global: boolean; global: boolean;
withAsk: boolean; withAsk: boolean;
pointer: SiteContentPointer;
onSwitchToAsk: () => void; onSwitchToAsk: () => void;
}, },
ref: React.Ref<SearchResultsRef> ref: React.Ref<SearchResultsRef>
) { ) {
const { children, query, pointer, revisionId, withAsk, global, onSwitchToAsk } = props; const { children, query, withAsk, global, onSwitchToAsk } = props;
const language = useLanguage(); const language = useLanguage();
const trackEvent = useTrackEvent(); const trackEvent = useTrackEvent();
@@ -93,10 +90,13 @@ export const SearchResults = React.forwardRef(function SearchResults(
return; return;
} }
const response = streamRecommendedQuestions(pointer.organizationId, pointer.siteId); const response = await streamRecommendedQuestions();
const stream = iterateStreamResponse(response); for await (const entry of readStreamableValue(response.stream)) {
if (!entry) {
continue;
}
for await (const { question } of stream) { const { question } = entry;
if (questions.has(question)) { if (questions.has(question)) {
continue; continue;
} }
@@ -124,8 +124,8 @@ export const SearchResults = React.forwardRef(function SearchResults(
let cancelled = false; let cancelled = false;
const timeout = setTimeout(async () => { const timeout = setTimeout(async () => {
const results = await (global const results = await (global
? searchAllSiteContent(query, pointer) ? searchAllSiteContent(query)
: searchSiteSpaceContent(query, pointer, revisionId)); : searchSiteSpaceContent(query));
if (cancelled) { if (cancelled) {
return; return;
@@ -154,7 +154,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
cancelled = true; cancelled = true;
clearTimeout(timeout); clearTimeout(timeout);
}; };
}, [query, global, pointer, revisionId, withAsk, trackEvent]); }, [query, global, withAsk, trackEvent]);
const results: ResultType[] = React.useMemo(() => { const results: ResultType[] = React.useMemo(() => {
if (!withAsk) { if (!withAsk) {
@@ -3,22 +3,24 @@
import type { import type {
RevisionPage, RevisionPage,
SearchAIAnswer, SearchAIAnswer,
SearchAIRecommendedQuestionStream,
SearchPageResult, SearchPageResult,
SiteSpace, SiteSpace,
Space, Space,
} from '@gitbook/api'; } 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 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 { getAbsoluteHref } from '@/lib/links';
import { resolvePageId } from '@/lib/pages'; import { resolvePageId } from '@/lib/pages';
import { findSiteSpaceById } from '@/lib/sites'; import { findSiteSpaceById } from '@/lib/sites';
import { filterOutNullable } from '@/lib/typescript'; 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'; import { DocumentView } from '../DocumentView';
export type OrderedComputedResult = ComputedPageResult | ComputedSectionResult; export type OrderedComputedResult = ComputedPageResult | ComputedSectionResult;
@@ -54,70 +56,13 @@ export interface AskAnswerResult {
sources: AskAnswerSource[]; 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. * Server action to search content in the entire site.
*/ */
export async function searchAllSiteContent( export async function searchAllSiteContent(query: string): Promise<OrderedComputedResult[]> {
query: string, const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
pointer: api.SiteContentPointer
): Promise<OrderedComputedResult[]> { return await searchSiteContent(context, {
return await searchSiteContent({
pointer,
query, query,
scope: { mode: 'all' }, scope: { mode: 'all' },
}); });
@@ -126,118 +71,191 @@ export async function searchAllSiteContent(
/** /**
* Server action to search content in a space. * Server action to search content in a space.
*/ */
export async function searchSiteSpaceContent( export async function searchSiteSpaceContent(query: string): Promise<OrderedComputedResult[]> {
query: string, const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
pointer: api.SiteContentPointer, const siteURLData = await getSiteURLDataFromMiddleware();
revisionId: string
): Promise<OrderedComputedResult[]> {
const siteSpaceId = pointer.siteSpaceId;
assert(siteSpaceId, 'Expected siteSpaceId for searchSiteSpaceContent');
return await searchSiteContent({ return await searchSiteContent(context, {
pointer,
query, query,
// If we have a siteSectionId that means its a sections site use `current` mode // 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 // which searches in the current space + all default spaces of sections
scope: pointer.siteSectionId scope: siteURLData.siteSection
? { mode: 'current', siteSpaceId } ? { mode: 'current', siteSpaceId: siteURLData.siteSpace }
: { mode: 'specific', siteSpaceIds: [siteSpaceId] }, : { mode: 'specific', siteSpaceIds: [siteURLData.siteSpace] },
// We want to break cache for this specific space if the revisionId is different so use it as a cache busting key
cacheBust: revisionId,
}); });
} }
/** /**
* Server action to ask a question in a space. * Server action to ask a question in a space.
*/ */
export const streamAskQuestion = streamResponse(async function* ({ export async function streamAskQuestion({
pointer,
question, question,
}: { }: {
pointer: api.SiteContentPointer;
question: string; question: string;
}) { }) {
const { organizationId, siteId, siteSpaceId } = pointer; const responseStream = createStreamableValue<AskAnswerResult | undefined>();
const [apiCtx, context] = await Promise.all([api.api(), fetchV1ContextForSitePointer(pointer)]);
const stream = apiCtx.client.orgs.streamAskInSite( (async () => {
organizationId, const context = await fetchServerActionSiteContext(
siteId, isV2() ? await getServerActionBaseContext() : await getV1BaseContext()
{ );
question,
context: siteSpaceId
? {
siteSpaceId,
}
: undefined,
scope: {
mode: 'default',
// Include the current site space regardless. const apiClient = await context.dataFetcher.api();
includedSiteSpaces: siteSpaceId ? [siteSpaceId] : undefined,
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[]>>(); const spacePromises = new Map<string, Promise<RevisionPage[]>>();
for await (const chunk of stream) { for await (const chunk of stream) {
const answer = chunk.answer; const answer = chunk.answer;
// Register the space of each page source into the promise queue. // Register the space of each page source into the promise queue.
const spaces = answer.sources const spaces = answer.sources
.map((source) => { .map((source) => {
if (source.type !== 'page') { if (source.type !== 'page') {
return null; return null;
} }
if (!spacePromises.has(source.space)) { if (!spacePromises.has(source.space)) {
spacePromises.set( spacePromises.set(
source.space, source.space,
context.dataFetcher.getRevisionPages({ context.dataFetcher.getRevisionPages({
spaceId: source.space, spaceId: source.space,
revisionId: source.revision, revisionId: source.revision,
metadata: false, metadata: false,
}) })
); );
} }
return source.space; return source.space;
}) })
.filter(filterOutNullable); .filter(filterOutNullable);
// Get the pages for all spaces referenced by this answer. // Get the pages for all spaces referenced by this answer.
const pages = await Promise.all( const pages = await Promise.all(
spaces.map(async (space) => { spaces.map(async (space) => {
const pages = await spacePromises.get(space); const pages = await spacePromises.get(space);
return { space, pages }; return { space, pages };
}) })
).then((results) => { ).then((results) => {
return results.reduce((map, result) => { return results.reduce((map, result) => {
if (result.pages) { if (result.pages) {
map.set(result.space, result.pages); map.set(result.space, result.pages);
} }
return map; return map;
}, new Map<string, RevisionPage[]>()); }, 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. * Stream a list of suggested questions for the site.
*/ */
export const streamRecommendedQuestions = streamResponse(async function* ( export async function streamRecommendedQuestions() {
organizationId: string, const siteURLData = await getSiteURLDataFromMiddleware();
siteId: string const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
) {
const apiCtx = await api.api();
const stream = apiCtx.client.orgs.streamRecommendedQuestionsInSite(organizationId, siteId);
for await (const chunk of stream) { const responseStream = createStreamableValue<SearchAIRecommendedQuestionStream | undefined>();
yield chunk;
(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( async function transformAnswer(
context: GitBookSiteContext, context: GitBookSiteContext,
@@ -343,16 +361,6 @@ async function transformSitePageResult(item: SearchPageResult, siteSpace?: SiteS
return [page, ...sections]; 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 // Resolve a relative path to an absolute URL
// if the search result is relative to another space, we use the space URL // if the search result is relative to another space, we use the space URL
async function getURLWithSections(path: string, spaceURL?: string) { async function getURLWithSections(path: string, spaceURL?: string) {
@@ -24,9 +24,10 @@ export async function SiteLayout(props: {
context: GitBookSiteContext; context: GitBookSiteContext;
forcedTheme?: CustomizationThemeMode | null; forcedTheme?: CustomizationThemeMode | null;
withTracking: boolean; withTracking: boolean;
visitorAuthToken: string | null;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const { context, nonce, forcedTheme, withTracking, children } = props; const { context, nonce, forcedTheme, withTracking, visitorAuthToken, children } = props;
const { scripts, customization } = context; const { scripts, customization } = context;
@@ -51,7 +52,11 @@ export async function SiteLayout(props: {
(customization.themes.toggeable ? undefined : customization.themes.default) (customization.themes.toggeable ? undefined : customization.themes.default)
} }
> >
<SpaceLayout context={context} withTracking={withTracking}> <SpaceLayout
context={context}
withTracking={withTracking}
visitorAuthToken={visitorAuthToken}
>
{children} {children}
</SpaceLayout> </SpaceLayout>
@@ -67,7 +67,7 @@ export async function SitePage(props: SitePageProps) {
const withSections = Boolean(sections && sections.list.length > 0); const withSections = Boolean(sections && sections.list.length > 0);
const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader }; 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 ( return (
<> <>
@@ -10,7 +10,6 @@ import { CONTAINER_STYLE } from '@/components/layout';
import { getSpaceLanguage } from '@/intl/server'; import { getSpaceLanguage } from '@/intl/server';
import { t } from '@/intl/translate'; import { t } from '@/intl/translate';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { getSitePointerFromContext } from '@/lib/v1';
import { SpacesDropdown } from '../Header/SpacesDropdown'; import { SpacesDropdown } from '../Header/SpacesDropdown';
import { InsightsProvider } from '../Insights'; import { InsightsProvider } from '../Insights';
@@ -25,10 +24,13 @@ export function SpaceLayout(props: {
/** Whether to enable tracking of events into site insights. */ /** Whether to enable tracking of events into site insights. */
withTracking: boolean; withTracking: boolean;
/** The visitor token used to access this content */
visitorAuthToken: string | null;
/** The children of the layout. */ /** The children of the layout. */
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const { context, withTracking, children } = props; const { context, withTracking, visitorAuthToken, children } = props;
const { siteSpace, customization, sections, siteSpaces } = context; const { siteSpace, customization, sections, siteSpaces } = context;
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None; const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
@@ -46,7 +48,7 @@ export function SpaceLayout(props: {
<InsightsProvider <InsightsProvider
enabled={withTracking} enabled={withTracking}
apiHost={context.dataFetcher.apiEndpoint} apiHost={context.dataFetcher.apiEndpoint}
visitorAuthToken={context.visitorAuthToken} visitorAuthToken={visitorAuthToken}
organizationId={context.organizationId} organizationId={context.organizationId}
siteId={context.site.id} siteId={context.site.id}
siteSectionId={context.sections?.current?.id ?? null} siteSectionId={context.sections?.current?.id ?? null}
@@ -129,11 +131,9 @@ export function SpaceLayout(props: {
<React.Suspense fallback={null}> <React.Suspense fallback={null}>
<SearchModal <SearchModal
revisionId={context.revisionId}
spaceTitle={siteSpace.title} spaceTitle={siteSpace.title}
withAsk={customization.aiSearch.enabled} withAsk={customization.aiSearch.enabled}
isMultiVariants={isMultiVariants} isMultiVariants={isMultiVariants}
pointer={getSitePointerFromContext(context)}
/> />
</React.Suspense> </React.Suspense>
</InsightsProvider> </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]);
}
-49
View File
@@ -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
View File
@@ -1,4 +1,5 @@
import { AsyncLocalStorage } from 'node:async_hooks'; import { AsyncLocalStorage } from 'node:async_hooks';
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
import 'server-only'; import 'server-only';
import { import {
@@ -14,7 +15,6 @@ import {
type RevisionReusableContent, type RevisionReusableContent,
} from '@gitbook/api'; } from '@gitbook/api';
import type { GitBookDataFetcher } from '@v2/lib/data/types'; import type { GitBookDataFetcher } from '@v2/lib/data/types';
import assertNever from 'assert-never';
import { headers } from 'next/headers'; import { headers } from 'next/headers';
import { batch } from './async'; import { batch } from './async';
@@ -171,7 +171,7 @@ export type PublishedContentWithCache =
export const getUserById = cache({ export const getUserById = cache({
name: 'api.getUserById', name: 'api.getUserById',
tag: (userId) => tag: (userId) =>
getAPICacheTag({ getCacheTag({
tag: 'user', tag: 'user',
user: userId, user: userId,
}), }),
@@ -204,7 +204,7 @@ export const getUserById = cache({
export const getLatestOpenAPISpecVersionContent = cache({ export const getLatestOpenAPISpecVersionContent = cache({
name: 'api.getLatestOpenApiSpecVersionContent', name: 'api.getLatestOpenApiSpecVersionContent',
tag: (organization, openAPISpec) => tag: (organization, openAPISpec) =>
getAPICacheTag({ getCacheTag({
tag: 'openapi', tag: 'openapi',
organization, organization,
openAPISpec, openAPISpec,
@@ -240,7 +240,7 @@ export const getLatestOpenAPISpecVersionContent = cache({
export const getPublishedContentByUrl = cache({ export const getPublishedContentByUrl = cache({
name: 'api.getPublishedContentByUrl.v4', name: 'api.getPublishedContentByUrl.v4',
tag: (url) => tag: (url) =>
getAPICacheTag({ getCacheTag({
tag: 'url', tag: 'url',
hostname: new URL(url).hostname, hostname: new URL(url).hostname,
}), }),
@@ -301,7 +301,7 @@ export const getPublishedContentByUrl = cache({
*/ */
export const getSpace = cache({ export const getSpace = cache({
name: 'api.getSpace', 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) => { get: async (spaceId: string, shareKey: string | undefined, options: CacheFunctionOptions) => {
const apiCtx = await api(); const apiCtx = await api();
const response = await apiCtx.client.spaces.getSpaceById( const response = await apiCtx.client.spaces.getSpaceById(
@@ -330,7 +330,7 @@ function checkHasErrorCode(error: unknown, code: number) {
export const getChangeRequest = cache({ export const getChangeRequest = cache({
name: 'api.getChangeRequest', name: 'api.getChangeRequest',
tag: (spaceId, changeRequestId) => 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) => { get: async (spaceId: string, changeRequestId: string, options: CacheFunctionOptions) => {
const apiCtx = await api(); const apiCtx = await api();
try { try {
@@ -379,7 +379,7 @@ const getAPIContextId = async () => {
export const getRevision = cache({ export const getRevision = cache({
name: 'api.getRevision.v2', name: 'api.getRevision.v2',
tag: (spaceId, revisionId) => tag: (spaceId, revisionId) =>
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
getKeySuffix: getAPIContextId, getKeySuffix: getAPIContextId,
get: async ( get: async (
spaceId: string, spaceId: string,
@@ -411,7 +411,7 @@ export const getRevision = cache({
export const getRevisionPages = cache({ export const getRevisionPages = cache({
name: 'api.getRevisionPages.v4', name: 'api.getRevisionPages.v4',
tag: (spaceId, revisionId) => tag: (spaceId, revisionId) =>
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
getKeySuffix: getAPIContextId, getKeySuffix: getAPIContextId,
get: async ( get: async (
spaceId: string, spaceId: string,
@@ -446,7 +446,7 @@ export const getRevisionPages = cache({
export const getRevisionPageByPath = cache({ export const getRevisionPageByPath = cache({
name: 'api.getRevisionPageByPath.v3', name: 'api.getRevisionPageByPath.v3',
tag: (spaceId, revisionId) => tag: (spaceId, revisionId) =>
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
getKeySuffix: getAPIContextId, getKeySuffix: getAPIContextId,
get: async ( get: async (
spaceId: string, spaceId: string,
@@ -492,7 +492,7 @@ export const getRevisionPageByPath = cache({
const getRevisionFileById = cache({ const getRevisionFileById = cache({
name: 'api.getRevisionFile.v3', name: 'api.getRevisionFile.v3',
tag: (spaceId, revisionId) => tag: (spaceId, revisionId) =>
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
get: async ( get: async (
spaceId: string, spaceId: string,
revisionId: string, revisionId: string,
@@ -528,7 +528,7 @@ const getRevisionFileById = cache({
const getRevisionReusableContentById = cache({ const getRevisionReusableContentById = cache({
name: 'api.getRevisionReusableContentById.v1', name: 'api.getRevisionReusableContentById.v1',
tag: (spaceId, revisionId) => tag: (spaceId, revisionId) =>
getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
getKeySuffix: getAPIContextId, getKeySuffix: getAPIContextId,
get: async ( get: async (
spaceId: string, spaceId: string,
@@ -569,7 +569,7 @@ const getRevisionReusableContentById = cache({
const getRevisionAllFiles = cache({ const getRevisionAllFiles = cache({
name: 'api.getRevisionAllFiles.v2', name: 'api.getRevisionAllFiles.v2',
tag: (spaceId, revisionId) => 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) => { get: async (spaceId: string, revisionId: string, options: CacheFunctionOptions) => {
const response = await getAll( const response = await getAll(
async (params) => { async (params) => {
@@ -684,7 +684,7 @@ export const getReusableContent = async (
export const getDocument = cache({ export const getDocument = cache({
name: 'api.getDocument.v2', name: 'api.getDocument.v2',
tag: (spaceId, documentId) => tag: (spaceId, documentId) =>
getAPICacheTag({ tag: 'document', space: spaceId, document: documentId }), getCacheTag({ tag: 'document', space: spaceId, document: documentId }),
getKeySuffix: getAPIContextId, getKeySuffix: getAPIContextId,
get: async (spaceId: string, documentId: string, options: CacheFunctionOptions) => { get: async (spaceId: string, documentId: string, options: CacheFunctionOptions) => {
const apiCtx = await api(); const apiCtx = await api();
@@ -712,14 +712,21 @@ export const getDocument = cache({
*/ */
export const getComputedDocument = cache({ export const getComputedDocument = cache({
name: 'api.getComputedDocument', name: 'api.getComputedDocument',
tag: (spaceId, source) => tag: (organizationId, spaceId, source) =>
getAPICacheTag({ getComputedContentSourceCacheTags(
tag: 'computed-document', {
space: spaceId, organizationId,
integration: source.integration, spaceId,
}), },
source
)[0],
getKeySuffix: getAPIContextId, 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 apiCtx = await api();
const response = await apiCtx.client.spaces.getComputedDocument( const response = await apiCtx.client.spaces.getComputedDocument(
spaceId, spaceId,
@@ -750,7 +757,7 @@ function validateSiteRedirectSource(source: string) {
*/ */
export const getSiteRedirectBySource = cache({ export const getSiteRedirectBySource = cache({
name: 'api.getSiteRedirectBySource', name: 'api.getSiteRedirectBySource',
tag: ({ siteId }) => getAPICacheTag({ tag: 'site', site: siteId }), tag: ({ siteId }) => getCacheTag({ tag: 'site', site: siteId }),
getKeySuffix: getAPIContextId, getKeySuffix: getAPIContextId,
get: async ( get: async (
args: { args: {
@@ -811,7 +818,7 @@ export const getSiteRedirectBySource = cache({
*/ */
export const getSite = cache({ export const getSite = cache({
name: 'api.getSite', name: 'api.getSite',
tag: (_organizationId, siteId) => getAPICacheTag({ tag: 'site', site: siteId }), tag: (_organizationId, siteId) => getCacheTag({ tag: 'site', site: siteId }),
getKeySuffix: getAPIContextId, getKeySuffix: getAPIContextId,
get: async (organizationId: string, siteId: string, options: CacheFunctionOptions) => { get: async (organizationId: string, siteId: string, options: CacheFunctionOptions) => {
const apiCtx = await api(); const apiCtx = await api();
@@ -830,7 +837,7 @@ export const getSite = cache({
*/ */
export const getPublishedContentSite = cache({ export const getPublishedContentSite = cache({
name: 'api.getPublishedContentSite', name: 'api.getPublishedContentSite',
tag: ({ siteId }) => getAPICacheTag({ tag: 'site', site: siteId }), tag: ({ siteId }) => getCacheTag({ tag: 'site', site: siteId }),
getKeySuffix: getAPIContextId, getKeySuffix: getAPIContextId,
get: async ( get: async (
args: { args: {
@@ -907,7 +914,7 @@ export async function getSpaceContentData(
*/ */
export const searchSiteContent = cache({ export const searchSiteContent = cache({
name: 'api.searchSiteContent', name: 'api.searchSiteContent',
tag: (_organizationId, siteId) => getAPICacheTag({ tag: 'site', site: siteId }), tag: (_organizationId, siteId) => getCacheTag({ tag: 'site', site: siteId }),
getKeySuffix: getAPIContextId, getKeySuffix: getAPIContextId,
get: async ( get: async (
organizationId: string, organizationId: string,
@@ -947,7 +954,7 @@ export const searchSiteContent = cache({
*/ */
export const renderIntegrationUi = cache({ export const renderIntegrationUi = cache({
name: 'api.renderIntegrationUi', name: 'api.renderIntegrationUi',
tag: (integrationName) => getAPICacheTag({ tag: 'integration', integration: integrationName }), tag: (integrationName) => getCacheTag({ tag: 'integration', integration: integrationName }),
get: async ( get: async (
integrationName: string, integrationName: string,
request: RequestRenderIntegrationUI, request: RequestRenderIntegrationUI,
@@ -971,7 +978,7 @@ export const renderIntegrationUi = cache({
*/ */
export const getEmbedByUrlInSpace = cache({ export const getEmbedByUrlInSpace = cache({
name: 'api.getEmbedByUrlInSpace', 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) => { get: async (spaceId: string, url: string, options: CacheFunctionOptions) => {
const apiCtx = await api(); const apiCtx = await api();
const response = await apiCtx.client.spaces.getEmbedByUrlInSpace( 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. * 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({ const { data: operation } = await resolveOpenAPIBlock({
block, block,
context, context,
+3 -2
View File
@@ -22,7 +22,7 @@ export function hasFullWidthBlock(document: JSONDocument): boolean {
if (node.data && 'fullWidth' in node.data && node.data.fullWidth) { if (node.data && 'fullWidth' in node.data && node.data.fullWidth) {
return true; return true;
} }
if (node.type === 'swagger') { if (node.type === 'swagger' || node.type === 'openapi-operation') {
return true; return true;
} }
} }
@@ -120,7 +120,8 @@ export function getBlockTitle(block: DocumentBlock): string {
return block.data.title ?? ''; return block.data.title ?? '';
} }
case 'swagger': { case 'swagger':
case 'openapi-operation': {
return `${block.data.method?.toUpperCase()} ${block.data.path}`; return `${block.data.method?.toUpperCase()} ${block.data.path}`;
} }
default: default:
+1 -1
View File
@@ -130,7 +130,7 @@ export async function resolveContentRef(
text = `#${anchor}`; text = `#${anchor}`;
if (resolveAnchorText) { if (resolveAnchorText) {
const document = await getPageDocument(dataFetcher, space.id, page); const document = await getPageDocument(dataFetcher, space, page);
if (document) { if (document) {
const block = getBlockById(document, anchor); const block = getBlockById(document, anchor);
if (block) { if (block) {
+22 -5
View File
@@ -7,7 +7,6 @@ import {
import type { GitBookDataFetcher } from '@v2/lib/data/types'; import type { GitBookDataFetcher } from '@v2/lib/data/types';
import { createImageResizer } from '@v2/lib/images'; import { createImageResizer } from '@v2/lib/images';
import { createLinker } from '@v2/lib/links'; import { createLinker } from '@v2/lib/links';
import { headers } from 'next/headers';
import { import {
type SiteContentPointer, type SiteContentPointer,
@@ -28,6 +27,7 @@ import {
getSiteRedirectBySource, getSiteRedirectBySource,
getSpace, getSpace,
getUserById, getUserById,
searchSiteContent,
} from './api'; } from './api';
import { getDynamicCustomizationSettings } from './customization'; import { getDynamicCustomizationSettings } from './customization';
import { getBasePath, getHost } from './links'; import { getBasePath, getHost } from './links';
@@ -74,9 +74,20 @@ export async function getV1BaseContext(): Promise<GitBookBaseContext> {
async function getDataFetcherV1(): Promise<GitBookDataFetcher> { async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
const apiClient = await api(); const apiClient = await api();
return { const dataFetcher: GitBookDataFetcher = {
apiEndpoint: apiClient.client.endpoint, 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) { getUserById(userId) {
return getUserById(userId); return getUserById(userId);
}, },
@@ -117,7 +128,7 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
}, },
getComputedDocument(params) { getComputedDocument(params) {
return getComputedDocument(params.spaceId, params.source); return getComputedDocument(params.organizationId, params.spaceId, params.source);
}, },
getRevisionPages(params) { getRevisionPages(params) {
@@ -145,7 +156,15 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
getEmbedByUrl(params) { getEmbedByUrl(params) {
return getEmbedByUrlInSpace(params.spaceId, params.url); 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) { export async function fetchV1ContextForSitePointer(pointer: SiteContentPointer) {
const baseContext = await getV1BaseContext(); const baseContext = await getV1BaseContext();
const headersList = await headers();
const context = await fetchSiteContextByIds(baseContext, { const context = await fetchSiteContextByIds(baseContext, {
organization: pointer.organizationId, organization: pointer.organizationId,
@@ -164,7 +182,6 @@ export async function fetchV1ContextForSitePointer(pointer: SiteContentPointer)
shareKey: pointer.siteShareKey, shareKey: pointer.siteShareKey,
changeRequest: pointer.changeRequestId, changeRequest: pointer.changeRequestId,
revision: pointer.revisionId, revision: pointer.revisionId,
visitorAuthToken: headersList.get('x-gitbook-visitor-token'),
}); });
context.customization = await getDynamicCustomizationSettings(context.customization); context.customization = await getDynamicCustomizationSettings(context.customization);
+26 -11
View File
@@ -32,6 +32,7 @@ import {
import { joinPath, normalizePathname } from '@/lib/paths'; import { joinPath, normalizePathname } from '@/lib/paths';
import { getProxyModeBasePath } from '@/lib/proxy'; import { getProxyModeBasePath } from '@/lib/proxy';
import { MiddlewareHeaders } from '@v2/lib/middleware';
export const config = { export const config = {
matcher: matcher:
@@ -83,6 +84,8 @@ export type LookupResult = PublishedContentWithCache & {
cookies?: LookupCookies; cookies?: LookupCookies;
/** Visitor authentication token */ /** Visitor authentication token */
visitorToken?: string; visitorToken?: string;
/** URL of the site */
siteURL?: string;
}; };
/** /**
@@ -199,6 +202,13 @@ export async function middleware(request: NextRequest) {
if (resolved.shareKey) { if (resolved.shareKey) {
headers.set('x-gitbook-content-site-share-key', 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 // 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); 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()); const target = new URL(joinPath('/middleware', rewritePathname), request.nextUrl.toString());
target.search = url.search; target.search = url.search;
@@ -761,14 +767,23 @@ async function lookupSiteByAPI(
return null; return null;
}); });
return ( if (result) {
result ?? { if ('site' in result) {
error: { return {
code: 404, ...result,
message: 'No content found', siteURL: `${lookupURL.origin}${result.basePath}`,
}, };
} }
);
return result;
}
return {
error: {
code: 404,
message: 'No content found',
},
};
} }
/** /**
+6
View File
@@ -1,5 +1,11 @@
# @gitbook/openapi-parser # @gitbook/openapi-parser
## 2.0.2
### Patch Changes
- 53f5dbe: Fix typing of `shouldIgnoreEntity`
## 2.0.1 ## 2.0.1
### Patch Changes ### Patch Changes
+1 -1
View File
@@ -9,7 +9,7 @@
"default": "./dist/index.js" "default": "./dist/index.js"
} }
}, },
"version": "2.0.1", "version": "2.0.2",
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@scalar/openapi-parser": "^0.10.4", "@scalar/openapi-parser": "^0.10.4",
@@ -4,10 +4,7 @@ import type { OpenAPIV3 } from '@scalar/openapi-types';
* Check if an entity should be ignored * Check if an entity should be ignored
*/ */
export function shouldIgnoreEntity( export function shouldIgnoreEntity(
data: data: undefined | OpenAPIV3.TagObject | OpenAPIV3.OperationObject
| undefined
| Pick<OpenAPIV3.TagObject, 'x-internal' | 'x-gitbook-ignore'>
| Pick<OpenAPIV3.OperationObject, 'x-internal' | 'x-gitbook-ignore'>
) { ) {
return data?.['x-internal'] === true || data?.['x-gitbook-ignore'] === true; return data?.['x-internal'] === true || data?.['x-gitbook-ignore'] === true;
} }
+6
View File
@@ -1,5 +1,11 @@
# @gitbook/react-contentkit # @gitbook/react-contentkit
## 0.6.1
### Patch Changes
- 8beb5d6: Add input elements to ContentKit
## 0.6.0 ## 0.6.0
### Minor Changes ### Minor Changes
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@gitbook/react-contentkit", "name": "@gitbook/react-contentkit",
"version": "0.6.0", "version": "0.6.1",
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -11,7 +11,8 @@
"dependencies": { "dependencies": {
"classnames": "^2.5.1", "classnames": "^2.5.1",
"@gitbook/api": "0.96.1", "@gitbook/api": "0.96.1",
"assert-never": "^1.2.1" "assert-never": "^1.2.1",
"@gitbook/icons": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*" "react": "*"
@@ -13,6 +13,7 @@ import { ElementCodeBlock } from './ElementCodeBlock';
import { ElementDivider } from './ElementDivider'; import { ElementDivider } from './ElementDivider';
import { ElementIcon } from './ElementIcon'; import { ElementIcon } from './ElementIcon';
import { ElementImage } from './ElementImage'; import { ElementImage } from './ElementImage';
import { ElementInput } from './ElementInput';
import { ElementMarkdown } from './ElementMarkdown'; import { ElementMarkdown } from './ElementMarkdown';
import { ElementModal } from './ElementModal'; import { ElementModal } from './ElementModal';
import { ElementStack } from './ElementStack'; import { ElementStack } from './ElementStack';
@@ -152,6 +153,8 @@ export function Element(props: {
return <ElementCodeBlock element={element} context={context} state={state} />; return <ElementCodeBlock element={element} context={context} state={state} />;
case 'webframe': case 'webframe':
return <ElementWebframe element={element} />; return <ElementWebframe element={element} />;
case 'input':
return <ElementInput element={element} context={context} state={state} />;
default: default:
return ( return (
<pre <pre
@@ -1,6 +1,7 @@
'use client'; 'use client';
import type { ContentKitButton } from '@gitbook/api'; import type { ContentKitButton } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -19,16 +20,14 @@ export function ElementButton(
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
// TODO: // TODO:
// - loading // - confirm dialog
// - confirm
return ( return (
<button <button
title={element.tooltip} title={element.tooltip}
className={classNames( className={classNames(
'contentkit-button', 'contentkit-button',
`contentkit-button-style-${element.style ?? 'secondary'}`, `contentkit-button-style-${element.style ?? 'secondary'}`
loading ? 'contentkit-button-loading' : null
)} )}
onClick={(event) => { onClick={(event) => {
if (element.disabled || loading) { if (element.disabled || loading) {
@@ -43,11 +42,17 @@ export function ElementButton(
}); });
}} }}
> >
{icon} {loading ? (
{element.label ? ( <Icon icon="spinner" className="contentkit-button-loading" />
<span className="contentkit-button-label">{element.label}</span> ) : (
) : null} <>
{trailingIcon} {icon}
{element.label ? (
<span className="contentkit-button-label">{element.label}</span>
) : null}
{trailingIcon}
</>
)}
</button> </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} value={value}
placeholder={element.placeholder} placeholder={element.placeholder}
onChange={onChange} onChange={onChange}
rows={4}
/> />
); );
} }
+7
View File
@@ -1,5 +1,12 @@
# @gitbook/react-openapi # @gitbook/react-openapi
## 1.0.5
### Patch Changes
- Updated dependencies [53f5dbe]
- @gitbook/openapi-parser@2.0.2
## 1.0.4 ## 1.0.4
### Patch Changes ### Patch Changes
+1 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js" "default": "./dist/index.js"
} }
}, },
"version": "1.0.4", "version": "1.0.5",
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@gitbook/openapi-parser": "workspace:*", "@gitbook/openapi-parser": "workspace:*",
@@ -4,6 +4,7 @@ import clsx from 'clsx';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria'; import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
import { useDisclosureState } from 'react-stately'; import { useDisclosureState } from 'react-stately';
import { Section, SectionBody, SectionHeader, SectionHeaderContent } from './StaticSection';
interface InteractiveSectionTab { interface InteractiveSectionTab {
key: string; key: string;
@@ -63,7 +64,7 @@ export function InteractiveSection(props: {
const { isFocusVisible, focusProps } = useFocusRing(); const { isFocusVisible, focusProps } = useFocusRing();
return ( return (
<div <Section
id={id} id={id}
className={clsx( className={clsx(
'openapi-section', 'openapi-section',
@@ -73,20 +74,15 @@ export function InteractiveSection(props: {
)} )}
> >
{header ? ( {header ? (
<div <SectionHeader
onClick={() => { onClick={() => {
if (toggeable) { if (toggeable) {
state.toggle(); state.toggle();
} }
}} }}
className={clsx('openapi-section-header', `${className}-header`)} className={className}
> >
<div <SectionHeaderContent className={className}>
className={clsx(
'openapi-section-header-content',
`${className}-header-content`
)}
>
{(children || selectedTab?.body) && toggeable ? ( {(children || selectedTab?.body) && toggeable ? (
<button <button
{...mergeProps(buttonProps, focusProps)} {...mergeProps(buttonProps, focusProps)}
@@ -102,7 +98,7 @@ export function InteractiveSection(props: {
</button> </button>
) : null} ) : null}
{header} {header}
</div> </SectionHeaderContent>
<div <div
className={clsx( className={clsx(
'openapi-section-header-controls', 'openapi-section-header-controls',
@@ -133,23 +129,19 @@ export function InteractiveSection(props: {
</select> </select>
) : null} ) : null}
</div> </div>
</div> </SectionHeader>
) : null} ) : null}
{(!toggeable || state.isExpanded) && (children || selectedTab?.body) ? ( {(!toggeable || state.isExpanded) && (children || selectedTab?.body) ? (
<div <SectionBody ref={panelRef} {...panelProps} className={className}>
ref={panelRef}
{...panelProps}
className={clsx('openapi-section-body', `${className}-body`)}
>
{children} {children}
{selectedTab?.body} {selectedTab?.body}
</div> </SectionBody>
) : null} ) : null}
{overlay ? ( {overlay ? (
<div className={clsx('openapi-section-overlay', `${className}-overlay`)}> <div className={clsx('openapi-section-overlay', `${className}-overlay`)}>
{overlay} {overlay}
</div> </div>
) : null} ) : null}
</div> </Section>
); );
} }
@@ -1,5 +1,5 @@
import { InteractiveSection } from './InteractiveSection';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
import { StaticSection } from './StaticSection';
import { type CodeSampleInput, codeSampleGenerators } from './code-samples'; import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample'; import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
import { stringifyOpenAPI } from './stringifyOpenAPI'; import { stringifyOpenAPI } from './stringifyOpenAPI';
@@ -117,9 +117,9 @@ export function OpenAPICodeSample(props: {
return ( return (
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}> <OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
<InteractiveSection header={<OpenAPITabsList />} className="openapi-codesample"> <StaticSection header={<OpenAPITabsList />} className="openapi-codesample">
<OpenAPITabsPanels /> <OpenAPITabsPanels />
</InteractiveSection> </StaticSection>
</OpenAPITabs> </OpenAPITabs>
); );
} }
@@ -1,3 +1,4 @@
'use client';
import type React from 'react'; import type React from 'react';
import { useRef } from 'react'; import { useRef } from 'react';
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria'; 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 { interface Props {
groups: TDisclosureGroup[]; groups: TDisclosureGroup[];
icon?: React.ReactNode; 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); const DisclosureGroupStateContext = createContext<DisclosureGroupState | null>(null);
/** /**
@@ -1,6 +1,6 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { InteractiveSection } from './InteractiveSection';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
import { StaticSection } from './StaticSection';
import { generateSchemaExample } from './generateSchemaExample'; import { generateSchemaExample } from './generateSchemaExample';
import { json2xml } from './json2xml'; import { json2xml } from './json2xml';
import { stringifyOpenAPI } from './stringifyOpenAPI'; import { stringifyOpenAPI } from './stringifyOpenAPI';
@@ -84,9 +84,9 @@ export function OpenAPIResponseExample(props: {
return ( return (
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}> <OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example"> <StaticSection header={<OpenAPITabsList />} className="openapi-response-example">
<OpenAPITabsPanels /> <OpenAPITabsPanels />
</InteractiveSection> </StaticSection>
</OpenAPITabs> </OpenAPITabs>
); );
} }
@@ -134,12 +134,9 @@ function OpenAPIResponse(props: {
return ( return (
<OpenAPITabs stateKey={createStateKey('response-media-types')} items={tabs}> <OpenAPITabs stateKey={createStateKey('response-media-types')} items={tabs}>
<InteractiveSection <StaticSection header={<OpenAPITabsList />} className="openapi-response-media-types">
header={<OpenAPITabsList />}
className="openapi-response-media-types"
>
<OpenAPITabsPanels /> <OpenAPITabsPanels />
</InteractiveSection> </StaticSection>
</OpenAPITabs> </OpenAPITabs>
); );
} }
@@ -173,23 +170,19 @@ function OpenAPIResponseMediaType(props: {
key: example.key, key: example.key,
label: example.example.summary || example.key, label: example.example.summary || example.key,
body: ( body: (
<OpenAPIExample <OpenAPIExample example={example.example} context={props.context} syntax={syntax} />
example={firstExample.example}
context={props.context}
syntax={syntax}
/>
), ),
}; };
}); });
return ( return (
<OpenAPITabs stateKey={createStateKey('response-media-type-examples')} items={tabs}> <OpenAPITabs stateKey={createStateKey('response-media-type-examples')} items={tabs}>
<InteractiveSection <StaticSection
header={<OpenAPITabsList />} header={<OpenAPITabsList />}
className="openapi-response-media-type-examples" className="openapi-response-media-type-examples"
> >
<OpenAPITabsPanels /> <OpenAPITabsPanels />
</InteractiveSection> </StaticSection>
</OpenAPITabs> </OpenAPITabs>
); );
} }
@@ -1,8 +1,8 @@
import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser'; import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
import { InteractiveSection } from './InteractiveSection';
import { Markdown } from './Markdown'; import { Markdown } from './Markdown';
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup'; import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
import { OpenAPIResponse } from './OpenAPIResponse'; import { OpenAPIResponse } from './OpenAPIResponse';
import { StaticSection } from './StaticSection';
import type { OpenAPIClientContext } from './types'; import type { OpenAPIClientContext } from './types';
/** /**
@@ -15,7 +15,7 @@ export function OpenAPIResponses(props: {
const { responses, context } = props; const { responses, context } = props;
return ( return (
<InteractiveSection header="Responses" className="openapi-responses"> <StaticSection header="Responses" className="openapi-responses">
<OpenAPIDisclosureGroup <OpenAPIDisclosureGroup
allowsMultipleExpanded allowsMultipleExpanded
icon={context.icons.chevronRight} icon={context.icons.chevronRight}
@@ -58,6 +58,6 @@ export function OpenAPIResponses(props: {
} }
)} )}
/> />
</InteractiveSection> </StaticSection>
); );
} }
+2 -2
View File
@@ -119,9 +119,9 @@ export function OpenAPISchemaProperties(props: {
return ( return (
<div id={id} className="openapi-schema-properties"> <div id={id} className="openapi-schema-properties">
{properties.map((property) => ( {properties.map((property, index) => (
<OpenAPISchemaProperty <OpenAPISchemaProperty
key={property.propertyName} key={index}
circularRefs={circularRefs} circularRefs={circularRefs}
{...property} {...property}
context={context} context={context}
+3 -5
View File
@@ -1,12 +1,10 @@
'use client';
import type { OpenAPI } from '@gitbook/openapi-parser'; import type { OpenAPI } from '@gitbook/openapi-parser';
import { InteractiveSection } from './InteractiveSection';
import { OpenAPIRequestBody } from './OpenAPIRequestBody'; import { OpenAPIRequestBody } from './OpenAPIRequestBody';
import { OpenAPIResponses } from './OpenAPIResponses'; import { OpenAPIResponses } from './OpenAPIResponses';
import { OpenAPISchemaProperties } from './OpenAPISchema'; import { OpenAPISchemaProperties } from './OpenAPISchema';
import { OpenAPISecurities } from './OpenAPISecurities'; import { OpenAPISecurities } from './OpenAPISecurities';
import { StaticSection } from './StaticSection';
import type { OpenAPIClientContext, OpenAPIOperationData } from './types'; import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
import { parameterToProperty } from './utils'; import { parameterToProperty } from './utils';
@@ -32,7 +30,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
{parameterGroups.map((group) => { {parameterGroups.map((group) => {
return ( return (
<InteractiveSection <StaticSection
key={group.key} key={group.key}
className="openapi-parameters" className="openapi-parameters"
header={group.label} header={group.label}
@@ -41,7 +39,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
properties={group.parameters.map(parameterToProperty)} properties={group.parameters.map(parameterToProperty)}
context={context} context={context}
/> />
</InteractiveSection> </StaticSection>
); );
})} })}
+53 -64
View File
@@ -1,10 +1,10 @@
'use client'; '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 { 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 { Markdown } from './Markdown';
import { useSyncedTabsGlobalState } from './useSyncedTabsGlobalState'; import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
export type TabItem = { export type TabItem = {
key: Key; key: Key;
@@ -15,7 +15,7 @@ export type TabItem = {
type OpenAPITabsContextData = { type OpenAPITabsContextData = {
items: TabItem[]; items: TabItem[];
selectedTab: TabItem; selectedTab: TabItem | null;
}; };
const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null); const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null);
@@ -35,68 +35,54 @@ export function OpenAPITabs(
props: React.PropsWithChildren<TabsProps & { items: TabItem[]; stateKey?: string }> props: React.PropsWithChildren<TabsProps & { items: TabItem[]; stateKey?: string }>
) { ) {
const { children, items, stateKey } = props; const { children, items, stateKey } = props;
const [ref, isIntersectionVisible] = useIntersectionObserver({ const [tabKey, setTabKey] = useState<Key | null>(() => {
threshold: 0.1, if (stateKey && typeof window !== 'undefined') {
rootMargin: '200px', const store = getOrCreateTabStoreByKey(stateKey);
}); const tabKey = store.getState().tabKey;
const isVisible = stateKey ? isIntersectionVisible : true; if (tabKey) {
const defaultTab = items[0] as TabItem; return tabKey;
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;
} }
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(() => { useEffect(() => {
if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) { if (!stateKey) {
const tabFromState = syncedTabs.get(stateKey); return undefined;
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);
}
} }
}, [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]); const contextValue = useMemo(() => ({ items, selectedTab }), [items, selectedTab]);
return ( return (
<OpenAPITabsContext.Provider value={contextValue}> <OpenAPITabsContext.Provider value={contextValue}>
<Tabs <Tabs
ref={ref}
className="openapi-tabs" className="openapi-tabs"
onSelectionChange={handleSelectionChange} onSelectionChange={(tabKey) => {
selectedKey={selectedTab?.key} selectTab(tabKey);
if (stateKey) {
const store = getOrCreateTabStoreByKey(stateKey);
store.setState({ tabKey });
}
}}
selectedKey={tabKey}
> >
{children} {children}
</Tabs> </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. * The OpenAPI Tabs list component.
* This component should be used as a child of the OpenAPITabs 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"> <TabList className="openapi-tabs-list">
{items.map((tab) => ( {items.map((tab) => (
<Tab <Tab
key={tab.key}
id={tab.key}
style={({ isFocusVisible }) => ({ style={({ isFocusVisible }) => ({
outline: isFocusVisible outline: isFocusVisible
? '2px solid rgb(var(--primary-color-500)/0.4)' ? '2px solid rgb(var(--primary-color-500)/0.4)'
: 'none', : 'none',
})} })}
className="openapi-tabs-tab" className="openapi-tabs-tab"
key={`Tab-${tab.key}`}
id={tab.key}
> >
{tab.label} {tab.label}
</Tab> </Tab>
@@ -144,12 +135,10 @@ export function OpenAPITabsPanels() {
return null; return null;
} }
const key = selectedTab.key.toString();
return ( return (
<TabPanel <TabPanel key={key} id={key} className="openapi-tabs-panel">
key={`TabPanel-${selectedTab.key}`}
id={selectedTab.key.toString()}
className="openapi-tabs-panel"
>
{selectedTab.body} {selectedTab.body}
{selectedTab.description ? ( {selectedTab.description ? (
<Markdown source={selectedTab.description} className="openapi-tabs-footer" /> <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'; 'use client';
import { create } from 'zustand'; import { createStore } from 'zustand';
interface SyncedTabsState<T> { type Key = string | number;
tabs: Map<string, T>;
setTabs: (updater: (tabs: Map<string, T>) => Map<string, T>) => void;
}
const useSyncedTabsStore = create<SyncedTabsState<any>>()((set) => ({ type TabState = {
tabs: new Map<string, any>(), tabKey: Key | null;
setTabs: (updater) => };
set((state) => ({
tabs: updater(new Map(state.tabs)), // Ensure a new Map is created for reactivity
})),
}));
// Selector for better performance - only re-renders when tabs change type TabActions = { setTabKey: (tab: Key | null) => void };
export function useSyncedTabsGlobalState<T>() {
const tabs = useSyncedTabsStore((state) => state.tabs as Map<string, T>); type TabStore = TabState & TabActions;
const setTabs = useSyncedTabsStore((state) => state.setTabs as SyncedTabsState<T>['setTabs']);
return [tabs, setTabs] as const; 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);