Skip to content

Commit ed78b73

Browse files
committed
refactor: parallelize docs rendering and Shiki setup
- Render doc sections, symbols, markdown blocks, and examples concurrently - Cache in-flight Shiki highlighter initialization to avoid duplicate work - Add ordering tests for parallel docs rendering
1 parent eb4d862 commit ed78b73

4 files changed

Lines changed: 129 additions & 39 deletions

File tree

server/utils/docs/render.ts

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,14 @@ export async function renderDocNodes(
5757
symbolLookup: SymbolLookup,
5858
): Promise<string> {
5959
const grouped = groupMergedByKind(symbols)
60-
const sections: string[] = []
61-
62-
for (const kind of KIND_DISPLAY_ORDER) {
60+
const sectionPromises = KIND_DISPLAY_ORDER.map(async kind => {
6361
const kindSymbols = grouped[kind]
64-
if (!kindSymbols || kindSymbols.length === 0) continue
62+
if (!kindSymbols || kindSymbols.length === 0) return ''
63+
return renderKindSection(kind, kindSymbols, symbolLookup)
64+
})
6565

66-
sections.push(await renderKindSection(kind, kindSymbols, symbolLookup))
67-
}
68-
69-
return sections.join('\n')
66+
const sections = await Promise.all(sectionPromises)
67+
return sections.filter(Boolean).join('\n')
7068
}
7169

