Skip to content

Commit c93d9b0

Browse files
trivikrghostdevv
andauthored
refactor: parallelize docs rendering and shiki setup (#2382)
Co-authored-by: Willow (GHOST) <git@willow.sh>
1 parent 9222620 commit c93d9b0

File tree

4 files changed

+183
-94
lines changed

4 files changed

+183
-94
lines changed

server/utils/docs/render.ts

Lines changed: 49 additions & 35 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

132-
if (signatures.length > 0) {
133-
const signatureCode = signatures.join('\n')
134-
const highlightedSignature = await highlightCodeBlock(signatureCode, 'typescript')
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+
144+
if (highlightedSignature) {
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
@@ -179,15 +186,30 @@ async function renderJsDocTags(tags: JsDocTag[], symbolLookup: SymbolLookup): Pr
179186
const deprecated = tags.find(t => t.kind === 'deprecated')
180187
const see = tags.filter(t => t.kind === 'see')
181188

189+
const deprecatedMessagePromise = deprecated?.doc
190+
? renderMarkdown(deprecated.doc.replace(/\n/g, ' '), symbolLookup)
191+
: null
192+
const examplePromises = examples.map(async example => {
193+
if (!example.doc) return ''
194+
const langMatch = example.doc.match(/```(\w+)?/)
195+
const lang = langMatch?.[1] || 'typescript'
196+
const code = example.doc.replace(/```\w*\n?/g, '').trim()
197+
return highlightCodeBlock(code, lang)
198+
})
199+
200+
const [renderedDeprecatedMessage, ...renderedExamples] = await Promise.all([
201+
deprecatedMessagePromise,
202+
...examplePromises,
203+
])
204+
182205
// Deprecated warning
183206
if (deprecated) {
184207
lines.push(`<div class="docs-deprecated">`)
185208
lines.push(`<strong>Deprecated</strong>`)
186-
if (deprecated.doc) {
209+
if (renderedDeprecatedMessage) {
187210
// We remove new lines because they look weird when rendered into the deprecated block
188211
// 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>`)
212+
lines.push(`<div class="docs-deprecated-message">${renderedDeprecatedMessage}</div>`)
191213
}
192214
lines.push(`</div>`)
193215
}
@@ -218,18 +240,10 @@ async function renderJsDocTags(tags: JsDocTag[], symbolLookup: SymbolLookup): Pr
218240
}
219241

220242
// Examples (with syntax highlighting)
221-
if (examples.length > 0) {
243+
if (examples.length > 0 && renderedExamples.some(Boolean)) {
222244
lines.push(`<div class="docs-examples">`)
223245
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-
}
246+
lines.push(...renderedExamples.filter(Boolean))
233247
lines.push(`</div>`)
234248
}
235249

server/utils/docs/text.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,13 @@ 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)
137-
result = result.replace(`__CODE_BLOCK_${i}__`, highlighted)
138-
}
134+
const highlightedCodeBlocks = await Promise.all(
135+
codeBlockData.map(({ lang, code }) => highlightCodeBlock(code, lang)),
136+
)
137+
138+
highlightedCodeBlocks.forEach((highlighted, i) => {
139+
result = result.replace(`__CODE_BLOCK_${i}__`, () => highlighted)
140+
})
139141

140142
return result
141143
}

server/utils/shiki.ts

Lines changed: 66 additions & 54 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,65 +19,76 @@ function replaceThemeColors(
1819
}
1920

