@@ -11,17 +11,76 @@ import { ReadmeContent } from "../components/readme-content";
1111const LOCAL_PACKAGE_PATTERN = / g i t h u b \. c o m \/ v e r c e l \/ c h a t \/ t r e e \/ [ ^ / ] + \/ ( .+ ) / ;
1212const GITHUB_SUBPATH_PATTERN =
1313 / g i t h u b \. c o m \/ ( [ ^ / ] + ) \/ ( [ ^ / ] + ) \/ t r e e \/ ( [ ^ / ] + ) \/ ( .+ ) / ;
14+ const GITHUB_REPO_REF_PATTERN =
15+ / ^ h t t p s : \/ \/ g i t h u b \. c o m \/ ( [ ^ / ] + ) \/ ( [ ^ / ] + ) \/ t r e e \/ ( [ ^ / ] + ) \/ ? $ / ;
1416const GITHUB_REPO_PATTERN = / g i t h u b \. c o m \/ ( [ ^ / ] + ) \/ ( [ ^ / ] + ) / ;
17+ const GITHUB_REPO_ROOT_PATTERN = / ^ ( h t t p s : \/ \/ g i t h u b \. c o m \/ [ ^ / ] + \/ [ ^ / ] + ) / ;
18+
19+ const UNPINNED_REF_PATTERN = / ^ ( m a i n | m a s t e r | h e a d | d e v | d e v e l o p | t r u n k | d e f a u l t ) $ / i;
20+
21+ const MAX_README_BYTES = 500_000 ;
22+
23+ type Adapter = ( typeof adapters ) [ number ] ;
1524
1625const getAdapter = ( slug : string ) => adapters . find ( ( a ) => a . slug === slug ) ;
1726
18- const getReadme = async ( repoUrl : string ) : Promise < string | undefined > => {
27+ const isCommunity = ( adapter : Adapter ) : boolean =>
28+ "community" in adapter && adapter . community === true ;
29+
30+ const isVendorOfficial = ( adapter : Adapter ) : boolean =>
31+ "vendorOfficial" in adapter && adapter . vendorOfficial === true ;
32+
33+ const getAuthor = ( adapter : Adapter ) : string | undefined =>
34+ "author" in adapter ? adapter . author : undefined ;
35+
36+ const getIssuesUrl = ( readmeUrl : string | undefined ) : string | undefined => {
37+ if ( ! readmeUrl ) {
38+ return undefined ;
39+ }
40+ const match = readmeUrl . match ( GITHUB_REPO_ROOT_PATTERN ) ;
41+ return match ? `${ match [ 1 ] } /issues` : undefined ;
42+ } ;
43+
44+ const warnUnpinned = ( adapter : Adapter , ref : string | undefined ) => {
45+ if ( ! isCommunity ( adapter ) ) {
46+ return ;
47+ }
48+ if ( ref && ! UNPINNED_REF_PATTERN . test ( ref ) ) {
49+ return ;
50+ }
51+ console . warn (
52+ `[adapters] Community adapter "${ adapter . name } " uses an unpinned README ref "${ ref ?? "<default branch>" } ". Pin to a commit SHA or tag in adapters.json to freeze content at review time.`
53+ ) ;
54+ } ;
55+
56+ const truncate = ( content : string ) : string =>
57+ content . length <= MAX_README_BYTES
58+ ? content
59+ : `${ content . slice ( 0 , MAX_README_BYTES ) } \n\n> _README truncated — view the full version on GitHub._` ;
60+
61+ const fetchGitHubReadme = async ( url : string ) : Promise < string | undefined > => {
62+ const response = await fetch ( url , {
63+ headers : { Accept : "application/vnd.github.raw+json" } ,
64+ next : { revalidate : 3600 } ,
65+ } ) ;
66+ if ( response . ok ) {
67+ return response . text ( ) ;
68+ }
69+ return undefined ;
70+ } ;
71+
72+ const getReadme = async ( adapter : Adapter ) : Promise < string | undefined > => {
73+ if ( ! adapter . readme ) {
74+ return undefined ;
75+ }
76+ const repoUrl = adapter . readme ;
77+
1978 const localMatch = repoUrl . match ( LOCAL_PACKAGE_PATTERN ) ;
2079 if ( localMatch ) {
2180 const [ , pkgPath ] = localMatch ;
2281 const filePath = join ( process . cwd ( ) , ".." , ".." , pkgPath , "README.md" ) ;
2382 try {
24- return await readFile ( filePath , "utf-8" ) ;
83+ return truncate ( await readFile ( filePath , "utf-8" ) ) ;
2584 } catch {
2685 return undefined ;
2786 }
@@ -30,32 +89,76 @@ const getReadme = async (repoUrl: string): Promise<string | undefined> => {
3089 const subpathMatch = repoUrl . match ( GITHUB_SUBPATH_PATTERN ) ;
3190 if ( subpathMatch ) {
3291 const [ , owner , repo , ref , path ] = subpathMatch ;
33- const url = `https://api.github.com/repos/${ owner } /${ repo } /readme/${ path } ?ref=${ ref } ` ;
34- const response = await fetch ( url , {
35- headers : { Accept : "application/vnd.github.raw+json" } ,
36- next : { revalidate : 3600 } ,
37- } ) ;
38- if ( response . ok ) {
39- return response . text ( ) ;
40- }
92+ warnUnpinned ( adapter , ref ) ;
93+ const content = await fetchGitHubReadme (
94+ `https://api.github.com/repos/${ owner } /${ repo } /readme/${ path } ?ref=${ ref } `
95+ ) ;
96+ return content ? truncate ( content ) : undefined ;
97+ }
98+
99+ const repoRefMatch = repoUrl . match ( GITHUB_REPO_REF_PATTERN ) ;
100+ if ( repoRefMatch ) {
101+ const [ , owner , repo , ref ] = repoRefMatch ;
102+ warnUnpinned ( adapter , ref ) ;
103+ const content = await fetchGitHubReadme (
104+ `https://api.github.com/repos/${ owner } /${ repo } /readme?ref=${ ref } `
105+ ) ;
106+ return content ? truncate ( content ) : undefined ;
41107 }
42108
43109 const repoMatch = repoUrl . match ( GITHUB_REPO_PATTERN ) ;
44110 if ( repoMatch ) {
45111 const [ , owner , repo ] = repoMatch ;
46- const url = `https://api.github.com/repos/${ owner } /${ repo } /readme` ;
47- const response = await fetch ( url , {
48- headers : { Accept : "application/vnd.github.raw+json" } ,
49- next : { revalidate : 3600 } ,
50- } ) ;
51- if ( response . ok ) {
52- return response . text ( ) ;
53- }
112+ warnUnpinned ( adapter , undefined ) ;
113+ const content = await fetchGitHubReadme (
114+ `https://api.github.com/repos/${ owner } /${ repo } /readme`
115+ ) ;
116+ return content ? truncate ( content ) : undefined ;
54117 }
55118
56119 return undefined ;
57120} ;
58121
122+ const CommunityNotice = ( { adapter } : { adapter : Adapter } ) => {
123+ if ( ! isCommunity ( adapter ) ) {
124+ return null ;
125+ }
126+ const issuesUrl = getIssuesUrl ( adapter . readme ) ;
127+ const author = getAuthor ( adapter ) ;
128+ const vendorOfficial = isVendorOfficial ( adapter ) && author ;
129+
130+ return (
131+ < div className = "mb-8 rounded-md border bg-muted/40 px-4 py-3 text-muted-foreground text-sm" >
132+ { vendorOfficial ? (
133+ < >
134+ < strong className = "text-foreground" > Vendor-official adapter</ strong > { " " }
135+ maintained by { author } , not Vercel or Chat SDK contributors. For
136+ feature requests, bug reports, and support,{ " " }
137+ </ >
138+ ) : (
139+ < >
140+ < strong className = "text-foreground" > Community adapter.</ strong > Not
141+ maintained by Vercel or Chat SDK contributors. For feature requests,
142+ bug reports, and support,{ " " }
143+ </ >
144+ ) }
145+ { issuesUrl ? (
146+ < a
147+ className = "text-primary underline hover:no-underline"
148+ href = { issuesUrl }
149+ rel = "noopener noreferrer"
150+ target = "_blank"
151+ >
152+ file an issue on the adapter's repo
153+ </ a >
154+ ) : (
155+ < span > file an issue on the adapter's repo</ span >
156+ ) }
157+ .
158+ </ div >
159+ ) ;
160+ } ;
161+
59162const AdapterPage = async ( {
60163 params,
61164} : PageProps < "/[lang]/adapters/[slug]" > ) => {
@@ -66,7 +169,7 @@ const AdapterPage = async ({
66169 notFound ( ) ;
67170 }
68171
69- const readme = await getReadme ( adapter . readme ) ;
172+ const readme = await getReadme ( adapter ) ;
70173
71174 return (
72175 < div className = "container mx-auto max-w-3xl" >
@@ -90,6 +193,7 @@ const AdapterPage = async ({
90193 < SiGithub className = "size-6" />
91194 </ a >
92195 </ div >
196+ < CommunityNotice adapter = { adapter } />
93197 < ReadmeContent > { readme } </ ReadmeContent >
94198 </ article >
95199 ) : (
@@ -102,6 +206,7 @@ const AdapterPage = async ({
102206 All Adapters
103207 </ Link >
104208 < h1 className = "mb-4 font-bold text-2xl" > { adapter . name } </ h1 >
209+ < CommunityNotice adapter = { adapter } />
105210 < p className = "text-muted-foreground" >
106211 README not available. Visit the{ " " }
107212 < a
0 commit comments