diff --git "a/docs/notes/theme/guide/\344\273\243\347\240\201/twoslash.md" "b/docs/notes/theme/guide/\344\273\243\347\240\201/twoslash.md" index 310e0d9c8..d4e533660 100644 --- "a/docs/notes/theme/guide/\344\273\243\347\240\201/twoslash.md" +++ "b/docs/notes/theme/guide/\344\273\243\347\240\201/twoslash.md" @@ -1,12 +1,12 @@ --- title: Two Slash -author: pengzhanbo icon: material-symbols:experiment-outline createTime: 2024/03/06 11:46:49 -permalink: /guide/markdown/experiment/ +permalink: /guide/markdown/twoslash/ +outline: [2, 4] --- -## twoslash +## 概述 为代码块添加支持 [TypeScript TwoSlash](https://www.typescriptlang.org/dev/twoslash/) 支持。 在代码块内提供内联类型提示。 @@ -14,9 +14,7 @@ permalink: /guide/markdown/experiment/ 该功能由 [shiki](https://shiki.style/) 和 [@shikijs/twoslash](https://shiki.style/packages/twoslash) 提供支持, 并整合在 [@vuepress-plume/plugin-shikiji](https://github.com/pengzhanbo/vuepress-theme-plume/tree/main/plugins/plugin-shikiji) 中。 -:::important -从 `vuepress@2.0.0-rc.12` 开始,不再需要对 `@vuepress/markdown` 进行额外的 hack 操作, -因此,现在你可以安全的使用这项功能了! +::: important __twoslash__ 是一个高级功能,您需要熟练掌握 [TypeScript](https://www.typescriptlang.org/) 的知识,并且了解 [twoslash 语法](https://twoslash.netlify.app/)。 ::: ::: warning @@ -29,26 +27,26 @@ permalink: /guide/markdown/experiment/ 可能某一个 `twoslash` 的代码块编译耗时就需要额外再等待 `500ms` 以上。 但不必担心 markdown 文件热更新时的编译耗时,主题针对 代码高亮编译 耗时做了优化,即使 单个 markdown 文件 -中包含多个 代码块,主题也仅会对 **有变更的代码块** 进行编译,因此热更新的速度依然非常快。 +中包含多个 代码块,主题也仅会对 __有变更的代码块__ 进行编译,因此热更新的速度依然非常快。 ::: -### 概述 - [twoslash](https://twoslash.netlify.app/) 是一种 `javascript` 和 `typescript` 标记语言。 你可以编写一个代码示例来描述整个 `javascript` 项目。 -`twoslash` 将 **双斜杠** 作为 代码示例的预处理器。 +`twoslash` 将 __双斜杠注释__ (`//`) 视为 代码示例的预处理器。 `twoslash` 使用与文本编辑器相同的编译器 API 来提供类型驱动的悬停信息、准确的错误和类型标注。 -### 功能预览 +## 功能预览 + +将鼠标悬停在 __变量__ 或 __函数__ 上查看效果: -````md ```ts twoslash import { getHighlighterCore } from 'shiki/core' const highlighter = await getHighlighterCore({}) // ^? +// // @log: Custom log message const a = 1 @@ -58,41 +56,114 @@ const b = 1 const c = 1 // @annotate: Custom annotation message ``` -```` -将鼠标悬停在 `highlighter` 变量上查看效果: +## 配置 + +### 启用功能 + +在 主题配置中,启用 `twoslash` 选项。 + +::: code-tabs +@tab .vuepress/config.ts + +```ts +export default defineUserConfig({ + theme: plumeTheme({ + plugins: { + shiki: { twoslash: true }, + }, + }), +}) +``` +::: + +### 从 `node_modules` 中导入类型文件 + +__twoslash__ 最大的特点就是对代码块进行类型编译,默认支持从项目的 `node_modules` 中导入类型文件。 + +例如,如果您需要使用 `express`的类型提示,你需要在项目中安装 `@types/express` 依赖: + +::: npm-to + +```sh +npm i -D @types/express +``` + +::: + +然后,可以在 代码块中使用 `express` 的类型,如下所示: + +```` md ```ts twoslash -import { getHighlighterCore } from 'shiki/core' +import express from 'express' -const highlighter = await getHighlighterCore({}) -// ^? +const app = express() +``` +```` -// @log: Custom log message -const a = 1 -// @error: Custom error message -const b = 1 -// @warn: Custom warning message -const c = 1 -// @annotate: Custom annotation message +### 导入本地类型文件 + +对于导入本地类型文件,由于代码块中的代码在编译时,并不容易确定代码的真实路径,我们很难直观的在 代码块中通过 +__相对路径__ 导入类型文件。 + +#### 从 `tsconfig.json` 中读取路径映射 + +主题支持从项目根目录下的 `tsconfig.json`,读取 `compilerOptions.paths` 中的路径映射,来解决这个问题。 + +假设你的项目的 `tsconfig.json` 配置如下: + +::: code-tabs +@tab tsconfig.json + +```json +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} ``` -::: tip -`twoslash` 仅支持 `typescript` 和 `vue` 的 代码块。 ::: -### 开启功能 +你可以直接在 代码块中使用 `@/` 开头的路径,导入 `src` 目录下的类型文件,如下所示: -在 主题配置中,启用 `twoslash` 选项。 +````md +```ts twoslash +import type { Foo } from '@/foo' + +const foo: Foo = 1 +``` +```` + +#### 从 `shiki.twoslash` 中读取路径映射 + +你可以在 `shiki.twoslash` 中配置 `compilerOptions`,来解决这个问题,如下所示: ::: code-tabs @tab .vuepress/config.ts ```ts +import path from 'node:path' + export default defineUserConfig({ theme: plumeTheme({ plugins: { - shiki: { twoslash: true }, + shiki: { + twoslash: { + compilerOptions: { // [!code hl:8] + paths: { + // 相对于工作目录 `process.cwd()` + '@/*': ['./src/*'], + // 使用绝对路径 + '@@/*': [path.resolve(process.cwd(), './src/*')], + } + } + } + }, }, }), }) @@ -100,7 +171,28 @@ export default defineUserConfig({ ::: -### 使用 +你可以直接在 代码块中使用 `@/` 开头的路径,导入 `src` 目录下的类型文件,如下所示: + +````md +```ts twoslash +import type { Foo } from '@/foo' + +const foo: Foo = 1 +``` +```` + +::: important +使用 `plugins.shiki.twoslash.compilerOptions` 可以更灵活的配置 类型编译,你可以在这里修改 `baseUrl` 等配置选项。 + +[参考 CompilerOptions](https://www.typescriptlang.org/tsconfig/#compilerOptions) + +通常您只需要配置 `paths` 选项,其它选项保持默认即可,除非您了解您在配置什么。 +::: + +## 使用 + +::: warning `twoslash` 仅支持 `typescript` 和 `vue` 的 代码块。 +::: 启用该功能后,你只需要在 原有的 markdown 代码块语法中,在代码语言声明后添加 `twoslash` 关键词即可: @@ -112,120 +204,257 @@ const a = 1 主题仅会对有 `twoslash` 关键词的代码进行编译处理。 -### 语法参考 +## 语法参考 完整语法请参考 [ts-twoslasher](https://github.com/microsoft/TypeScript-Website/tree/v2/packages/ts-twoslasher) 和 [shikijs-twoslash](https://twoslash.netlify.app/) -`twoslash` 将 **双斜杠** 视为代码示例的预处理器。 +`twoslash` 将 __双斜杠__ 视为代码示例的预处理器。 因此,所有的标记都是在 `//` 之后添加的。 +### 符号标记 + 常用的 `twoslash` 标记: -#### `^?` {id=twoslash-hover} +#### `^?` {#extract-type} + +使用 `^?` 可以提取位于其上方代码行中特定标识符的类型信息。 -`^?` 用于突出显示类型,而无需用户进行悬停交互: +__输入:__ ````md ```ts twoslash -const a = 1 -// ^? +const hi = 'Hello' +const msg = `${hi}, world` +// ^? ``` ```` +__输出:__ + ```ts twoslash -const a = 1 -// ^? +const hi = 'Hello' +const msg = `${hi}, world` +// ^? // ``` -**需要注意的是,`^`必须正确指向需要突出显示类型的变量** +::: important 符号 `^`必须正确指向需要突出显示类型的变量 +::: + +#### `^|` {#completions} -#### `^|` {id=twoslash-list} +使用 `^|` ,可以提取特定位置的自动补全信息。 -`^|` 可以将展示代码编辑过程中的内容预测列表,如编辑器中的自动完成功能: +__输入:__ ````md ```ts twoslash // @noErrors -// @esModuleInterop -import express from 'express' -const app = express() -app.get('/', (req, res) => { - res.sen -// ^| -}) -app.listen(3000) +console.e +// ^| ``` ```` +__输出:__ + ```ts twoslash // @noErrors -// @esModuleInterop -import express from 'express' -const app = express() -app.get('/', (req, res) => { - res.sen +console.e // ^| -}) -app.listen(3000) -// -// // ``` -**需要注意的是,`^`必须正确指向需要进行内容预测的位置** +::: important 符号`^`必须正确指向需要进行内容预测的位置 +::: -这些类型提示并不是凭空而来的,它依赖于你的项目中的 `node_modules`。比如 `express`。 -你需要在项目中 安装 `@types/express` 才能使用这些类型提示。 +Twoslash 会向 TypeScript 请求获取在 `^` 位置的自动补全建议,然后根据 `.` 后面的字母过滤可能的输出。 +最多会显示 5 个内联结果,如果某个补全项被标记为已弃用,输出中会予以体现。 -#### `@errors` {id=twoslash-errors} +因此,在这种情况下,Twoslash 向 TypeScript 请求 `console` 的补全建议,然后筛选出以 `e` 开头的补全项。 +注意,设置 `// @noErrors` 编译器标志,因为 `console.e` 是一个失败的 TypeScript 代码示例,但我们并不关心这一点。 -`@errors: ` 显示代码是如何出现错误的: +#### `^^^` {#highlighting} + +使用 `^^^` 来突出显示其上方某行的特定范围。 + +__输入:__ ````md ```ts twoslash -// @errors: 2339 -const welcome = 'Tudo bem gente?' -const words = welcome.contains(' ') +function add(a: number, b: number) { + // ^^^ + return a + b +} ``` ```` +__输出:__ + ```ts twoslash -// @errors: 2339 -const welcome = 'Tudo bem gente?' -const words = welcome.contains(' ') +function add(a: number, b: number) { + // ^^^ + return a + b +} ``` -你需要在 `@errors` 后面,声明对应的 `typescript` 错误码。 - -::: note -如果你不知道应该添加哪个 错误码,你可以先尝试直接编写好代码,然后等待编译失败, -你应该能够在控制台中查看到相关的错误信息,然后在错误信息的 `description` 中找到对应的错误码。 -然后再将错误码添加到 `@errors` 中 +::: important 使用连续多个符号`^`正确指向需要突出显示的范围 ::: -#### `---cut---` {id=twoslash-cut} +### `@filename` {#import-files} -`---cut---` 用于将代码分割, 被 `---cut---` 标记的,在其之前的代码将被忽略,不会显示: +`@filename: ` 用于声明后续的代码将来自哪个文件, +你可以在其他部分的代码中通过 `import` 导入该文件。 + +__输入:__ ````md ```ts twoslash -const hi = 'hi' +// @filename: sum.ts +export function sum(a: number, b: number): number { + return a + b +} + +// @filename: ok.ts +import { sum } from './sum' +sum(1, 2) + +// @filename: error.ts +// @errors: 2345 +import { sum } from './sum' +sum(4, 'woops') +``` +```` + +__输出:__ + +```ts twoslash +// @filename: sum.ts +export function sum(a: number, b: number): number { + return a + b +} + +// @filename: ok.ts +import { sum } from './sum' +sum(1, 2) + +// @filename: error.ts +// @errors: 2345 +import { sum } from './sum' +sum(4, 'woops') +``` + +### 剪切代码 + +#### `---cut-before---` {#cut-before} + +在 TypeScript 生成项目并提取所有编辑器信息(如标识符、查询、高亮等)后,剪切操作会修正所有偏移量和行号, +以适应较小的输出。 + +用户所见的内容为 `// ---cut-before---` 以下的部分。此外,还可使用简写形式 `// ---cut---` 。 + +__输入:__ + +````md +```ts twoslash +const level: string = 'Danger' +// ---cut--- +console.log(level) +```` + +__输出:__ + +```ts twoslash +const level: string = 'Danger' // ---cut--- -const msg = `${hi} words` as const -// ^? +console.log(level) +``` + +仅显示单个文件: + +__输入:__ + +````md +```ts twoslash +// @filename: a.ts +export const helloWorld: string = 'Hi' +// ---cut--- +// @filename: b.ts +import { helloWorld } from './a' + +console.log(helloWorld) ``` ```` +__输出:__ + ```ts twoslash -const hi = 'hi' +// @filename: a.ts +export const helloWorld: string = 'Hi' // ---cut--- -const msg = `${hi} words` as const -// ^? -// +// @filename: b.ts +import { helloWorld } from './a' + +console.log(helloWorld) +``` + +只显示最后两行,但对 TypeScript 来说,这是一个包含两个文件的程序,并且所有 IDE 信息在文件间都正确连接。 +这就是为什么 `// @filename: [file]` 是唯一不会被移除的 Twoslash 命令,因为如果不相关,它可以被 `---cut---` 掉。 + +#### `---cut-after---` {#cut-after} + +`---cut-before---` 的兄弟,用于修剪符号后的所有内容: + +__输入:__ + +````md +```ts twoslash +const level: string = 'Danger' +// ---cut-before--- +console.log(level) +// ---cut-after--- +console.log('This is not shown') +``` +```` + +__输出:__ + +```ts twoslash +const level: string = 'Danger' +// ---cut-before--- +console.log(level) +// ---cut-after--- +console.log('This is not shown') +``` + +#### `---cut-start---` 和 `---cut-end---` {#cut-start-end} + +你也可以使用 `---cut-start---` 和 `---cut-end---` 对来剪切两个符号之间的代码段。 + +__输入:__ + +````md +```ts twoslash +const level: string = 'Danger' +// ---cut-start--- +console.log(level) // 这里是被剪切的 +// ---cut-end--- +console.log('This is shown') ``` +```` -#### 自定义输出信息 {id=twoslash-custom-message} +__输出:__ + +```ts twoslash +const level: string = 'Danger' +// ---cut-start--- +console.log(level) // 这里是被剪切的 +// ---cut-end--- +console.log('This is shown') +``` + +支持多个实例以剪切多个部分,但符号必须成对出现。 + +### 自定义输出信息 {id=twoslash-custom-message} `@log`, `@error`, `@warn` 和 `@annotate` 用于向用户输出不同级别的自定义信息 @@ -251,45 +480,284 @@ const c = 1 // @annotate: Custom annotation message ``` -#### import_files {id=twoslash-import-files} +### 输出已编译文件 -`@filename: ` 用于声明后续的代码将来自哪个文件, -你可以在其他部分的代码中通过 `import` 导入该文件。 +运行 Twoslash 代码示例会触发完整的 TypeScript 编译运行,该运行会在虚拟文件系统中创建文件。 +你可以将代码示例的内容替换为对项目运行TypeScript后的结果。 + +#### `@showEmit` + +`// @showEmit` 是 告诉 Twoslash 你希望将代码示例的输出替换为等效的 ·.js· 文件的主要命令。 + +__输入:__ ````md ```ts twoslash -// @filename: sum.ts -export function sum(a: number, b: number): number { - return a + b -} +// @showEmit +const level: string = 'Danger' +``` +```` -// @filename: ok.ts -import { sum } from './sum' -sum(1, 2) +__输出:__ -// @filename: error.ts -// @errors: 2345 -import { sum } from './sum' -sum(4, 'woops') +```ts twoslash +// @showEmit +const level: string = 'Danger' +``` + +结果将显示此 `.ts` 文件所对应的 `.js` 文件。 +可以看到 TypeScript 输出的内容中移除了 `: string` ,并添加了 `export {}`。 + +#### `@showEmittedFile: [file]` + +虽然 `.js` 文件可能是开箱即用最有用的文件,但 TypeScript 确实会在启用正确标志时发出其他文件(`.d.ts`和 `.map`), +并且在有多文件代码示例时,你可能需要告诉 Twoslash 显示哪个文件。 +对于所有这些情况,你也可以添加 `@showEmittedFile: [file]` 来告诉 Twoslash 你想显示哪个文件。 + +__显示TypeScript代码示例的 `.d.ts` 文件:__ + +__输入:__ + +````md +```ts twoslash +// @declaration +// @showEmit +// @showEmittedFile: index.d.ts +export const hello = 'world' ``` ```` +__输出:__ + ```ts twoslash -// @filename: sum.ts -export function sum(a: number, b: number): number { - return a + b -} +// @declaration +// @showEmit +// @showEmittedFile: index.d.ts +export const hello = 'world' +``` -// @filename: ok.ts -import { sum } from './sum' -sum(1, 2) +__显示 JavaScript 到 TypeScript 的 `.map` 文件:__ -// @filename: error.ts -// @errors: 2345 -import { sum } from './sum' -sum(4, 'woops') +__输入:__ + +````md +```ts twoslash +// @sourceMap +// @showEmit +// @showEmittedFile: index.js.map +export const hello = 'world' ``` +```` -::: warning -在本节中未提及的其他标记,由于未正式验证其是否可用,请谨慎使用。 +__输出:__ + +```ts twoslash +// @sourceMap +// @showEmit +// @showEmittedFile: index.js.map +export const hello = 'world' +``` + +__显示 `.d.ts` 文件的 `.map`(主要用于项目引用):__ + +__输入:__ + +````md +```ts twoslash +// @declaration +// @declarationMap +// @showEmit +// @showEmittedFile: index.d.ts.map +export const hello: string = 'world' +``` +```` + +__输出:__ + +```ts twoslash +// @declaration +// @declarationMap +// @showEmit +// @showEmittedFile: index.d.ts.map +export const hello: string = 'world' +``` + +__为 `b.ts` 生成 `.js` 文件:__ + +__输入:__ + +````md +```ts twoslash +// @showEmit +// @showEmittedFile: b.js +// @filename: a.ts +export const helloWorld: string = 'Hi' + +// @filename: b.ts +import { helloWorld } from './a' +console.log(helloWorld) +``` +```` + +__输出:__ + +```ts twoslash +// @showEmit +// @showEmittedFile: b.js +// @filename: a.ts +export const helloWorld: string = 'Hi' + +// @filename: b.ts +import { helloWorld } from './a' +console.log(helloWorld) +``` + +### `@errors` + +`@errors: ` 显示代码是如何出现错误的: + +__输入:__ + +````md +```ts twoslash +// @errors: 2322 2588 +const str: string = 1 +str = 'Hello' +```` + +__输出:__ + +```ts twoslash +// @errors: 2322 2588 +const str: string = 1 +str = 'Hello' +``` + +你需要在 `@errors` 后面,声明对应的 `typescript` 错误码。使用空格分隔多个错误代码。 + +::: note +如果你不知道应该添加哪个 错误码,你可以先尝试直接编写好代码,然后等待编译失败, +你应该能够在控制台中查看到相关的错误信息,然后在错误信息的 `description` 中找到对应的错误码。 +然后再将错误码添加到 `@errors` 中。 + +不用担心变异失败会终止进程,主题会在编译失败时显示错误信息,同时在代码块中输出未编译的代码。 ::: + +### `@noErrors` + +在代码中屏蔽所有错误。你还可以提供错误代码来屏蔽特定错误。 + +__输入:__ + +````md +```ts twoslash +// @noErrors +const str: string = 1 +str = 'Hello' +``` +```` + +__输出:__ + +```ts twoslash +// @noErrors +const str: string = 1 +str = 'Hello' +``` + +### `@noErrorsCutted` + +忽略在剪切代码中发生的错误。 + +__输入:__ + +````md +```ts twoslash +// @noErrorsCutted +const hello = 'world' +// ---cut-after--- +hello = 'hi' // 本应为错误,但因被截断而忽略。 +``` +```` + +__输出:__ + +```ts twoslash +// @noErrorsCutted +const hello = 'world' +// ---cut-after--- +hello = 'hi' // 本应为错误,但因被截断而忽略。 +``` + +### `@noErrorValidation` + +禁用错误验证,错误信息仍将呈现,但 Twoslash 不会抛出编译时代码中的错误。 + +__输入:__ + +````md +```ts twoslash +// @noErrorValidation +const str: string = 1 +``` +```` + +__输出:__ + +```ts twoslash +// @noErrorValidation +const str: string = 1 +``` + +### `@keepNotations` + +告知Twoslash不要移除任何注释,并保持原始代码不变。节点将包含原始代码的位置信息。 + +__输入:__ + +````md +```ts twoslash +// @keepNotations +// @module: esnext +// @errors: 2322 +const str: string = 1 +``` +```` + +__输出:__ + +```ts twoslash +// @keepNotations +// @module: esnext +// @errors: 2322 +const str: string = 1 +``` + +### 覆盖编译器选项 + +使用 `// @name` 和 `// @name: value` 注释来覆盖 TypeScript 的[编译器选项](https://www.typescriptlang.org/tsconfig#compilerOptions)。 +这些注释将从输出中移除。 + +__输入:__ + +````md +```ts twoslash +// @noImplicitAny: false +// @target: esnext +// @lib: esnext +// 这本应抛出一个错误, +// 但由于我们禁用了noImplicitAny,所以不会抛出错误。 +const fn = a => a + 1 +``` +```` + +__输出:__ + +```ts twoslash +// @noImplicitAny: false +// @target: esnext +// @lib: esnext +// 这本应抛出一个错误, +// 但由于我们禁用了noImplicitAny,所以不会抛出错误。 +const fn = a => a + 1 +``` diff --git a/plugins/plugin-shikiji/src/node/highlight/highlight.ts b/plugins/plugin-shikiji/src/node/highlight/highlight.ts index 6f05d4bd3..667da5fed 100644 --- a/plugins/plugin-shikiji/src/node/highlight/highlight.ts +++ b/plugins/plugin-shikiji/src/node/highlight/highlight.ts @@ -1,7 +1,7 @@ import type { HighlighterOptions, ThemeOptions } from '../types.js' import { customAlphabet } from 'nanoid' import { bundledLanguages, createHighlighter } from 'shiki' -import { logger } from 'vuepress/utils' +import { colors, logger } from 'vuepress/utils' import { getLanguage } from './getLanguage.js' import { baseTransformers, getInlineTransformers } from './transformers.js' @@ -45,7 +45,13 @@ export async function highlight( lang, transformers: [ ...baseTransformers, - ...getInlineTransformers({ attrs, lang, enabledTwoslash, whitespace }), + ...getInlineTransformers({ + attrs, + lang, + enabledTwoslash, + whitespace, + twoslash: options.twoslash, + }), ...userTransformers, ], meta: { __raw: attrs }, @@ -59,7 +65,11 @@ export async function highlight( return rendered } catch (e) { - logger.error(e) + logger.error( + (e as Error)?.message, + '\n', + (e as Error)?.stack ? colors.gray(String((e as Error)?.stack)) : '', + ) return str } } diff --git a/plugins/plugin-shikiji/src/node/highlight/index.ts b/plugins/plugin-shikiji/src/node/highlight/index.ts index e01cb0d39..6e06de950 100644 --- a/plugins/plugin-shikiji/src/node/highlight/index.ts +++ b/plugins/plugin-shikiji/src/node/highlight/index.ts @@ -1,2 +1,3 @@ export * from './highlight.js' +export * from './resolveTsPaths.js' export * from './scanLanguages.js' diff --git a/plugins/plugin-shikiji/src/node/highlight/resolveTsPaths.ts b/plugins/plugin-shikiji/src/node/highlight/resolveTsPaths.ts new file mode 100644 index 000000000..0988456ea --- /dev/null +++ b/plugins/plugin-shikiji/src/node/highlight/resolveTsPaths.ts @@ -0,0 +1,28 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' + +export async function resolveTsPaths(): Promise | undefined> { + const tsconfigPath = path.join(process.cwd(), 'tsconfig.json') + try { + const tsconfig = JSON.parse(await fs.readFile(tsconfigPath, 'utf-8')) + const paths = tsconfig.compilerOptions?.paths ?? undefined + const baseUrl = tsconfig.compilerOptions?.baseUrl + + 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 if (value) + paths[key] = path.resolve(dirname, value) + } + } + + return paths + } + catch { + return undefined + } +} diff --git a/plugins/plugin-shikiji/src/node/highlight/transformers.ts b/plugins/plugin-shikiji/src/node/highlight/transformers.ts index b8394cc5e..c15559b5f 100644 --- a/plugins/plugin-shikiji/src/node/highlight/transformers.ts +++ b/plugins/plugin-shikiji/src/node/highlight/transformers.ts @@ -1,5 +1,8 @@ +import type { TransformerTwoslashOptions } from '@shikijs/twoslash/core' import type { ShikiTransformer } from 'shiki' +import type { VueSpecificOptions } from 'twoslash-vue' import type { WhitespacePosition } from '../utils/index.js' +import process from 'node:process' import { transformerCompactLineOptions, transformerNotationDiff, @@ -10,7 +13,9 @@ import { transformerRemoveNotationEscape, transformerRenderWhitespace, } from '@shikijs/transformers' +import { defaultTwoslashOptions } from '@shikijs/twoslash/core' import { addClassToHast } from 'shiki' +import { isPlainObject } from 'vuepress/shared' import { defaultHoverInfoProcessor, transformerTwoslash } from '../twoslash/rendererTransformer.js' import { attrsToLines, resolveWhitespacePosition } from '../utils/index.js' @@ -53,11 +58,12 @@ export const baseTransformers: ShikiTransformer[] = [ ] const vueRE = /-vue$/ -export function getInlineTransformers({ attrs, lang, enabledTwoslash, whitespace }: { +export function getInlineTransformers({ attrs, lang, enabledTwoslash, whitespace, twoslash }: { attrs: string lang: string enabledTwoslash: boolean whitespace: boolean | WhitespacePosition + twoslash?: boolean | TransformerTwoslashOptions['twoslashOptions'] & VueSpecificOptions }): ShikiTransformer[] { const vPre = vueRE.test(lang) ? '' : 'v-pre' const inlineTransformers: ShikiTransformer[] = [ @@ -65,10 +71,20 @@ export function getInlineTransformers({ attrs, lang, enabledTwoslash, whitespace ] if (enabledTwoslash) { + const { compilerOptions, ...twoslashOptions } = isPlainObject(twoslash) ? twoslash : {} + const defaultOptions = defaultTwoslashOptions() inlineTransformers.push(transformerTwoslash({ processHoverInfo(info) { return defaultHoverInfoProcessor(info) }, + twoslashOptions: { + ...defaultOptions, + ...twoslashOptions, + compilerOptions: { + baseUrl: process.cwd(), + ...compilerOptions, + }, + }, })) } else { diff --git a/plugins/plugin-shikiji/src/node/shikiPlugin.ts b/plugins/plugin-shikiji/src/node/shikiPlugin.ts index ab21f3e03..efcb5fbaf 100644 --- a/plugins/plugin-shikiji/src/node/shikiPlugin.ts +++ b/plugins/plugin-shikiji/src/node/shikiPlugin.ts @@ -8,7 +8,7 @@ import type { import { isPlainObject } from 'vuepress/shared' import { colors } from 'vuepress/utils' import { copyCodeButtonPlugin } from './copy-code-button/index.js' -import { highlight, scanLanguages } from './highlight/index.js' +import { highlight, resolveTsPaths, scanLanguages } from './highlight/index.js' import { collapsedLinesPlugin, highlightLinesPlugin, @@ -47,7 +47,7 @@ export function shikiPlugin({ clientConfigFile: app => prepareClientConfigFile(app, { copyCode: copyCode !== false, - twoslash: options.twoslash ?? false, + twoslash: !!options.twoslash, }), extendsMarkdown: async (md, app) => { @@ -64,6 +64,18 @@ export function shikiPlugin({ } } + if (options.twoslash) { + const paths = await resolveTsPaths() + if (paths) { + options.twoslash = isPlainObject(options.twoslash) ? options.twoslash : {} + options.twoslash.compilerOptions ??= {} + options.twoslash.compilerOptions.paths = { + ...paths, + ...options.twoslash.compilerOptions.paths, + } + } + } + md.options.highlight = await highlight(theme, options) if (app.env.isDebug) { logger.info(`highlight Loaded in: ${(performance.now() - start).toFixed(2)}ms`) diff --git a/plugins/plugin-shikiji/src/node/twoslash/rendererTransformer.ts b/plugins/plugin-shikiji/src/node/twoslash/rendererTransformer.ts index 71fbf412a..f37b0e0fc 100644 --- a/plugins/plugin-shikiji/src/node/twoslash/rendererTransformer.ts +++ b/plugins/plugin-shikiji/src/node/twoslash/rendererTransformer.ts @@ -24,8 +24,6 @@ export interface VitePressPluginTwoslashOptions extends TransformerTwoslashVueOp /** * Create a Shiki transformer for VitePress to enable twoslash integration - * - * Add this to `markdown.codeTransformers` in `.vitepress/config.ts` */ export function transformerTwoslash(options: VitePressPluginTwoslashOptions = {}): ShikiTransformer { const { diff --git a/plugins/plugin-shikiji/src/node/types.ts b/plugins/plugin-shikiji/src/node/types.ts index db434228e..ee62105b8 100644 --- a/plugins/plugin-shikiji/src/node/types.ts +++ b/plugins/plugin-shikiji/src/node/types.ts @@ -1,3 +1,4 @@ +import type { TransformerTwoslashOptions } from '@shikijs/twoslash/core' import type { BuiltinTheme, BundledLanguage, @@ -8,6 +9,7 @@ import type { StringLiteralUnion, ThemeRegistration, } from 'shiki' +import type { VueSpecificOptions } from 'twoslash-vue' import type { LocaleConfig } from 'vuepress/shared' export type ShikiLang = @@ -73,7 +75,7 @@ export interface HighlighterOptions { * Enable transformerTwoslash * @default false */ - twoslash?: boolean + twoslash?: boolean | TransformerTwoslashOptions['twoslashOptions'] & VueSpecificOptions /** * Enable transformerRenderWhitespace diff --git a/theme/src/client/styles/twoslash.css b/theme/src/client/styles/twoslash.css index 00e4ebfc1..9c3dccf4e 100644 --- a/theme/src/client/styles/twoslash.css +++ b/theme/src/client/styles/twoslash.css @@ -18,6 +18,8 @@ --twoslash-tag-warn-bg: var(--vp-c-warning-soft); --twoslash-tag-annotate-color: var(--vp-c-green-1); --twoslash-tag-annotate-bg: var(--vp-c-green-soft); + --twoslash-highlighted-bg: var(--vp-c-gray-soft); + --twoslash-highlighted-border: var(--vp-c-border); } /* Respect people's wishes to not have animations */