7270
/**
@@ -79,13 +77,13 @@ async function renderKindSection(
7977
): Promise<string> {
8078
const title = KIND_TITLES[kind] || kind
8179
const lines: string[] = []
80+
const renderedSymbols = await Promise.all(
81+
symbols.map(symbol => renderMergedSymbol(symbol, symbolLookup)),
82+
)
8283

8384
lines.push(`<section class="docs-section" id="section-${kind}">`)
8485
lines.push(`<h2 class="docs-section-title">${title}</h2>`)
85-
86-
for (const symbol of symbols) {
87-
lines.push(await renderMergedSymbol(symbol, symbolLookup))
88-
}
86+
lines.push(...renderedSymbols)
8987

9088
lines.push(`</section>`)
9189

@@ -129,9 +127,21 @@ async function renderMergedSymbol(
129127
.map(n => getNodeSignature(n))
130128
.filter(Boolean) as string[]
131129

130+
const description = symbol.jsDoc?.doc?.trim()
131+
const signaturePromise =
132+
signatures.length > 0 ? highlightCodeBlock(signatures.join('\n'), 'typescript') : null
133+
const descriptionPromise = description ? renderMarkdown(description, symbolLookup) : null
134+
const jsDocTagsPromise =
135+
symbol.jsDoc?.tags && symbol.jsDoc.tags.length > 0
136+
? renderJsDocTags(symbol.jsDoc.tags, symbolLookup)
137+
: null
138+
const [highlightedSignature, renderedDescription, renderedJsDocTags] = await Promise.all([
139+
signaturePromise,
140+
descriptionPromise,
141+
jsDocTagsPromise,
142+
])
143+
132144
if (signatures.length > 0) {
133-
const signatureCode = signatures.join('\n')
134-
const highlightedSignature = await highlightCodeBlock(signatureCode, 'typescript')
135145
lines.push(`<div class="docs-signature">${highlightedSignature}</div>`)
136146

137147
if (symbol.nodes.length > MAX_OVERLOAD_SIGNATURES) {
@@ -141,16 +151,13 @@ async function renderMergedSymbol(
141151
}
142152

143153
// Description
144-
if (symbol.jsDoc?.doc) {
145-
const description = symbol.jsDoc.doc.trim()
146-
lines.push(
147-
`<div class="docs-description">${await renderMarkdown(description, symbolLookup)}</div>`,
148-
)
154+
if (renderedDescription) {
155+
lines.push(`<div class="docs-description">${renderedDescription}</div>`)
149156
}
150157

151158
// JSDoc tags
152-
if (symbol.jsDoc?.tags && symbol.jsDoc.tags.length > 0) {
153-
lines.push(await renderJsDocTags(symbol.jsDoc.tags, symbolLookup))
159+
if (renderedJsDocTags) {
160+
lines.push(renderedJsDocTags)
154161
}
155162

156163
// Type-specific members
@@ -178,16 +185,29 @@ async function renderJsDocTags(tags: JsDocTag[], symbolLookup: SymbolLookup): Pr
178185
const examples = tags.filter(t => t.kind === 'example')
179186
const deprecated = tags.find(t => t.kind === 'deprecated')
180187
const see = tags.filter(t => t.kind === 'see')
188+
const deprecatedMessagePromise = deprecated?.doc
189+
? renderMarkdown(deprecated.doc.replace(/\n/g, ' '), symbolLookup)
190+
: null
191+
const examplePromises = examples.map(async example => {
192+
if (!example.doc) return ''
193+
const langMatch = example.doc.match(/```(\w+)?/)
194+
const lang = langMatch?.[1] || 'typescript'
195+
const code = example.doc.replace(/```\w*\n?/g, '').trim()
196+
return highlightCodeBlock(code, lang)
197+
})
198+
const [renderedDeprecatedMessage, renderedExamples] = await Promise.all([
199+
deprecatedMessagePromise,
200+
Promise.all(examplePromises),
201+
])
181202

182203
// Deprecated warning
183204
if (deprecated) {
184205
lines.push(`<div class="docs-deprecated">`)
185206
lines.push(`<strong>Deprecated</strong>`)
186-
if (deprecated.doc) {
207+
if (renderedDeprecatedMessage) {
187208
// We remove new lines because they look weird when rendered into the deprecated block
188209
// I think markdown is actually supposed to collapse single new lines automatically but this function doesn't do that so if that changes remove this
189-
const renderedMessage = await renderMarkdown(deprecated.doc.replace(/\n/g, ' '), symbolLookup)
190-
lines.push(`<div class="docs-deprecated-message">${renderedMessage}</div>`)
210+
lines.push(`<div class="docs-deprecated-message">${renderedDeprecatedMessage}</div>`)
191211
}
192212
lines.push(`</div>`)
193213
}
@@ -221,15 +241,7 @@ async function renderJsDocTags(tags: JsDocTag[], symbolLookup: SymbolLookup): Pr
221241
if (examples.length > 0) {
222242
lines.push(`<div class="docs-examples">`)
223243
lines.push(`<h4>Example${examples.length > 1 ? 's' : ''}</h4>`)
224-
for (const example of examples) {
225-
if (example.doc) {
226-
const langMatch = example.doc.match(/```(\w+)?/)
227-
const lang = langMatch?.[1] || 'typescript'
228-
const code = example.doc.replace(/```\w*\n?/g, '').trim()
229-
const highlighted = await highlightCodeBlock(code, lang)
230-
lines.push(highlighted)
231-
}
232-
}
244+
lines.push(...renderedExamples.filter(Boolean))
233245
lines.push(`</div>`)
234246
}
235247

server/utils/docs/text.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,12 @@ export async function renderMarkdown(text: string, symbolLookup: SymbolLookup):
131131
.replace(/\n/g, '<br>')
132132

133133
// Highlight and restore code blocks
134-
for (let i = 0; i < codeBlockData.length; i++) {
135-
const { lang, code } = codeBlockData[i]!
136-
const highlighted = await highlightCodeBlock(code, lang)
134+
const highlightedCodeBlocks = await Promise.all(
135+
codeBlockData.map(({ lang, code }) => highlightCodeBlock(code, lang)),
136+
)
137+
138+
for (let i = 0; i < highlightedCodeBlocks.length; i++) {
139+
const highlighted = highlightedCodeBlocks[i]!
137140
result = result.replace(`__CODE_BLOCK_${i}__`, highlighted)
138141
}
139142

server/utils/shiki.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createHighlighterCore, type HighlighterCore } from 'shiki/core'
33
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
44

55
let highlighter: HighlighterCore | null = null
6+
let highlighterPromise: Promise<HighlighterCore> | null = null
67

78
function replaceThemeColors(
89
theme: ThemeRegistration,
@@ -18,8 +19,12 @@ function replaceThemeColors(
1819
}
1920

2021
export async function getShikiHighlighter(): Promise<HighlighterCore> {
21-
if (!highlighter) {
22-
highlighter = await createHighlighterCore({
22+
if (highlighter) {
23+
return highlighter
24+
}
25+
26+
if (!highlighterPromise) {
27+
highlighterPromise = createHighlighterCore({
2328
themes: [
2429
import('@shikijs/themes/github-dark'),
2530
import('@shikijs/themes/github-light').then(t =>
@@ -75,8 +80,17 @@ export async function getShikiHighlighter(): Promise<HighlighterCore> {
7580
},
7681
engine: createJavaScriptRegexEngine(),
7782
})
83+
.then(createdHighlighter => {
84+
highlighter = createdHighlighter
85+
return createdHighlighter
86+
})
87+
.catch(error => {
88+
highlighterPromise = null
89+
throw error
90+
})
7891
}
79-
return highlighter
92+
93+
return highlighterPromise
8094
}
8195

8296
/**

test/unit/server/utils/docs/render.spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,37 @@ function createClassSymbol(classDef: DenoDocNode['classDef']): MergedSymbol {
2121
}
2222
}
2323

24+
function createFunctionSymbol(name: string): MergedSymbol {
25+
const node: DenoDocNode = {
26+
name,
27+
kind: 'function',
28+
functionDef: {
29+
params: [],
30+
returnType: { repr: 'void', kind: 'keyword', keyword: 'void' },
31+
},
32+
}
33+
34+
return {
35+
name,
36+
kind: 'function',
37+
nodes: [node],
38+
}
39+
}
40+
41+
function createInterfaceSymbol(name: string): MergedSymbol {
42+
const node: DenoDocNode = {
43+
name,
44+
kind: 'interface',
45+
interfaceDef: {},
46+
}
47+
48+
return {
49+
name,
50+
kind: 'interface',
51+
nodes: [node],
52+
}
53+
}
54+
2455
describe('issue #1943 - class getters separated from methods', () => {
2556
it('renders getters under a "Getters" heading, not "Methods"', async () => {
2657
const symbol = createClassSymbol({
@@ -131,3 +162,33 @@ describe('issue #1943 - class getters separated from methods', () => {
131162
expect(html).toContain('static get instance')
132163
})
133164
})
165+
166+
describe('renderDocNodes ordering', () => {
167+
it('preserves kind display order while rendering sections in parallel', async () => {
168+
const html = await renderDocNodes(
169+
[createInterfaceSymbol('Config'), createFunctionSymbol('run')],
170+
new Map(),
171+
)
172+
173+
const functionsIndex = html.indexOf('id="section-function"')
174+
const interfacesIndex = html.indexOf('id="section-interface"')
175+
176+
expect(functionsIndex).toBeGreaterThanOrEqual(0)
177+
expect(interfacesIndex).toBeGreaterThanOrEqual(0)
178+
expect(functionsIndex).toBeLessThan(interfacesIndex)
179+
})
180+
181+
it('preserves symbol order within a section while rendering symbols in parallel', async () => {
182+
const html = await renderDocNodes(
183+
[createFunctionSymbol('alpha'), createFunctionSymbol('beta')],
184+
new Map(),
185+
)
186+
187+
const alphaIndex = html.indexOf('id="function-alpha"')
188+
const betaIndex = html.indexOf('id="function-beta"')
189+
190+
expect(alphaIndex).toBeGreaterThanOrEqual(0)
191+
expect(betaIndex).toBeGreaterThanOrEqual(0)
192+
expect(alphaIndex).toBeLessThan(betaIndex)
193+
})
194+
})

0 commit comments

Comments
 (0)