Skip to content

Commit 4958a31

Browse files
committed
handle aliases
1 parent fa175d3 commit 4958a31

File tree

3 files changed

+134
-46
lines changed

3 files changed

+134
-46
lines changed

docs/generate.ts

Lines changed: 105 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ import * as typedoc from 'typedoc'
55
import packageJson from '../packages/remix/package.json' with { type: 'json' }
66
import * as prettier from 'prettier'
77

8-
// TODO:
9-
// - Handle sub-modules: `import { createCookie } from 'remix/cookie'`
10-
// - Handle alias re-exports: `export { openFile as getFile } `
11-
128
//#region Types
139

1410
// Function parameter or Class property
@@ -104,10 +100,15 @@ async function main() {
104100
let { comments, apisToDocument } = createLookupMaps(project)
105101

106102
// Prefer `remix` package exports over other package exports
107-
getDuplicateAPIS(apisToDocument).forEach((dup) => apisToDocument.delete(dup))
103+
getDuplicateAPIs(apisToDocument).forEach((name) => apisToDocument.delete(name))
104+
105+
// Remove aliased APIs and only document the canonicals
106+
getAliasedAPIs(comments).forEach((name) => apisToDocument.delete(name))
108107

109108
// Parse JSDocs into DocumentedAPI instances we can write out to markdown
110-
let documentedAPIs = [...apisToDocument].map((name) => getDocumentedAPI(comments.get(name)!))
109+
let documentedAPIs = [...apisToDocument].map((name) =>
110+
getDocumentedAPI(name, comments.get(name)!),
111+
)
111112

112113
// Write out docs
113114
await writeMarkdownFiles(documentedAPIs)
@@ -135,6 +136,9 @@ async function loadTypedocJson(): Promise<typedoc.ProjectReflection> {
135136
name: packageJson.name,
136137
entryPoints: ['./packages/*'],
137138
entryPointStrategy: 'packages',
139+
packageOptions: {
140+
blockTags: [...typedoc.OptionDefaults.blockTags, '@alias'],
141+
},
138142
})
139143
let reflection = await app.convert()
140144
invariant(reflection, 'Failed to generate TypeDoc reflection from source code')
@@ -155,7 +159,6 @@ async function loadTypedocJson(): Promise<typedoc.ProjectReflection> {
155159
function createLookupMaps(reflection: typedoc.ProjectReflection): Maps {
156160
let comments = new Map<string, typedoc.Reflection>()
157161
let apisToDocument = new Set<string>()
158-
let referenceTargetMap = new Map<string, number>()
159162

160163
// Reflections we want to traverse through to find documented APIs
161164
let traverseKinds = new Set<typedoc.ReflectionKind>([
@@ -172,7 +175,7 @@ function createLookupMaps(reflection: typedoc.ProjectReflection): Maps {
172175

173176
return { comments, apisToDocument }
174177

175-
function recurse(node: typedoc.Reflection) {
178+
function recurse(node: typedoc.Reflection, alias?: string) {
176179
node.traverse((child) => {
177180
if (
178181
cliArgs.module &&
@@ -183,27 +186,25 @@ function createLookupMaps(reflection: typedoc.ProjectReflection): Maps {
183186
return
184187
}
185188

186-
comments.set(child.getFriendlyFullName(), child)
189+
let apiName = alias || child.getFriendlyFullName()
190+
comments.set(apiName, child)
187191

188-
let indent = ' '.repeat(child.getFriendlyFullName().split('.').length - 1)
192+
let indent = ' '.repeat(apiName.split('.').length - 1)
189193
let logApi = (suffix: string) =>
190194
log(
191195
[
192196
`${indent}[${typedoc.ReflectionKind[child.kind]}]`,
193-
child.getFriendlyFullName(),
197+
apiName,
194198
`(${child.id})`,
195199
`(${suffix})`,
196200
].join(' '),
197201
)
198202

199203
// Reference types are aliases - stick them off into a separate map for post-processing
200-
if (
201-
child.kind === typedoc.ReflectionKind.Reference &&
202-
'_target' in child &&
203-
typeof child._target === 'number'
204-
) {
205-
logApi(`reference to ${child._target}`)
206-
referenceTargetMap.set(child.getFriendlyFullName(), child._target)
204+
if (child.isReference()) {
205+
logApi(`reference to ${child.getTargetReflectionDeep().getFriendlyFullName()}`)
206+
let ref = child.getTargetReflection()
207+
recurse(ref, child.getFriendlyFullName())
207208
return
208209
}
209210

@@ -215,7 +216,7 @@ function createLookupMaps(reflection: typedoc.ProjectReflection): Maps {
215216

216217
// Grab APIs with JSDoc comments that we should generate docs for
217218
if (child.comment && (!cliArgs.api || child.name === cliArgs.api)) {
218-
apisToDocument.add(child.getFriendlyFullName())
219+
apisToDocument.add(apiName)
219220
logApi(`commenting`)
220221
}
221222

@@ -228,13 +229,13 @@ function createLookupMaps(reflection: typedoc.ProjectReflection): Maps {
228229
}
229230

230231
// Deduplicate APIs that are exported from multiple packages, preferring the remix package
231-
function getDuplicateAPIS(apisToDocument: Set<string>): Set<string> {
232+
function getDuplicateAPIs(apisToDocument: Set<string>): Set<string> {
232233
let apisByName = new Map<string, string[]>()
233234
let duplicates = new Set<string>()
234235

235236
// Group APIs by short name
236237
for (let fullName of apisToDocument) {
237-
let apiName = fullName.split('.').slice(0, -1)[0]
238+
let apiName = getApiNameFromFullName(fullName)
238239
apisByName.set(apiName, [...(apisByName.get(apiName) || []), fullName])
239240
}
240241

@@ -255,24 +256,50 @@ function getDuplicateAPIS(apisToDocument: Set<string>): Set<string> {
255256
}
256257
} else if (!remixAPI && fullNames.length > 1) {
257258
// Multiple non-remix packages export this API
258-
warn(`Multiple packages export ${apiName} but none is remix: ${fullNames.join(', ')}`)
259+
warn(`Multiple packages export ${apiName}: ${fullNames.join(', ')}`)
259260
}
260261
}
261262

262263
return duplicates
263264
}
264265

266+
function getAliasedAPIs(comments: Map<string, typedoc.Reflection>): Set<string> {
267+
let aliasedAPIs = new Set<string>()
268+
269+
comments.forEach((reflection, name) => {
270+
let parts = name.split('.')
271+
let apiName = parts.pop()
272+
let alias = reflection.comment?.blockTags.find((tag) => tag.tag === '@alias')
273+
if (alias) {
274+
// The canonical API should include `@alias`
275+
// We will generate a markdown doc for the canonical API, and not the aliases
276+
// The canonical doc will list the aliases names
277+
let aliasName = alias.content.reduce((acc, part) => {
278+
invariant(part.kind === 'text')
279+
return acc + part.text
280+
}, '')
281+
if (apiName !== aliasName) {
282+
let aliasFullName = [...parts, aliasName].join('.')
283+
log(`Preferring canonical API \`${name}\` over alias \`${aliasFullName}\``)
284+
aliasedAPIs.add(aliasFullName)
285+
}
286+
}
287+
})
288+
289+
return aliasedAPIs
290+
}
291+
265292
//#region DocumentedAPI
266293

267294
// Convert a typedoc reflection for a given node into a documentable instance
268-
function getDocumentedAPI(node: typedoc.Reflection): DocumentedAPI {
295+
function getDocumentedAPI(fullName: string, node: typedoc.Reflection): DocumentedAPI {
269296
try {
270297
if (node.isSignature()) {
271-
return getDocumentedFunction(node)
298+
return getDocumentedFunction(fullName, node)
272299
}
273300

274301
if (node.isDeclaration() && node.kind === typedoc.ReflectionKind.Class) {
275-
return getDocumentedClass(node)
302+
return getDocumentedClass(fullName, node)
276303
}
277304

278305
throw new Error(`Unsupported documented API kind: ${typedoc.ReflectionKind[node.kind]}`)
@@ -286,21 +313,27 @@ function getDocumentedAPI(node: typedoc.Reflection): DocumentedAPI {
286313
}
287314
}
288315

289-
function getDocumentedFunction(node: typedoc.SignatureReflection): DocumentedFunction {
290-
let method = getMethod(node)
316+
function getDocumentedFunction(
317+
fullName: string,
318+
node: typedoc.SignatureReflection,
319+
): DocumentedFunction {
320+
let method = getMethod(fullName, node)
291321
invariant(method, `Failed to get method for function: ${node.getFriendlyFullName()}`)
292322
return {
293323
type: 'function',
294-
path: getDocumentedApiPath(node),
295-
aliases: undefined,
324+
path: getDocumentedApiPath(fullName),
325+
aliases: getDocumentedApiAliases(node.comment!),
296326
example: node.comment?.getTag('@example')?.content
297327
? processComment(node.comment.getTag('@example')!.content)
298328
: undefined,
299329
...method,
300330
} satisfies DocumentedFunction
301331
}
302332

303-
function getDocumentedClass(node: typedoc.DeclarationReflection): DocumentedClass {
333+
function getDocumentedClass(
334+
fullName: string,
335+
node: typedoc.DeclarationReflection,
336+
): DocumentedClass {
304337
let constructor: Method | undefined
305338
let properties: ParameterOrProperty[] = []
306339
let methods: Method[] = []
@@ -312,7 +345,7 @@ function getDocumentedClass(node: typedoc.DeclarationReflection): DocumentedClas
312345
signature,
313346
`Missing constructor signature for class: ${node.getFriendlyFullName()}`,
314347
)
315-
constructor = getMethod(signature)
348+
constructor = getMethod(fullName, signature)
316349
} else if (child.kind === typedoc.ReflectionKind.Property) {
317350
let property = getParameterOrProperty(child)
318351
if (property) {
@@ -326,7 +359,7 @@ function getDocumentedClass(node: typedoc.DeclarationReflection): DocumentedClas
326359
} else if (child.kind === typedoc.ReflectionKind.Method) {
327360
let signature = child.getAllSignatures()[0]
328361
invariant(`Missing method signature for class: ${child.getFriendlyFullName()}`)
329-
let method = getMethod(signature)
362+
let method = getMethod(fullName, signature)
330363
if (method) {
331364
methods.push(method)
332365
}
@@ -340,19 +373,35 @@ function getDocumentedClass(node: typedoc.DeclarationReflection): DocumentedClas
340373

341374
return {
342375
type: 'class',
343-
aliases: undefined,
376+
aliases: getDocumentedApiAliases(node.comment!),
344377
example: undefined,
345-
path: getDocumentedApiPath(node),
346-
name: node.name,
378+
path: getDocumentedApiPath(fullName),
379+
name: getApiNameFromFullName(fullName),
347380
description: getDocumentedApiDescription(node.comment!),
348381
constructor,
349382
properties,
350383
methods,
351384
}
352385
}
353386

354-
function getDocumentedApiPath(node: typedoc.Reflection): string {
355-
let nameParts = node.getFriendlyFullName().split('.')
387+
function getDocumentedApiAliases(typedocComment: typedoc.Comment): string[] | undefined {
388+
let tags = typedocComment.getTags('@alias')
389+
if (!tags || tags.length === 0) {
390+
return undefined
391+
}
392+
return tags.map((tag) => {
393+
return tag.content.reduce((acc, part) => {
394+
invariant(
395+
part.kind === 'text',
396+
`Invalid @alias tag content: ${typedocComment.getTags('@alias').join(', ')}`,
397+
)
398+
return acc + part.text
399+
}, '')
400+
})
401+
}
402+
403+
function getDocumentedApiPath(fullName: string): string {
404+
let nameParts = fullName.split('.')
356405
return (
357406
nameParts
358407
.map((s) => s.replace(/^@remix-run\//g, ''))
@@ -369,7 +418,7 @@ function getDocumentedApiDescription(typedocComment: typedoc.Comment): string {
369418
return description
370419
}
371420

372-
function getMethod(node: typedoc.SignatureReflection): Method | undefined {
421+
function getMethod(fullName: string, node: typedoc.SignatureReflection): Method | undefined {
373422
let parameters: ParameterOrProperty[] = []
374423
node.traverse((child) => {
375424
// Only process params, not type params (generics)
@@ -400,7 +449,7 @@ function getMethod(node: typedoc.SignatureReflection): Method | undefined {
400449
}
401450

402451
return {
403-
name: node.name,
452+
name: getApiNameFromFullName(fullName),
404453
signature,
405454
description: node.comment?.summary ? processComment(node.comment.summary) : '',
406455
parameters,
@@ -489,14 +538,19 @@ function processComment(parts: typedoc.CommentDisplayPart[]): string {
489538
target && target instanceof typedoc.Reflection,
490539
`Missing/invalid target for @link content: ${part.text}`,
491540
)
492-
let path = getDocumentedApiPath(target).replace(/\.md$/, '')
541+
let path = getDocumentedApiPath(target.getFriendlyFullName()).replace(/\.md$/, '')
493542
let href = `${cliArgs.websiteDocsPath}/${path}`
494543
text = `[\`${part.text}\`](${href})`
495544
}
496545
return acc + text
497546
}, '')
498547
}
499548

549+
function getApiNameFromFullName(fullName: string): string {
550+
return fullName.split('.').slice(-1)[0]
551+
}
552+
553+
//#endregion
500554
//#region Markdown
501555

502556
async function writeMarkdownFiles(comments: DocumentedAPI[]) {
@@ -534,9 +588,7 @@ const pre = async (content: string, lang = 'ts') => {
534588

535589
async function getFunctionMarkdown(comment: DocumentedFunction): Promise<string> {
536590
return [
537-
`---\ntitle: ${comment.name}\n---`,
538-
h1(comment.name),
539-
h2('Summary', comment.description),
591+
...getCommonMarkdown(comment),
540592
h2('Signature', await pre(comment.signature)),
541593
comment.example
542594
? h2(
@@ -556,9 +608,7 @@ async function getFunctionMarkdown(comment: DocumentedFunction): Promise<string>
556608

557609
async function getClassMarkdown(comment: DocumentedClass): Promise<string> {
558610
return [
559-
`---\ntitle: ${comment.name}\n---`,
560-
h1(comment.name),
561-
h2('Summary', comment.description),
611+
...getCommonMarkdown(comment),
562612
comment.example ? h2('Example', comment.example) : undefined,
563613
comment.constructor
564614
? h2(
@@ -595,6 +645,15 @@ async function getClassMarkdown(comment: DocumentedClass): Promise<string> {
595645
.join('\n\n')
596646
}
597647

648+
function getCommonMarkdown(comment: DocumentedFunction | DocumentedClass): (string | undefined)[] {
649+
return [
650+
`---\ntitle: ${comment.name}\n---`,
651+
h1(comment.name),
652+
h2('Summary', comment.description),
653+
comment.aliases ? h2('Aliases', comment.aliases.join(', ')) : undefined,
654+
].filter(Boolean)
655+
}
656+
598657
//#region utils
599658

600659
function log(...args: unknown[]) {

0 commit comments

Comments
 (0)