Skip to content

Commit be89afc

Browse files
fix(twoslash): forward tsModule to createTwoslasher (#1271)
1 parent 4365957 commit be89afc

2 files changed

Lines changed: 87 additions & 7 deletions

File tree

packages/twoslash/src/index.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createTransformerFactory, rendererRich } from './core'
77

88
export * from './core'
99

10-
export interface TransformerTwoslashIndexOptions extends TransformerTwoslashOptions, Pick<CreateTwoslashOptions, 'cache'> {
10+
export interface TransformerTwoslashIndexOptions extends TransformerTwoslashOptions, Pick<CreateTwoslashOptions, 'cache' | 'tsModule'> {
1111
/**
1212
* Options for the default rich renderer.
1313
*
@@ -20,13 +20,25 @@ export interface TransformerTwoslashIndexOptions extends TransformerTwoslashOpti
2020
* Factory function to create a Shiki transformer for twoslash integrations.
2121
*/
2222
export function transformerTwoslash(options: TransformerTwoslashIndexOptions = {}): ShikiTransformer {
23+
const twoslashOptions: CreateTwoslashOptions = {
24+
cache: options.cache,
25+
compilerOptions: {
26+
moduleResolution: 100 satisfies ModuleResolutionKind.Bundler,
27+
},
28+
}
29+
30+
// tsModule is a create-time option that must reach createTwoslasher directly.
31+
// It can be set at the top level or inside twoslashOptions; top level takes precedence.
32+
const tsModule = options.tsModule || options.twoslashOptions?.tsModule
33+
34+
// Only include when defined, passing `undefined` explicitly overrides the default
35+
// TypeScript module that twoslash sets internally, causing an immediate crash.
36+
if (tsModule) {
37+
twoslashOptions.tsModule = tsModule
38+
}
39+
2340
return createTransformerFactory(
24-
createTwoslasher({
25-
cache: options?.cache,
26-
compilerOptions: {
27-
moduleResolution: 100 satisfies ModuleResolutionKind.Bundler,
28-
},
29-
}),
41+
createTwoslasher(twoslashOptions),
3042
rendererRich(options.rendererRich),
3143
)(options)
3244
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { codeToHtml } from 'shiki'
2+
import ts from 'typescript'
3+
import { describe, expect, it, vi } from 'vitest'
4+
import { transformerTwoslash } from '../src'
5+
6+
// Wraps `ts` with a spy on `createLanguageService` so we can assert it was called
7+
// by the virtual TypeScript environment that twoslash creates internally.
8+
function spiedTsModule() {
9+
const createLanguageService = vi.fn((...args: Parameters<typeof ts.createLanguageService>) =>
10+
ts.createLanguageService(...args),
11+
)
12+
const tsModule = new Proxy(ts, {
13+
get(target, prop, receiver) {
14+
if (prop === 'createLanguageService')
15+
return createLanguageService
16+
return Reflect.get(target, prop, receiver)
17+
},
18+
}) as typeof ts
19+
return { tsModule, createLanguageService }
20+
}
21+
22+
describe('transformerTwoslash: tsModule forwarding', () => {
23+
const sample = 'const a: number = 1'
24+
25+
it('uses tsModule from top-level options', async () => {
26+
const { tsModule, createLanguageService } = spiedTsModule()
27+
28+
await codeToHtml(sample, {
29+
lang: 'ts',
30+
theme: 'vitesse-dark',
31+
transformers: [transformerTwoslash({ cache: false, tsModule })],
32+
})
33+
34+
expect(createLanguageService).toHaveBeenCalled()
35+
})
36+
37+
it('uses tsModule from twoslashOptions as fallback', async () => {
38+
const { tsModule, createLanguageService } = spiedTsModule()
39+
40+
await codeToHtml(sample, {
41+
lang: 'ts',
42+
theme: 'vitesse-dark',
43+
transformers: [transformerTwoslash({ cache: false, twoslashOptions: { tsModule } })],
44+
})
45+
46+
expect(createLanguageService).toHaveBeenCalled()
47+
})
48+
49+
it('top-level tsModule takes precedence over twoslashOptions.tsModule', async () => {
50+
const topLevel = spiedTsModule()
51+
const nested = spiedTsModule()
52+
53+
await codeToHtml(sample, {
54+
lang: 'ts',
55+
theme: 'vitesse-dark',
56+
transformers: [
57+
transformerTwoslash({
58+
cache: false,
59+
tsModule: topLevel.tsModule,
60+
twoslashOptions: { tsModule: nested.tsModule },
61+
}),
62+
],
63+
})
64+
65+
expect(topLevel.createLanguageService).toHaveBeenCalled()
66+
expect(nested.createLanguageService).not.toHaveBeenCalled()
67+
})
68+
})

0 commit comments

Comments
 (0)