diff --git a/docs/plugins/markdown/shiki.md b/docs/plugins/markdown/shiki.md index 0de39e20d6..60f03fcd28 100644 --- a/docs/plugins/markdown/shiki.md +++ b/docs/plugins/markdown/shiki.md @@ -673,7 +673,29 @@ body > div { ### twoslash -- Type: `boolean | TwoslashOptions` +- Type: `boolean | ShikiTwoslashOptions` + + ```ts + interface ShikiTwoslashOptions extends TransformerTwoslashOptions { + /** + * Requires adding `twoslash` to the code block explicitly to run twoslash + * @default true + */ + explicitTrigger?: RegExp | boolean + + /** + * twoslash options + */ + twoslashOptions?: TransformerTwoslashOptions['twoslashOptions'] & + VueSpecificOptions + + /** + * The options for caching resolved types + * @default true + */ + typesCache?: TwoslashTypesCache | boolean + } + ``` - Default: `false` @@ -688,7 +710,9 @@ body > div { - Also see: - [Shiki > Twoslash](https://shiki.style/packages/twoslash) - - [Twoslash > TwoslashOptions](https://github.com/twoslashes/twoslash/blob/main/packages/twoslash/src/types/options.ts#L18) + - [Twoslash > TransformerTwoslashOptions](https://github.com/shikijs/shiki/blob/main/packages/twoslash/src/types.ts#L30) + - [Twoslash > VueSpecificOptions](https://github.com/twoslashes/twoslash/blob/main/packages/twoslash-vue/src/index.ts#L36) + - [TwoslashTypesCache](https://github.com/vuepress/ecosystem/blob/main/tools/shiki-twoslash/src/node/options.ts#L47) - Example: diff --git a/docs/zh/plugins/markdown/shiki.md b/docs/zh/plugins/markdown/shiki.md index e2cfc10d82..073b736bdc 100644 --- a/docs/zh/plugins/markdown/shiki.md +++ b/docs/zh/plugins/markdown/shiki.md @@ -675,7 +675,29 @@ body > div { ### twoslash -- 类型: `boolean | TwoslashOptions` +- 类型: `boolean | ShikiTwoslashOptions` + + ```ts + interface ShikiTwoslashOptions extends TransformerTwoslashOptions { + /** + * 是否需要显式地将 `twoslash` 添加到代码块中以运行 twoslash + * @default true + */ + explicitTrigger?: RegExp | boolean + + /** + * twoslash 配置 + */ + twoslashOptions?: TransformerTwoslashOptions['twoslashOptions'] & + VueSpecificOptions + + /** + * 缓存解析后类型 + * @default true + */ + typesCache?: TwoslashTypesCache | boolean + } + ``` - 默认值: `false` @@ -690,7 +712,9 @@ body > div { - 参考: - [Shiki > Twoslash](https://shiki.style/packages/twoslash) - - [Twoslash > TwoslashOptions](https://github.com/twoslashes/twoslash/blob/main/packages/twoslash/src/types/options.ts#L18) + - [Twoslash > TransformerTwoslashOptions](https://github.com/shikijs/shiki/blob/main/packages/twoslash/src/types.ts#L30) + - [Twoslash > VueSpecificOptions](https://github.com/twoslashes/twoslash/blob/main/packages/twoslash-vue/src/index.ts#L36) + - [TwoslashTypesCache](https://github.com/vuepress/ecosystem/blob/main/tools/shiki-twoslash/src/node/options.ts#L47) - 示例: diff --git a/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts index ba9915d3ea..4e85ad1fb1 100644 --- a/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts +++ b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts @@ -1,11 +1,17 @@ import { createRequire } from 'node:module' -import type { TwoslashTransformer } from '@vuepress/shiki-twoslash' -import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki' +import type { + BundledLanguage, + BundledTheme, + HighlighterGeneric, + ShikiTransformer, +} from 'shiki' import { createHighlighter, isSpecialLang } from 'shiki' import { createSyncFn } from 'synckit' +import type { App } from 'vuepress' import { isPlainObject } from 'vuepress/shared' import type { ShikiPluginOptions } from '../../options.js' import type { ShikiResolveLang } from '../../resolveLang.js' +import { vPreTransformer } from '../../transformers/vuepressTransformers.js' import { resolveLanguage } from '../../utils.js' const require = createRequire(import.meta.url) @@ -16,16 +22,20 @@ const resolveLangSync = createSyncFn( export type ShikiLoadLang = (lang: string) => boolean -export const createShikiHighlighter = async ({ - langs = [], - langAlias = {}, - defaultLang, - shikiSetup, - ...options -}: ShikiPluginOptions = {}): Promise<{ +export const createShikiHighlighter = async ( + app: App, + { + langs = [], + langAlias = {}, + defaultLang, + shikiSetup, + ...options + }: ShikiPluginOptions = {}, + enableVPre = true, +): Promise<{ highlighter: HighlighterGeneric loadLang: ShikiLoadLang - twoslashTransformer: TwoslashTransformer + extraTransformers: ShikiTransformer[] }> => { const highlighter = await createHighlighter({ langs: [...langs, ...Object.values(langAlias)], @@ -63,19 +73,31 @@ export const createShikiHighlighter = async ({ return rawGetLanguage.call(highlighter, name) } - let twoslashTransformer: TwoslashTransformer = () => [] + const extraTransformers: ShikiTransformer[] = [] + + if (enableVPre) extraTransformers.push(vPreTransformer) if (options.twoslash) { - const { createTwoslashTransformer } = await import( - '@vuepress/shiki-twoslash' + const { createTwoslashTransformer, createFileSystemTypesCache } = + await import('@vuepress/shiki-twoslash') + + const { typesCache, ...twoslashOptions } = isPlainObject(options.twoslash) + ? options.twoslash + : {} + extraTransformers.push( + await createTwoslashTransformer({ + ...twoslashOptions, + typesCache: + typesCache === true || typeof typesCache === 'undefined' + ? createFileSystemTypesCache({ + dir: app.dir.cache('markdown/twoslash'), + }) + : typesCache, + }), ) - - twoslashTransformer = await createTwoslashTransformer({ - twoslashOptions: isPlainObject(options.twoslash) ? options.twoslash : {}, - }) } await shikiSetup?.(highlighter) - return { highlighter, loadLang, twoslashTransformer } + return { highlighter, loadLang, extraTransformers } } diff --git a/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getHighLightFunction.ts b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getHighLightFunction.ts index f8f090daf3..1997191a91 100644 --- a/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getHighLightFunction.ts +++ b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getHighLightFunction.ts @@ -1,6 +1,10 @@ import { transformerCompactLineOptions } from '@shikijs/transformers' -import type { TwoslashTransformer } from '@vuepress/shiki-twoslash' -import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki' +import type { + BundledLanguage, + BundledTheme, + HighlighterGeneric, + ShikiTransformer, +} from 'shiki' import { getTransformers, whitespaceTransformer, @@ -21,9 +25,9 @@ type MarkdownItHighlight = ( export const getHighLightFunction = ( highlighter: HighlighterGeneric, options: ShikiHighlightOptions, + extraTransformers: ShikiTransformer[] | undefined, loadLang: ShikiLoadLang, markdownFilePathGetter: MarkdownFilePathGetter, - twoslashTransformer: TwoslashTransformer = () => [], ): MarkdownItHighlight => { const transformers = getTransformers(options) @@ -44,7 +48,7 @@ export const getHighLightFunction = ( ? [transformerCompactLineOptions(attrsToLines(attrs))] : []), ...whitespaceTransformer(attrs, options.whitespace), - ...twoslashTransformer(attrs), + ...(extraTransformers ?? []), ...(options.transformers ?? []), ], ...('themes' in options diff --git a/plugins/markdown/plugin-shiki/src/node/options.ts b/plugins/markdown/plugin-shiki/src/node/options.ts index a95e2d2e08..ae21dfa640 100644 --- a/plugins/markdown/plugin-shiki/src/node/options.ts +++ b/plugins/markdown/plugin-shiki/src/node/options.ts @@ -2,7 +2,7 @@ import type { MarkdownItCollapsedLinesOptions, MarkdownItLineNumbersOptions, } from '@vuepress/highlighter-helper' -import type { TwoslashOptions } from '@vuepress/shiki-twoslash' +import type { ShikiTwoslashOptions } from '@vuepress/shiki-twoslash' import type { MarkdownItPreWrapperOptions } from './markdown/index.js' import type { ShikiHighlightOptions } from './types.js' @@ -20,5 +20,13 @@ export type ShikiPluginOptions = MarkdownItLineNumbersOptions & * * @default false */ - twoslash?: TwoslashOptions | boolean + twoslash?: + | boolean + | (ShikiTwoslashOptions & { + /** + * The options for caching resolved types + * @default true + */ + typesCache?: ShikiTwoslashOptions['typesCache'] | true + }) } diff --git a/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts b/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts index 2bf6c40af8..f771a5b0e3 100644 --- a/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts +++ b/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts @@ -47,22 +47,43 @@ export const shikiPlugin = (_options: ShikiPluginOptions = {}): Plugin => { options.twoslash = false } + /** + * Whether to enable the `v-pre` configuration of the code block + */ + let enableVPre = true + return { name: '@vuepress/plugin-shiki', + extendsMarkdownOptions: (opts) => { + /** + * Turn off the `v-pre` configuration of the code block. + */ + if (opts.vPre !== false) { + const vPre = isPlainObject(opts.vPre) ? opts.vPre : { block: true } + if (vPre.block) { + opts.vPre ??= {} + opts.vPre.block = false + } + enableVPre = vPre.block ?? true + } else { + enableVPre = false + } + }, + extendsMarkdown: async (md) => { const { preWrapper, lineNumbers, collapsedLines } = options const markdownFilePathGetter = createMarkdownFilePathGetter(md) - const { highlighter, loadLang, twoslashTransformer } = - await createShikiHighlighter(options) + const { highlighter, loadLang, extraTransformers } = + await createShikiHighlighter(app, options, enableVPre) md.options.highlight = getHighLightFunction( highlighter, options, + extraTransformers, loadLang, markdownFilePathGetter, - twoslashTransformer, ) md.use(highlightLinesPlugin) @@ -77,20 +98,6 @@ export const shikiPlugin = (_options: ShikiPluginOptions = {}): Plugin => { } }, - extendsMarkdownOptions: (opts) => { - /** - * After injecting twoslash & floating-vue, - * it is necessary to turn off the `v-pre` configuration of the code block. - */ - if (options.twoslash && opts.vPre !== false) { - const vPre = isPlainObject(opts.vPre) ? opts.vPre : { block: true } - if (vPre.block) { - opts.vPre ??= {} - opts.vPre.block = false - } - } - }, - clientConfigFile: () => prepareClientConfigFile(app, options), } } diff --git a/plugins/markdown/plugin-shiki/src/node/transformers/vuepressTransformers.ts b/plugins/markdown/plugin-shiki/src/node/transformers/vuepressTransformers.ts index 3ceb435237..b2b873fc20 100644 --- a/plugins/markdown/plugin-shiki/src/node/transformers/vuepressTransformers.ts +++ b/plugins/markdown/plugin-shiki/src/node/transformers/vuepressTransformers.ts @@ -51,3 +51,10 @@ export const emptyLineTransformer: ShikiTransformer = { }) }, } + +export const vPreTransformer: ShikiTransformer = { + name: 'vuepress:v-pre', + pre(node) { + node.properties['v-pre'] = '' + }, +} diff --git a/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts b/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts index 5f239298a5..3e4ddc04ff 100644 --- a/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts +++ b/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts @@ -8,6 +8,7 @@ import { } from '@vuepress/highlighter-helper' import MarkdownIt from 'markdown-it' import { describe, expect, it } from 'vitest' +import type { App } from 'vuepress' import type { MarkdownItPreWrapperOptions } from '../src/node/markdown/index.js' import { createMarkdownFilePathGetter, @@ -18,7 +19,7 @@ import { } from '../src/node/markdown/index.js' import type { ShikiPluginOptions } from '../src/node/options.js' -const { highlighter, loadLang } = await createShikiHighlighter() +const { highlighter, loadLang } = await createShikiHighlighter({} as App) const createMarkdown = ({ preWrapper = true, @@ -33,6 +34,7 @@ const createMarkdown = ({ md.options.highlight = getHighLightFunction( highlighter, options, + [], loadLang, markdownFilePathGetter, ) diff --git a/tools/shiki-twoslash/src/node/createFileSystemTypesCache.ts b/tools/shiki-twoslash/src/node/createFileSystemTypesCache.ts new file mode 100644 index 0000000000..6a890ac875 --- /dev/null +++ b/tools/shiki-twoslash/src/node/createFileSystemTypesCache.ts @@ -0,0 +1,36 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import type { TwoslashReturn } from 'twoslash' +import { hash as createHash } from 'vuepress/utils' +import type { TwoslashTypesCache } from './options.js' + +export interface FileSystemTypeResultCacheOptions { + /** + * The directory to store the cache files. + */ + dir: string +} + +export const createFileSystemTypesCache = ({ + dir, +}: FileSystemTypeResultCacheOptions): TwoslashTypesCache => ({ + init() { + mkdirSync(dir, { recursive: true }) + }, + read(code) { + const hash = createHash(code) + const filePath = join(dir, `${hash}.json`) + if (!existsSync(filePath)) { + return null + } + return JSON.parse( + readFileSync(filePath, { encoding: 'utf-8' }), + ) as TwoslashReturn + }, + write(code, data) { + const hash = createHash(code) + const filePath = join(dir, `${hash}.json`) + const json = JSON.stringify(data) + writeFileSync(filePath, json, { encoding: 'utf-8' }) + }, +}) diff --git a/tools/shiki-twoslash/src/node/createTwoslashTransformer.ts b/tools/shiki-twoslash/src/node/createTwoslashTransformer.ts index bda8c0aa52..ba5881ac00 100644 --- a/tools/shiki-twoslash/src/node/createTwoslashTransformer.ts +++ b/tools/shiki-twoslash/src/node/createTwoslashTransformer.ts @@ -1,25 +1,129 @@ +import process from 'node:process' +import { + createTransformerFactory, + defaultTwoslashOptions, +} from '@shikijs/twoslash/core' import type { ShikiTransformer } from 'shiki' -import { transformerTwoslashFactory } from './transformerTwoslashFactory.js' -import type { VuePressTwoslashOptions } from './types.js' +import type { TwoslashExecuteOptions, TwoslashReturn } from 'twoslash' +import { removeTwoslashNotations } from 'twoslash' +import { createTwoslasher } from 'twoslash-vue' +import { logger } from 'vuepress/utils' +import type { ShikiTwoslashOptions } from './options.js' +import { rendererFloatingVue } from './rendererFloatingVue.js' +import { resolveTypeScriptPaths } from './resolveTypeScriptPaths.js' -const TWOSLASH_REGEXP = /\btwoslash\b/ +/** + * Create a Shiki transformer for VuePress to enable twoslash integration + */ +export const createTwoslashTransformer = async ( + options: ShikiTwoslashOptions = {}, +): Promise => { + // eslint-disable-next-line no-multi-assign + const explicitTrigger = (options.explicitTrigger ??= true) + // eslint-disable-next-line no-multi-assign + const _twoslashOptions = (options.twoslashOptions ??= {}) -const vPreTransformer: ShikiTransformer = { - name: 'vuepress:v-pre', - pre(node) { - node.properties['v-pre'] = '' - }, -} + const { compilerOptions = {} } = _twoslashOptions + + const twoslashOptions = { + ...defaultTwoslashOptions(), + ..._twoslashOptions, + compilerOptions: { + baseUrl: process.cwd(), + ...compilerOptions, + path: { + ...compilerOptions.paths, + ...(await resolveTypeScriptPaths()), + }, + }, + } + const shouldThrow = + // respect user option + options.throws ?? + // in CI or production mode + (process.env.CI || process.env.NODE_ENV === 'production') -export type TwoslashTransformer = (meta: string) => ShikiTransformer[] + const onError = (error: unknown, code: string): string => { + logger.error( + `\n\n--------\nTwoslash error in code:\n--------\n${code.split(/\n/g).slice(0, 15).join('\n').trim()}\n--------\n`, + ) -export const createTwoslashTransformer = async ( - options: VuePressTwoslashOptions = {}, -): Promise => { - const transformer = await transformerTwoslashFactory(options) + if (shouldThrow) { + throw error + } else { + logger.error(error) + } + + return removeTwoslashNotations(code) + } + + const defaultTwoslashInstance = createTwoslasher(twoslashOptions) + const { typesCache } = options + let twoslashInstance = defaultTwoslashInstance + if (typesCache) { + twoslashInstance = (( + code: string, + extension?: string, + opt?: TwoslashExecuteOptions, + ): TwoslashReturn => { + const cached = typesCache.read(code) // Restore cache + if (cached) return cached + + const twoslashResult = defaultTwoslashInstance(code, extension, opt) + typesCache.write(code, twoslashResult) + return twoslashResult + }) as typeof defaultTwoslashInstance + twoslashInstance.getCacheMap = defaultTwoslashInstance.getCacheMap + + typesCache.init?.() + } + + const twoslashTransformer = createTransformerFactory(twoslashInstance)({ + langs: ['ts', 'tsx', 'js', 'jsx', 'json', 'vue'], + renderer: rendererFloatingVue(options), + onShikiError: onError, + onTwoslashError: onError, + ...options, + explicitTrigger, + twoslashOptions, + }) + + const triggerRegExp = + explicitTrigger instanceof RegExp ? explicitTrigger : /\btwoslash\b/ + + return { + name: 'vuepress:twoslash', + + ...twoslashTransformer, + + preprocess(code, preprocessOptions) { + const { transformers } = preprocessOptions + + if (transformers) { + const cleanupIndex = transformers.findIndex( + ({ name }) => name === 'vuepress:clean-up', + ) + + if (cleanupIndex >= 0) transformers.splice(cleanupIndex, 1) + + // Disable v-pre for twoslash, because we need render it with FloatingVue + if ( + !explicitTrigger || + preprocessOptions.meta?.__raw?.match(triggerRegExp) + ) { + const vPreIndex = transformers.findIndex( + ({ name }) => name === 'vuepress:v-pre', + ) + + if (vPreIndex >= 0) transformers.splice(vPreIndex, 1) + } + } + + return twoslashTransformer.preprocess!.call(this, code, preprocessOptions) + }, - return (meta = ''): ShikiTransformer[] => { - if (TWOSLASH_REGEXP.test(meta)) return [transformer] - return [vPreTransformer] + postprocess(html) { + return this.meta.twoslash ? html.replace(/\{/g, '{') : html + }, } } diff --git a/tools/shiki-twoslash/src/node/index.ts b/tools/shiki-twoslash/src/node/index.ts index efbb1f2a68..1e2c53b87d 100644 --- a/tools/shiki-twoslash/src/node/index.ts +++ b/tools/shiki-twoslash/src/node/index.ts @@ -1,5 +1,5 @@ export * from './rendererFloatingVue.js' -export * from './transformerTwoslashFactory.js' +export * from './resolveTypeScriptPaths.js' export * from './createTwoslashTransformer.js' -export * from './resolveTsPaths.js' -export type * from './types.js' +export * from './createFileSystemTypesCache.js' +export type * from './options.js' diff --git a/tools/shiki-twoslash/src/node/types.ts b/tools/shiki-twoslash/src/node/options.ts similarity index 58% rename from tools/shiki-twoslash/src/node/types.ts rename to tools/shiki-twoslash/src/node/options.ts index a09ae261ef..3c9829bd5c 100644 --- a/tools/shiki-twoslash/src/node/types.ts +++ b/tools/shiki-twoslash/src/node/options.ts @@ -1,11 +1,13 @@ import type { RendererRichOptions } from '@shikijs/twoslash' import type { TransformerTwoslashOptions } from '@shikijs/twoslash/core' +import type { TwoslashReturn } from 'twoslash' import type { VueSpecificOptions } from 'twoslash-vue' export interface TwoslashFloatingVueOptions { classCopyIgnore?: string classFloatingPanel?: string classCode?: string + classMarkdown?: string attrMarkdown?: string floatingVueTheme?: string @@ -21,21 +23,45 @@ export interface TwoslashFloatingVueRendererOptions floatingVue?: TwoslashFloatingVueOptions } -export type TwoslashOptions = TransformerTwoslashOptions['twoslashOptions'] & - VueSpecificOptions - -export interface VuePressTwoslashOptions +export interface ShikiTwoslashOptions extends TransformerTwoslashOptions, TwoslashFloatingVueRendererOptions { /** * Twoslash options */ - twoslashOptions?: TwoslashOptions - + twoslashOptions?: TransformerTwoslashOptions['twoslashOptions'] & + VueSpecificOptions /** * Requires adding `twoslash` to the code block explicitly to run twoslash * * @default true */ explicitTrigger?: TransformerTwoslashOptions['explicitTrigger'] + + /** + * The options for caching resolved types + */ + typesCache?: TwoslashTypesCache | false +} + +export interface TwoslashTypesCache { + /** + * Read cached result + * + * @param code Source code + */ + read: (code: string) => TwoslashReturn | null + + /** + * Save result to cache + * + * @param code Source code + * @param data Twoslash data + */ + write: (code: string, data: TwoslashReturn) => void + + /** + * On initialization + */ + init?: () => void } diff --git a/tools/shiki-twoslash/src/node/rendererFloatingVue.ts b/tools/shiki-twoslash/src/node/rendererFloatingVue.ts index 6774201ecd..1b1ba8f056 100644 --- a/tools/shiki-twoslash/src/node/rendererFloatingVue.ts +++ b/tools/shiki-twoslash/src/node/rendererFloatingVue.ts @@ -5,13 +5,12 @@ import { fromMarkdown } from 'mdast-util-from-markdown' import { gfmFromMarkdown } from 'mdast-util-gfm' import { defaultHandlers, toHast } from 'mdast-util-to-hast' import type { ShikiTransformerContextCommon } from 'shiki' -import type { TwoslashFloatingVueRendererOptions } from './types.js' +import type { TwoslashFloatingVueRendererOptions } from './options.js' -const vPre = (el: T): T => { +const addVPreProp = (el: T): T => { if (el.type === 'element') { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - el.properties ??= {} - el.properties['v-pre'] = '' + ;(el.properties ??= {})['v-pre'] = '' } return el } @@ -35,7 +34,7 @@ const compose = (parts: { }, content: { type: 'root', - children: [vPre(parts.popup)], + children: [addVPreProp(parts.popup)], }, children: [], }, @@ -71,6 +70,7 @@ function renderMarkdown( properties: { 'class': `language-${lang}`, 'data-ext': lang, + 'data-title': lang, 'data-highlighter': 'shiki', 'style': children[0]?.type === 'element' && @@ -93,16 +93,17 @@ function renderMarkdownInline( md: string, context?: string, ): ElementContent[] { - let str = md - if (context === 'tag:param') str = md.replace(/^([\w$-]+)/, '`$1` ') - + const str = context === 'tag:param' ? md.replace(/^([\w$-]+)/, '`$1` ') : md const children = renderMarkdown.call(this, str) + + // return the children (content) of the first paragraph if it's the only one if ( children.length === 1 && children[0].type === 'element' && children[0].tagName === 'p' ) return children[0].children + return children } @@ -113,6 +114,7 @@ export const rendererFloatingVue = ( classCopyIgnore = 'vp-copy-ignore', classFloatingPanel = 'twoslash-floating', classCode = 'vp-code', + classMarkdown = '', attrMarkdown = 'vp-content', floatingVueTheme = 'twoslash', floatingVueThemeQuery = 'twoslash-query', @@ -132,7 +134,7 @@ export const rendererFloatingVue = ( 'theme': floatingVueTheme, } - const rich = rendererRich({ + const richRenderer = rendererRich({ classExtra: classCopyIgnore, ...options, renderMarkdown, @@ -153,19 +155,19 @@ export const rendererFloatingVue = ( }, queryCompose: compose, popupDocs: { - class: 'twoslash-popup-docs', + class: `twoslash-popup-docs ${classMarkdown}`, properties: { [attrMarkdown]: '', }, }, popupDocsTags: { - class: 'twoslash-popup-docs twoslash-popup-docs-tags', + class: `twoslash-popup-docs twoslash-popup-docs-tags ${classMarkdown}`, properties: { [attrMarkdown]: '', }, }, popupError: { - class: 'twoslash-popup-error', + class: `twoslash-popup-error ${classMarkdown}`, properties: { [attrMarkdown]: '', }, @@ -205,7 +207,7 @@ export const rendererFloatingVue = ( }, content: { type: 'root', - children: [vPre(popup)], + children: [addVPreProp(popup)], }, }, ], @@ -215,5 +217,5 @@ export const rendererFloatingVue = ( }, }) - return rich + return richRenderer } diff --git a/tools/shiki-twoslash/src/node/resolveTsPaths.ts b/tools/shiki-twoslash/src/node/resolveTypeScriptPaths.ts similarity index 52% rename from tools/shiki-twoslash/src/node/resolveTsPaths.ts rename to tools/shiki-twoslash/src/node/resolveTypeScriptPaths.ts index a3a66610f1..89291f9985 100644 --- a/tools/shiki-twoslash/src/node/resolveTsPaths.ts +++ b/tools/shiki-twoslash/src/node/resolveTypeScriptPaths.ts @@ -2,30 +2,36 @@ import fs from 'node:fs/promises' import path from 'node:path' import process from 'node:process' -export async function resolveTsPaths(): Promise< - Record | undefined -> { +export type TypeScriptPaths = Record + +export async function resolveTypeScriptPaths(): Promise { const tsconfigPath = path.join(process.cwd(), 'tsconfig.json') + try { const tsconfig = JSON.parse(await fs.readFile(tsconfigPath, 'utf-8')) as { - compilerOptions?: { paths?: Record; baseUrl?: string } + compilerOptions?: { + paths?: Record + baseUrl?: string + } } - const paths = tsconfig.compilerOptions?.paths ?? undefined const baseUrl = tsconfig.compilerOptions?.baseUrl + const paths = tsconfig.compilerOptions?.paths ?? null if (baseUrl && paths) { const dirname = path.join(process.cwd(), baseUrl) + for (const key in paths) { const value = paths[key] - if (Array.isArray(value)) - paths[key] = value.map((v) => path.resolve(dirname, v)) - else paths[key] = [path.resolve(dirname, value)] + + paths[key] = Array.isArray(value) + ? value.map((v) => path.resolve(dirname, v)) + : [path.resolve(dirname, value)] } } return paths } catch { - return undefined + return null } } diff --git a/tools/shiki-twoslash/src/node/transformerTwoslashFactory.ts b/tools/shiki-twoslash/src/node/transformerTwoslashFactory.ts deleted file mode 100644 index f23b05c5e6..0000000000 --- a/tools/shiki-twoslash/src/node/transformerTwoslashFactory.ts +++ /dev/null @@ -1,97 +0,0 @@ -import process from 'node:process' -import { - createTransformerFactory, - defaultHoverInfoProcessor, - defaultTwoslashOptions, -} from '@shikijs/twoslash/core' -import type { ShikiTransformer } from 'shiki' -import { removeTwoslashNotations } from 'twoslash' -import { createTwoslasher } from 'twoslash-vue' -import { logger } from 'vuepress/utils' -import { rendererFloatingVue } from './rendererFloatingVue.js' -import { resolveTsPaths } from './resolveTsPaths.js' -import type { VuePressTwoslashOptions } from './types.js' - -/** - * Create a Shiki transformer for VuePress to enable twoslash integration - */ -export const transformerTwoslashFactory = async ( - options: VuePressTwoslashOptions = {}, -): Promise => { - const { explicitTrigger = true } = options - - const onError = (error: unknown, code: string): string => { - const isCI = process.env.CI - const isDev = - typeof process !== 'undefined' && process.env.NODE_ENV === 'development' - const shouldThrow = - (options.throws ?? isCI ?? !isDev) && options.throws !== false - logger.error( - `\n\n--------\nTwoslash error in code:\n--------\n${code.split(/\n/g).slice(0, 15).join('\n').trim()}\n--------\n`, - ) - if (shouldThrow) { - throw error - } else { - logger.error(error) - } - return removeTwoslashNotations(code) - } - options.processHoverInfo ??= defaultHoverInfoProcessor - - const paths = await resolveTsPaths() - const { compilerOptions = {}, ...twoslashOptions } = - options.twoslashOptions ?? {} - if (paths) { - compilerOptions.paths = { - ...compilerOptions.paths, - ...paths, - } - } - options.twoslashOptions = { - ...defaultTwoslashOptions(), - ...twoslashOptions, - compilerOptions: { - baseUrl: process.cwd(), - ...compilerOptions, - }, - } - - const twoslash = createTransformerFactory( - createTwoslasher(options.twoslashOptions), - )({ - langs: ['ts', 'tsx', 'js', 'jsx', 'json', 'vue'], - renderer: rendererFloatingVue(options), - onTwoslashError: onError, - onShikiError: onError, - ...options, - explicitTrigger, - }) - - const trigger = - explicitTrigger instanceof RegExp ? explicitTrigger : /\btwoslash\b/ - - return { - ...twoslash, - name: '@shiki/vuepress-twoslash', - preprocess(code, opt) { - const cleanup = opt.transformers?.find( - (i) => i.name === 'vuepress:clean-up', - ) - if (cleanup) - opt.transformers?.splice(opt.transformers.indexOf(cleanup), 1) - - // Disable v-pre for twoslash, because we need render it with FloatingVue - if (!explicitTrigger || opt.meta?.__raw?.match(trigger)) { - const vPre = opt.transformers?.find((i) => i.name === 'vuepress:v-pre') - if (vPre) opt.transformers?.splice(opt.transformers.indexOf(vPre), 1) - } - - return twoslash.preprocess!.call(this, code, opt) - }, - postprocess(html) { - if (this.meta.twoslash) return html.replace(/\{/g, '{') - - return html - }, - } -}