2024-06-13 13:09:27 +00:00
import { MarkdownWriter } from '@/scripts/utils'
import {
ApiDocumentedItem ,
ApiFunction ,
ApiItem ,
ApiModel ,
ApiPackage ,
ApiVariable ,
ExcerptToken ,
ExcerptTokenKind ,
} from '@microsoft/api-extractor-model'
import { assert } from '@tldraw/utils'
export class TldrawApiModel extends ApiModel {
private reactComponents = new Set < ApiItem > ( )
private reactComponentProps = new Set < ApiItem > ( )
2024-06-17 14:47:22 +00:00
nonBlockingErrors : Error [ ] = [ ]
2024-06-13 13:09:27 +00:00
async preprocessReactComponents() {
for ( const packageModel of this . members ) {
assert ( packageModel instanceof ApiPackage )
if ( packageModel . name !== 'tldraw' ) continue
const entrypoint = packageModel . entryPoints [ 0 ]
for ( const member of entrypoint . members ) {
assert ( member instanceof ApiDocumentedItem )
if ( ! member . tsdocComment ) continue
if ( ! member . tsdocComment . modifierTagSet . hasTagName ( '@react' ) ) continue
this . reactComponents . add ( member )
try {
const props = this . getReactPropsItem ( member )
if ( props instanceof ApiDocumentedItem && props . tsdocComment ) {
const markdown = await MarkdownWriter . docNodeToMarkdown (
props ,
props . tsdocComment . summarySection
)
if ( markdown . trim ( ) ) {
2024-06-17 14:47:22 +00:00
this . nonBlockingError (
2024-06-13 13:09:27 +00:00
props ,
"Component props should not contain documentation as it won't be included in the docs site. Add it to the component instead."
)
}
}
if ( props ) this . reactComponentProps . add ( props )
2024-06-17 14:47:22 +00:00
} catch ( e : any ) {
this . nonBlockingErrors . push ( e )
2024-06-13 13:09:27 +00:00
}
}
}
}
resolveToken ( origin : ApiItem , token : ExcerptToken ) {
const apiItemResult = this . resolveDeclarationReference ( token . canonicalReference ! , origin )
if ( apiItemResult . errorMessage ) {
this . error ( origin , apiItemResult . errorMessage )
}
return apiItemResult . resolvedApiItem !
}
2024-06-17 14:47:22 +00:00
tryResolveToken ( origin : ApiItem , token : ExcerptToken ) {
const apiItemResult = this . resolveDeclarationReference ( token . canonicalReference ! , origin )
if ( apiItemResult . errorMessage ) {
return null
}
return apiItemResult . resolvedApiItem !
}
2024-06-13 13:09:27 +00:00
getReactPropsItem ( component : ApiItem ) : ApiItem | null {
if ( component instanceof ApiFunction ) {
if ( component . parameters . length === 0 ) return null
this . assert (
component ,
component . parameters . length === 1 ,
` Expected 1 parameter for @react component `
)
const propsParam = component . parameters [ 0 ]
const tokens = propsParam . parameterTypeExcerpt . spannedTokens
if ( tokens . length === 1 && tokens [ 0 ] . kind === 'Reference' ) {
return this . resolveToken ( component , tokens [ 0 ] )
} else if (
tokens . length === 2 &&
tokens [ 0 ] . kind === 'Reference' &&
tokens [ 1 ] . text . startsWith ( '<' )
) {
return this . resolveToken ( component , tokens [ 0 ] )
}
2024-06-17 14:47:22 +00:00
this . nonBlockingError (
2024-06-13 13:09:27 +00:00
component ,
` Expected props parameter to be a simple reference. Rewrite this to use a \` ${ component . displayName } Props \` interface. \ nFound: ${ propsParam . parameterTypeExcerpt . text } `
)
2024-06-17 14:47:22 +00:00
return null
2024-06-13 13:09:27 +00:00
} else if ( component instanceof ApiVariable ) {
const tokens = component . variableTypeExcerpt . spannedTokens
if (
tokens . length === 5 &&
tokens [ 0 ] . text === 'import("react").' &&
tokens [ 1 ] . text === 'NamedExoticComponent' &&
tokens [ 2 ] . text === '<' &&
tokens [ 3 ] . kind === ExcerptTokenKind . Reference &&
tokens [ 4 ] . text === '>'
) {
return this . resolveToken ( component , tokens [ 3 ] )
}
if (
tokens . length === 4 &&
tokens [ 0 ] . text === 'React.NamedExoticComponent' &&
tokens [ 1 ] . text === '<' &&
tokens [ 2 ] . kind === ExcerptTokenKind . Reference &&
tokens [ 3 ] . text === '>'
) {
return this . resolveToken ( component , tokens [ 2 ] )
}
if (
tokens . length === 8 &&
tokens [ 0 ] . text === 'React.ForwardRefExoticComponent' &&
tokens [ 1 ] . text === '<' &&
tokens [ 2 ] . kind === ExcerptTokenKind . Reference &&
tokens [ 3 ] . text === ' & ' &&
tokens [ 4 ] . text === 'React.RefAttributes' &&
tokens [ 5 ] . text === '<' &&
tokens [ 6 ] . kind === ExcerptTokenKind . Reference &&
tokens [ 7 ] . text === '>>'
) {
return this . resolveToken ( component , tokens [ 2 ] )
}
if ( component . variableTypeExcerpt . text === 'import("react").NamedExoticComponent<object>' ) {
// this is a `memo` component with no props
return null
}
2024-06-17 14:47:22 +00:00
this . nonBlockingError (
2024-06-13 13:09:27 +00:00
component ,
` Expected a simple props interface for react component. Got: ${ component . variableTypeExcerpt . text } `
)
2024-06-17 14:47:22 +00:00
return null
2024-06-13 13:09:27 +00:00
} else {
2024-06-17 14:47:22 +00:00
this . nonBlockingError ( component , ` Unknown item kind for @react component: ${ component . kind } ` )
return null
2024-06-13 13:09:27 +00:00
}
}
isComponent ( item : ApiItem ) : boolean {
return this . reactComponents . has ( item )
}
isComponentProps ( item : ApiItem ) : boolean {
return this . reactComponentProps . has ( item )
}
2024-06-17 14:47:22 +00:00
private createError ( item : ApiItem , message : string ) {
2024-06-13 13:09:27 +00:00
const suffix =
'_fileUrlPath' in item && typeof item . _fileUrlPath === 'string'
? ` \ nin ${ item . _fileUrlPath } `
: ''
2024-06-17 14:47:22 +00:00
return new Error ( ` ${ item . displayName } : ${ message } ${ suffix } ` )
}
nonBlockingError ( item : ApiItem , message : string ) {
this . nonBlockingErrors . push ( this . createError ( item , message ) )
}
throwEncounteredErrors() {
if ( this . nonBlockingErrors . length > 0 ) {
throw new Error ( this . nonBlockingErrors . map ( ( e ) = > ( e as any ) . message ) . join ( '\n\n' ) )
}
}
error ( item : ApiItem , message : string ) : never {
throw this . createError ( item , message )
2024-06-13 13:09:27 +00:00
}
assert ( item : ApiItem , condition : unknown , message : string ) : asserts condition {
if ( ! condition ) {
this . error ( item , message )
}
}
}