2021
export async function getShikiHighlighter(): Promise<HighlighterCore> {
21-
if (!highlighter) {
22-
highlighter = await createHighlighterCore({
23-
themes: [
24-
import('@shikijs/themes/github-dark'),
25-
import('@shikijs/themes/github-light').then(t =>
26-
replaceThemeColors(t.default ?? t, {
27-
'#22863A': '#227436', // green
28-
'#E36209': '#BA4D02', // orange
29-
'#D73A49': '#CD3443', // red
30-
'#B31D28': '#AC222F', // red
31-
}),
32-
),
33-
],
34-
langs: [
35-
// Core web languages
36-
import('@shikijs/langs/javascript'),
37-
import('@shikijs/langs/typescript'),
38-
import('@shikijs/langs/json'),
39-
import('@shikijs/langs/jsonc'),
40-
import('@shikijs/langs/html'),
41-
import('@shikijs/langs/css'),
42-
import('@shikijs/langs/scss'),
43-
import('@shikijs/langs/less'),
22+
if (highlighter) {
23+
return highlighter
24+
}
25+
26+
highlighterPromise ??= createHighlighterCore({
27+
themes: [
28+
import('@shikijs/themes/github-dark'),
29+
import('@shikijs/themes/github-light').then(t =>
30+
replaceThemeColors(t.default ?? t, {
31+
'#22863A': '#227436', // green
32+
'#E36209': '#BA4D02', // orange
33+
'#D73A49': '#CD3443', // red
34+
'#B31D28': '#AC222F', // red
35+
}),
36+
),
37+
],
38+
langs: [
39+
// Core web languages
40+
import('@shikijs/langs/javascript'),
41+
import('@shikijs/langs/typescript'),
42+
import('@shikijs/langs/json'),
43+
import('@shikijs/langs/jsonc'),
44+
import('@shikijs/langs/html'),
45+
import('@shikijs/langs/css'),
46+
import('@shikijs/langs/scss'),
47+
import('@shikijs/langs/less'),
4448

45-
// Frameworks
46-
import('@shikijs/langs/vue'),
47-
import('@shikijs/langs/jsx'),
48-
import('@shikijs/langs/tsx'),
49-
import('@shikijs/langs/svelte'),
50-
import('@shikijs/langs/astro'),
51-
import('@shikijs/langs/glimmer-js'),
52-
import('@shikijs/langs/glimmer-ts'),
49+
// Frameworks
50+
import('@shikijs/langs/vue'),
51+
import('@shikijs/langs/jsx'),
52+
import('@shikijs/langs/tsx'),
53+
import('@shikijs/langs/svelte'),
54+
import('@shikijs/langs/astro'),
55+
import('@shikijs/langs/glimmer-js'),
56+
import('@shikijs/langs/glimmer-ts'),
5357

54-
// Shell/CLI
55-
import('@shikijs/langs/bash'),
56-
import('@shikijs/langs/shell'),
58+
// Shell/CLI
59+
import('@shikijs/langs/bash'),
60+
import('@shikijs/langs/shell'),
5761

58-
// Config/Data formats
59-
import('@shikijs/langs/yaml'),
60-
import('@shikijs/langs/toml'),
61-
import('@shikijs/langs/xml'),
62-
import('@shikijs/langs/markdown'),
62+
// Config/Data formats
63+
import('@shikijs/langs/yaml'),
64+
import('@shikijs/langs/toml'),
65+
import('@shikijs/langs/xml'),
66+
import('@shikijs/langs/markdown'),
6367

64-
// Other languages
65-
import('@shikijs/langs/diff'),
66-
import('@shikijs/langs/sql'),
67-
import('@shikijs/langs/graphql'),
68-
import('@shikijs/langs/python'),
69-
import('@shikijs/langs/rust'),
70-
import('@shikijs/langs/go'),
71-
],
72-
langAlias: {
73-
gjs: 'glimmer-js',
74-
gts: 'glimmer-ts',
75-
},
76-
engine: createJavaScriptRegexEngine(),
68+
// Other languages
69+
import('@shikijs/langs/diff'),
70+
import('@shikijs/langs/sql'),
71+
import('@shikijs/langs/graphql'),
72+
import('@shikijs/langs/python'),
73+
import('@shikijs/langs/rust'),
74+
import('@shikijs/langs/go'),
75+
],
76+
langAlias: {
77+
gjs: 'glimmer-js',
78+
gts: 'glimmer-ts',
79+
},
80+
engine: createJavaScriptRegexEngine(),
81+
})
82+
.then(createdHighlighter => {
83+
highlighter = createdHighlighter
84+
return createdHighlighter
7785
})
78-
}
79-
return highlighter
86+
.catch(error => {
87+
highlighterPromise = null
88+
throw error
89+
})
90+
91+
return highlighterPromise
8092
}
8193

8294
/**

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)