29044867dd
This PR adds the docs app back into the tldraw monorepo. ## Deploying We'll want to update our deploy script to update the SOURCE_SHA to the newest release sha... and then deploy the docs pulling api.json files from that release. We _could_ update the docs on every push to main, but we don't have to unless something has changed. Right now there's no automated deployments from this repo. ## Side effects To make this one work, I needed to update the lock file. This might be ok (new year new lock file), and everything builds as expected, though we may want to spend some time with our scripts to be sure that things are all good. I also updated our prettier installation, which decided to add trailing commas to every generic type. Which is, I suppose, [correct behavior](https://github.com/prettier/prettier-vscode/issues/955)? But that caused diffs in every file, which is unfortunate. ### Change Type - [x] `internal` — Any other changes that don't affect the published package[^2]
427 lines
12 KiB
TypeScript
427 lines
12 KiB
TypeScript
import {
|
|
ApiClass,
|
|
ApiConstructSignature,
|
|
ApiConstructor,
|
|
ApiDeclaredItem,
|
|
ApiDocumentedItem,
|
|
ApiEnum,
|
|
ApiFunction,
|
|
ApiInterface,
|
|
ApiItem,
|
|
ApiItemKind,
|
|
ApiMethod,
|
|
ApiMethodSignature,
|
|
ApiNamespace,
|
|
ApiProperty,
|
|
ApiPropertySignature,
|
|
ApiReadonlyMixin,
|
|
ApiReleaseTagMixin,
|
|
ApiStaticMixin,
|
|
ApiTypeAlias,
|
|
ApiVariable,
|
|
Excerpt,
|
|
ReleaseTag,
|
|
} from '@microsoft/api-extractor-model'
|
|
import { MarkdownWriter, formatWithPrettier, getPath, getSlug } from '../utils'
|
|
|
|
type Result = { markdown: string; keywords: string[] }
|
|
|
|
const REPO_URL = 'https://github.com/tldraw/tldraw/blob/main/'
|
|
|
|
const date = new Intl.DateTimeFormat('en-US', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
}).format(new Date())
|
|
|
|
export async function getApiMarkdown(categoryName: string, item: ApiItem, j: number) {
|
|
const result: Result = { markdown: '', keywords: [] }
|
|
const toc: Result = { markdown: '', keywords: [] }
|
|
const membersResult: Result = { markdown: '', keywords: [] }
|
|
|
|
if (item.members) {
|
|
const constructors = []
|
|
const properties = []
|
|
const methods = []
|
|
for (const member of item.members) {
|
|
switch (member.kind) {
|
|
case ApiItemKind.Constructor:
|
|
case ApiItemKind.ConstructSignature:
|
|
constructors.push(member)
|
|
break
|
|
case ApiItemKind.Variable:
|
|
case ApiItemKind.Property:
|
|
case ApiItemKind.PropertySignature:
|
|
properties.push(member)
|
|
break
|
|
case ApiItemKind.Method:
|
|
case ApiItemKind.Function:
|
|
case ApiItemKind.MethodSignature:
|
|
methods.push(member)
|
|
|
|
break
|
|
case ApiItemKind.EnumMember:
|
|
case ApiItemKind.Class:
|
|
case ApiItemKind.TypeAlias:
|
|
case ApiItemKind.Interface:
|
|
// TODO: document these
|
|
break
|
|
default:
|
|
throw new Error(`Unknown member kind: ${member.kind} ${member.displayName}`)
|
|
}
|
|
}
|
|
|
|
const constructorResult: Result = { markdown: '', keywords: [] }
|
|
const propertiesResult: Result = { markdown: '', keywords: [] }
|
|
const methodsResult: Result = { markdown: '', keywords: [] }
|
|
|
|
if (constructors.length) {
|
|
for (const member of constructors) {
|
|
await addMarkdownForMember(constructorResult, member)
|
|
addHorizontalRule(constructorResult)
|
|
}
|
|
addMarkdown(membersResult, constructorResult.markdown)
|
|
}
|
|
|
|
if (properties.length) {
|
|
addMarkdown(toc, `- [Properties](#properties)\n`)
|
|
addMarkdown(propertiesResult, `## Properties\n\n`)
|
|
for (const member of properties) {
|
|
const slug = getSlug(member)
|
|
addMarkdown(toc, ` - [${member.displayName}](#${slug})\n`)
|
|
await addMarkdownForMember(propertiesResult, member)
|
|
addHorizontalRule(propertiesResult)
|
|
}
|
|
addMarkdown(membersResult, propertiesResult.markdown)
|
|
}
|
|
|
|
if (methods.length) {
|
|
addMarkdown(toc, `- [Methods](#methods)\n`)
|
|
addMarkdown(methodsResult, `## Methods\n\n`)
|
|
for (const member of methods) {
|
|
const slug = getSlug(member)
|
|
addMarkdown(toc, ` - [${member.displayName}](#${slug})\n`)
|
|
await addMarkdownForMember(methodsResult, member)
|
|
addHorizontalRule(methodsResult)
|
|
}
|
|
addMarkdown(membersResult, methodsResult.markdown)
|
|
}
|
|
}
|
|
|
|
await addFrontmatter(result, item, categoryName, j)
|
|
|
|
if (toc.markdown.length) {
|
|
result.markdown += `<details class="article__table-of-contents">\n\t<summary>Table of contents</summary>\n`
|
|
addMarkdown(result, toc.markdown)
|
|
result.markdown += `</details>\n\n`
|
|
}
|
|
|
|
addTags(result, item)
|
|
|
|
await addDocComment(result, item)
|
|
|
|
addReferences(result, item)
|
|
addLinkToSource(result, item)
|
|
|
|
if (membersResult.markdown.length) {
|
|
addHorizontalRule(result)
|
|
addMarkdown(result, membersResult.markdown)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/* --------------------- Helpers -------------------- */
|
|
|
|
function addMarkdown(result: Result, markdown: string) {
|
|
result.markdown += markdown
|
|
}
|
|
|
|
async function addMarkdownForMember(result: Result, member: ApiItem) {
|
|
if (member.displayName.startsWith('_')) return
|
|
addMemberName(result, member)
|
|
addTags(result, member)
|
|
await addDocComment(result, member)
|
|
addReferences(result, member)
|
|
addLinkToSource(result, member)
|
|
}
|
|
|
|
async function addFrontmatter(
|
|
result: Result,
|
|
member: ApiItem,
|
|
categoryName: string,
|
|
order: number
|
|
) {
|
|
let description = ''
|
|
if (member instanceof ApiDocumentedItem && member.tsdocComment) {
|
|
const comment = await MarkdownWriter.docNodeToMarkdown(
|
|
member,
|
|
member.tsdocComment.summarySection
|
|
)
|
|
// only up to the first newline
|
|
description = comment.trim().split('\n')[0].replace(/:/g, '')
|
|
}
|
|
|
|
let kw = ''
|
|
|
|
if (result.keywords.length) {
|
|
kw += `\nkeywords:`
|
|
for (const k of result.keywords) {
|
|
if (k.startsWith('_')) continue
|
|
kw += `\n - ${k.trim().split('\n')[0]}`
|
|
}
|
|
}
|
|
|
|
result.markdown += `---
|
|
title: ${member.displayName}
|
|
status: published
|
|
description: ${description}
|
|
category: ${categoryName}
|
|
group: ${member.kind}
|
|
author: api
|
|
date: ${date}
|
|
order: ${order}
|
|
sourceUrl: ${'_fileUrlPath' in member ? member._fileUrlPath : ''}${kw}
|
|
---
|
|
`
|
|
}
|
|
|
|
function addHorizontalRule(result: Result) {
|
|
result.markdown += `---\n\n`
|
|
}
|
|
|
|
function addMemberName(result: Result, member: ApiItem) {
|
|
if (member.kind === 'Constructor') {
|
|
result.markdown += `### Constructor\n\n`
|
|
return
|
|
}
|
|
|
|
if (!member.displayName) return
|
|
result.markdown += `### \`${member.displayName}${member.kind === 'Method' ? '()' : ''}\`\n\n`
|
|
}
|
|
|
|
async function addDocComment(result: Result, member: ApiItem) {
|
|
if (!(member instanceof ApiDocumentedItem)) {
|
|
return
|
|
}
|
|
|
|
if (member.tsdocComment) {
|
|
result.markdown += await MarkdownWriter.docNodeToMarkdown(
|
|
member,
|
|
member.tsdocComment.summarySection
|
|
)
|
|
|
|
const exampleBlocks = member.tsdocComment.customBlocks.filter(
|
|
(block) => block.blockTag.tagNameWithUpperCase === '@EXAMPLE'
|
|
)
|
|
|
|
if (exampleBlocks.length) {
|
|
result.markdown += `\n\n`
|
|
result.markdown += `##### Example\n\n`
|
|
for (const example of exampleBlocks) {
|
|
result.markdown += await MarkdownWriter.docNodeToMarkdown(member, example.content)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
member instanceof ApiVariable ||
|
|
member instanceof ApiTypeAlias ||
|
|
member instanceof ApiProperty ||
|
|
member instanceof ApiPropertySignature ||
|
|
member instanceof ApiClass ||
|
|
member instanceof ApiFunction ||
|
|
member instanceof ApiInterface ||
|
|
member instanceof ApiEnum ||
|
|
member instanceof ApiNamespace ||
|
|
member instanceof ApiMethod
|
|
) {
|
|
result.markdown += `<ApiHeading>Signature</ApiHeading>\n\n`
|
|
result.markdown += await typeExcerptToMarkdown(member.excerpt, {
|
|
kind: member.kind,
|
|
})
|
|
result.markdown += `\n\n`
|
|
}
|
|
|
|
if (
|
|
member instanceof ApiMethod ||
|
|
member instanceof ApiMethodSignature ||
|
|
member instanceof ApiConstructor ||
|
|
member instanceof ApiConstructSignature ||
|
|
member instanceof ApiFunction
|
|
) {
|
|
if (!member.parameters.length) {
|
|
return
|
|
} else {
|
|
result.markdown += `<ApiHeading>Parameters</ApiHeading>\n\n`
|
|
result.markdown += '<ParametersTable>\n\n'
|
|
for (const param of member.parameters) {
|
|
result.markdown += '<ParametersTableRow>\n'
|
|
result.markdown += '<ParametersTableName>\n\n'
|
|
result.markdown += `\`${param.name}\`\n\n`
|
|
result.markdown += `</ParametersTableName>\n`
|
|
result.markdown += `<ParametersTableDescription>\n\n`
|
|
result.markdown += await typeExcerptToMarkdown(param.parameterTypeExcerpt, {
|
|
kind: 'ParameterType',
|
|
printWidth: 60,
|
|
})
|
|
result.markdown += `\n\n`
|
|
if (param.tsdocParamBlock) {
|
|
result.markdown += await MarkdownWriter.docNodeToMarkdown(
|
|
member,
|
|
param.tsdocParamBlock.content
|
|
)
|
|
}
|
|
result.markdown += `\n\n</ParametersTableDescription>\n`
|
|
result.markdown += `</ParametersTableRow>\n`
|
|
}
|
|
result.markdown += '</ParametersTable>\n\n'
|
|
}
|
|
|
|
if (!(member instanceof ApiConstructor)) {
|
|
result.markdown += `<ApiHeading>Returns</ApiHeading>\n\n`
|
|
result.markdown += await typeExcerptToMarkdown(member.returnTypeExcerpt, {
|
|
kind: 'ReturnType',
|
|
})
|
|
result.markdown += `\n\n`
|
|
if (member.tsdocComment && member.tsdocComment.returnsBlock) {
|
|
result.markdown += await MarkdownWriter.docNodeToMarkdown(
|
|
member,
|
|
member.tsdocComment.returnsBlock.content
|
|
)
|
|
}
|
|
}
|
|
} else if (
|
|
member instanceof ApiVariable ||
|
|
member instanceof ApiTypeAlias ||
|
|
member instanceof ApiProperty ||
|
|
member instanceof ApiPropertySignature ||
|
|
member instanceof ApiClass ||
|
|
member instanceof ApiInterface ||
|
|
member instanceof ApiEnum ||
|
|
member instanceof ApiNamespace
|
|
) {
|
|
const params = member.tsdocComment?.params
|
|
if (params && params.count > 0) {
|
|
result.markdown += `<ApiHeading>Parameters</ApiHeading>\n\n`
|
|
result.markdown += '<ParametersTable>\n\n'
|
|
for (const block of params.blocks) {
|
|
result.markdown += '<ParametersTableRow>\n'
|
|
result.markdown += '<ParametersTableName>\n\n'
|
|
result.markdown += `\`${block.parameterName}\`\n\n`
|
|
result.markdown += `</ParametersTableName>\n`
|
|
result.markdown += `<ParametersTableDescription>\n\n`
|
|
result.markdown += await MarkdownWriter.docNodeToMarkdown(member, block.content)
|
|
result.markdown += `\n\n</ParametersTableDescription>\n`
|
|
result.markdown += `</ParametersTableRow>\n`
|
|
}
|
|
result.markdown += '</ParametersTable>\n\n'
|
|
}
|
|
} else {
|
|
throw new Error('unknown member kind: ' + member.kind)
|
|
}
|
|
}
|
|
|
|
async function typeExcerptToMarkdown(
|
|
excerpt: Excerpt,
|
|
{ kind, printWidth }: { kind: ApiItemKind | 'ReturnType' | 'ParameterType'; printWidth?: number }
|
|
) {
|
|
let code = ''
|
|
for (const token of excerpt.spannedTokens) {
|
|
code += token.text
|
|
}
|
|
|
|
code = code.replace(/^export /, '')
|
|
code = code.replace(/^declare /, '')
|
|
|
|
switch (kind) {
|
|
case ApiItemKind.CallSignature:
|
|
case ApiItemKind.EntryPoint:
|
|
case ApiItemKind.EnumMember:
|
|
case ApiItemKind.Function:
|
|
case ApiItemKind.Model:
|
|
case ApiItemKind.Namespace:
|
|
case ApiItemKind.None:
|
|
case ApiItemKind.Package:
|
|
case ApiItemKind.TypeAlias:
|
|
code = await formatWithPrettier(code, { printWidth })
|
|
break
|
|
case 'ReturnType':
|
|
case 'ParameterType':
|
|
code = await formatWithPrettier(`type X = () =>${code}`, { printWidth })
|
|
if (!code.startsWith('type X = () =>')) {
|
|
throw Error()
|
|
}
|
|
code = code = code.replace(/^type X = \(\) =>[ \n]/, '')
|
|
break
|
|
case ApiItemKind.Class:
|
|
case ApiItemKind.Enum:
|
|
case ApiItemKind.Interface:
|
|
code = await formatWithPrettier(`${code} {}`, { printWidth })
|
|
break
|
|
case ApiItemKind.Constructor:
|
|
case ApiItemKind.ConstructSignature:
|
|
case ApiItemKind.IndexSignature:
|
|
case ApiItemKind.Method:
|
|
case ApiItemKind.MethodSignature:
|
|
case ApiItemKind.Property:
|
|
case ApiItemKind.PropertySignature:
|
|
case ApiItemKind.Variable:
|
|
code = await formatWithPrettier(`class X { ${code} }`, { printWidth })
|
|
if (!(code.startsWith('class X {\n') && code.endsWith('\n}'))) {
|
|
throw Error()
|
|
}
|
|
code = code.slice('class X {\n'.length, -'\n}'.length)
|
|
code = code.replace(/^ {2}/gm, '')
|
|
break
|
|
default:
|
|
throw Error()
|
|
}
|
|
|
|
return ['```ts', code, '```'].join('\n')
|
|
}
|
|
|
|
function addTags(result: Result, member: ApiItem) {
|
|
const tags = []
|
|
if (ApiReleaseTagMixin.isBaseClassOf(member)) {
|
|
tags.push(ReleaseTag[member.releaseTag])
|
|
}
|
|
if (ApiStaticMixin.isBaseClassOf(member) && member.isStatic) {
|
|
tags.push('static')
|
|
}
|
|
if (ApiReadonlyMixin.isBaseClassOf(member) && member.isReadonly) {
|
|
tags.push('readonly')
|
|
}
|
|
tags.push(member.kind.toLowerCase())
|
|
result.markdown += `<Small>${tags.join(' ')}</Small>\n\n`
|
|
}
|
|
|
|
function addReferences(result: Result, member: ApiItem) {
|
|
if (!(member instanceof ApiDeclaredItem)) return
|
|
const references = new Set<string>()
|
|
|
|
member.excerptTokens.forEach((token) => {
|
|
if (token.kind !== 'Reference') return
|
|
const apiItemResult = member
|
|
.getAssociatedModel()!
|
|
.resolveDeclarationReference(token.canonicalReference!, member)
|
|
if (apiItemResult.errorMessage) {
|
|
return
|
|
}
|
|
const apiItem = apiItemResult.resolvedApiItem!
|
|
const url = `/gen/${getPath(apiItem)}`
|
|
references.add(`[${token.text}](${url})`)
|
|
})
|
|
|
|
if (references.size) {
|
|
result.markdown += `<ApiHeading>References</ApiHeading>\n\n`
|
|
result.markdown += Array.from(references).join(', ') + '\n\n'
|
|
}
|
|
}
|
|
|
|
function addLinkToSource(result: Result, member: ApiItem) {
|
|
if ('_fileUrlPath' in member && member._fileUrlPath) {
|
|
result.markdown += `<ApiHeading>Source</ApiHeading>\n\n`
|
|
result.markdown += `[${member._fileUrlPath}](${REPO_URL}${member._fileUrlPath})\n\n`
|
|
}
|
|
}
|