Two Slash
3222字约11分钟
2024-03-06
概述
为代码块添加支持 TypeScript TwoSlash 支持。 在代码块内提供内联类型提示。
该功能由 shiki 和 @shikijs/twoslash 提供支持, 并整合在 @vuepress-plume/plugin-shikiji 中。
twoslash 是一个高级功能,您需要熟练掌握 TypeScript 的知识,并且了解 twoslash 语法。
注意
twoslash
是一个比较耗时的功能,由于它需要对代码进行类型编译,如果代码引入的包 比较大,会花费较长时间。
特别的,由于 vuepress 启动时,会预编译所有的 markdown 文件,因此它会直接影响 vuepress 的启动时间, 如果 包含了比较多的 twoslash
代码块,这可能会使 vuepress 启动时间变得很长。
比如,在未使用 twoslash
时,vuepress 的启动时间区间大约在 300ms ~ 1000ms
之间,而在使用 twoslash
后, 可能某一个 twoslash
的代码块编译耗时就需要额外再等待 500ms
以上。
但不必担心 markdown 文件热更新时的编译耗时,主题针对 代码高亮编译 耗时做了优化,即使 单个 markdown 文件 中包含多个 代码块,主题也仅会对 有变更的代码块 进行编译,因此热更新的速度依然非常快。
twoslash 是一种 javascript
和 typescript
标记语言。 你可以编写一个代码示例来描述整个 javascript
项目。
twoslash
将 双斜杠注释 (//
) 视为 代码示例的预处理器。
twoslash
使用与文本编辑器相同的编译器 API 来提供类型驱动的悬停信息、准确的错误和类型标注。
功能预览
将鼠标悬停在 变量 或 函数 上查看效果:
import { getHighlighterCore } from 'shiki/core'
const highlighter = await getHighlighterCore({})
//
const a = 1Custom log messageconst b = 1Custom error messageconst c = 1Custom warning messageCustom annotation message
配置
启用功能
在 主题配置中,启用 twoslash
选项。
export default defineUserConfig({
theme: plumeTheme({
plugins: {
shiki: { twoslash: true },
},
}),
})
从 node_modules
中导入类型文件
twoslash 最大的特点就是对代码块进行类型编译,默认支持从项目的 node_modules
中导入类型文件。
例如,如果您需要使用 express
的类型提示,你需要在项目中安装 @types/express
依赖:
pnpm add -D @types/express
yarn add -D @types/express
npm i -D @types/express
然后,可以在 代码块中使用 express
的类型,如下所示:
```ts twoslash
import express from 'express'
const app = express()
```
导入本地类型文件
对于导入本地类型文件,由于代码块中的代码在编译时,并不容易确定代码的真实路径,我们很难直观的在 代码块中通过 相对路径 导入类型文件。
从 tsconfig.json
中读取路径映射
主题支持从项目根目录下的 tsconfig.json
,读取 compilerOptions.paths
中的路径映射,来解决这个问题。
假设你的项目的 tsconfig.json
配置如下:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
你可以直接在 代码块中使用 @/
开头的路径,导入 src
目录下的类型文件,如下所示:
```ts twoslash
import type { Foo } from '@/foo'
const foo: Foo = 1
```
从 shiki.twoslash
中读取路径映射
你可以在 shiki.twoslash
中配置 compilerOptions
,来解决这个问题,如下所示:
import path from 'node:path'
export default defineUserConfig({
theme: plumeTheme({
plugins: {
shiki: {
twoslash: {
compilerOptions: {
paths: {
// 相对于工作目录 `process.cwd()`
'@/*': ['./src/*'],
// 使用绝对路径
'@@/*': [path.resolve(process.cwd(), './src/*')],
}
}
}
},
},
}),
})
你可以直接在 代码块中使用 @/
开头的路径,导入 src
目录下的类型文件,如下所示:
```ts twoslash
import type { Foo } from '@/foo'
const foo: Foo = 1
```
重要
使用 plugins.shiki.twoslash.compilerOptions
可以更灵活的配置 类型编译,你可以在这里修改 baseUrl
等配置选项。
通常您只需要配置 paths
选项,其它选项保持默认即可,除非您了解您在配置什么。
使用
twoslash
仅支持 typescript
和 vue
的 代码块。
启用该功能后,你只需要在 原有的 markdown 代码块语法中,在代码语言声明后添加 twoslash
关键词即可:
```ts twoslash
const a = 1
```
主题仅会对有 twoslash
关键词的代码进行编译处理。
语法参考
完整语法请参考 ts-twoslasher 和 shikijs-twoslash
twoslash
将 双斜杠 视为代码示例的预处理器。 因此,所有的标记都是在 //
之后添加的。
符号标记
常用的 twoslash
标记:
^?
使用 ^?
可以提取位于其上方代码行中特定标识符的类型信息。
输入:
```ts twoslash
const hi = 'Hello'
const msg = `${hi}, world`
// ^?
```
输出:
const hi = 'Hello'
const msg = `${hi}, world`
//
符号 ^
必须正确指向需要突出显示类型的变量
^|
使用 ^|
,可以提取特定位置的自动补全信息。
输入:
```ts twoslash
// @noErrors
console.e
// ^|
```
输出:
console.e
//
符号^
必须正确指向需要进行内容预测的位置
Twoslash 会向 TypeScript 请求获取在 ^
位置的自动补全建议,然后根据 .
后面的字母过滤可能的输出。 最多会显示 5 个内联结果,如果某个补全项被标记为已弃用,输出中会予以体现。
因此,在这种情况下,Twoslash 向 TypeScript 请求 console
的补全建议,然后筛选出以 e
开头的补全项。 注意,设置 // @noErrors
编译器标志,因为 console.e
是一个失败的 TypeScript 代码示例,但我们并不关心这一点。
^^^
使用 ^^^
来突出显示其上方某行的特定范围。
输入:
```ts twoslash
function add(a: number, b: number) {
// ^^^
return a + b
}
```
输出:
function add(a: number, b: number) {
return a + b
}
使用连续多个符号^
正确指向需要突出显示的范围
@filename
@filename: <filename>
用于声明后续的代码将来自哪个文件, 你可以在其他部分的代码中通过 import
导入该文件。
输入:
```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')
```
输出:
// @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
import { sum } from './sum'
sum(4, 'woops')Argument of type 'string' is not assignable to parameter of type 'number'.
剪切代码
---cut-before---
在 TypeScript 生成项目并提取所有编辑器信息(如标识符、查询、高亮等)后,剪切操作会修正所有偏移量和行号, 以适应较小的输出。
用户所见的内容为 // ---cut-before---
以下的部分。此外,还可使用简写形式 // ---cut---
。
输入:
```ts twoslash
const level: string = 'Danger'
// ---cut---
console.log(level)
输出:
console.log(level)
仅显示单个文件:
输入:
```ts twoslash
// @filename: a.ts
export const helloWorld: string = 'Hi'
// ---cut---
// @filename: b.ts
import { helloWorld } from './a'
console.log(helloWorld)
```
输出:
// @filename: b.ts
import { helloWorld } from './a'
console.log(helloWorld)
只显示最后两行,但对 TypeScript 来说,这是一个包含两个文件的程序,并且所有 IDE 信息在文件间都正确连接。 这就是为什么 // @filename: [file]
是唯一不会被移除的 Twoslash 命令,因为如果不相关,它可以被 ---cut---
掉。
---cut-after---
---cut-before---
的兄弟,用于修剪符号后的所有内容:
输入:
```ts twoslash
const level: string = 'Danger'
// ---cut-before---
console.log(level)
// ---cut-after---
console.log('This is not shown')
```
输出:
console.log(level)
---cut-start---
和 ---cut-end---
你也可以使用 ---cut-start---
和 ---cut-end---
对来剪切两个符号之间的代码段。
输入:
```ts twoslash
const level: string = 'Danger'
// ---cut-start---
console.log(level) // 这里是被剪切的
// ---cut-end---
console.log('This is shown')
```
输出:
const level: string = 'Danger'
console.log('This is shown')
支持多个实例以剪切多个部分,但符号必须成对出现。
自定义输出信息
@log
, @error
, @warn
和 @annotate
用于向用户输出不同级别的自定义信息
```ts twoslash
// @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
```
const a = 1Custom log messageconst b = 1Custom error messageconst c = 1Custom warning messageCustom annotation message
输出已编译文件
运行 Twoslash 代码示例会触发完整的 TypeScript 编译运行,该运行会在虚拟文件系统中创建文件。 你可以将代码示例的内容替换为对项目运行TypeScript后的结果。
@showEmit
// @showEmit
是 告诉 Twoslash 你希望将代码示例的输出替换为等效的 ·.js· 文件的主要命令。
输入:
```ts twoslash
// @showEmit
const level: string = 'Danger'
```
输出:
const level = 'Danger';
export {};
结果将显示此 .ts
文件所对应的 .js
文件。 可以看到 TypeScript 输出的内容中移除了 : string
,并添加了 export {}
。
@showEmittedFile: [file]
虽然 .js
文件可能是开箱即用最有用的文件,但 TypeScript 确实会在启用正确标志时发出其他文件(.d.ts
和 .map
), 并且在有多文件代码示例时,你可能需要告诉 Twoslash 显示哪个文件。 对于所有这些情况,你也可以添加 @showEmittedFile: [file]
来告诉 Twoslash 你想显示哪个文件。
显示TypeScript代码示例的 .d.ts
文件:
输入:
```ts twoslash
// @declaration
// @showEmit
// @showEmittedFile: index.d.ts
export const hello = 'world'
```
输出:
export declare const hello = "world";
显示 JavaScript 到 TypeScript 的 .map
文件:
输入:
```ts twoslash
// @sourceMap
// @showEmit
// @showEmittedFile: index.js.map
export const hello = 'world'
```
输出:
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,KAAK,GAAG,OAAO,CAAA"}
显示 .d.ts
文件的 .map
(主要用于项目引用):
输入:
```ts twoslash
// @declaration
// @declarationMap
// @showEmit
// @showEmittedFile: index.d.ts.map
export const hello: string = 'world'
```
输出:
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,EAAE,MAAgB,CAAA"}
为 b.ts
生成 .js
文件:
输入:
```ts twoslash
// @showEmit
// @showEmittedFile: b.js
// @filename: a.ts
export const helloWorld: string = 'Hi'
// @filename: b.ts
import { helloWorld } from './a'
console.log(helloWorld)
```
输出:
// @filename: b.ts
import { helloWorld } from './a';
console.log(helloWorld);
@errors
@errors: <error code>
显示代码是如何出现错误的:
输入:
```ts twoslash
// @errors: 2322 2588
const str: string = 1
str = 'Hello'
输出:
const str: string = 1Type 'number' is not assignable to type 'string'.str = 'Hello'Cannot assign to 'str' because it is a constant.
你需要在 @errors
后面,声明对应的 typescript
错误码。使用空格分隔多个错误代码。
注
如果你不知道应该添加哪个 错误码,你可以先尝试直接编写好代码,然后等待编译失败, 你应该能够在控制台中查看到相关的错误信息,然后在错误信息的 description
中找到对应的错误码。 然后再将错误码添加到 @errors
中。
不用担心变异失败会终止进程,主题会在编译失败时显示错误信息,同时在代码块中输出未编译的代码。
@noErrors
在代码中屏蔽所有错误。你还可以提供错误代码来屏蔽特定错误。
输入:
```ts twoslash
// @noErrors
const str: string = 1
str = 'Hello'
```
输出:
const str: string = 1
str = 'Hello'
@noErrorsCutted
忽略在剪切代码中发生的错误。
输入:
```ts twoslash
// @noErrorsCutted
const hello = 'world'
// ---cut-after---
hello = 'hi' // 本应为错误,但因被截断而忽略。
```
输出:
const hello = 'world'
@noErrorValidation
禁用错误验证,错误信息仍将呈现,但 Twoslash 不会抛出编译时代码中的错误。
输入:
```ts twoslash
// @noErrorValidation
const str: string = 1
```
输出:
const str: string = 1Type 'number' is not assignable to type 'string'.
@keepNotations
告知Twoslash不要移除任何注释,并保持原始代码不变。节点将包含原始代码的位置信息。
输入:
```ts twoslash
// @keepNotations
// @module: esnext
// @errors: 2322
const str: string = 1
```
输出:
// @keepNotations
// @module: esnext
// @errors: 2322
const str: string = 1Type 'number' is not assignable to type 'string'.
覆盖编译器选项
使用 // @name
和 // @name: value
注释来覆盖 TypeScript 的编译器选项。 这些注释将从输出中移除。
输入:
```ts twoslash
// @noImplicitAny: false
// @target: esnext
// @lib: esnext
// 这本应抛出一个错误,
// 但由于我们禁用了noImplicitAny,所以不会抛出错误。
const fn = a => a + 1
```
输出:
// 这本应抛出一个错误,
// 但由于我们禁用了noImplicitAny,所以不会抛出错误。
const fn = a => a + 1
变更历史
最后更新于: 查看全部变更历史
feat(plugin-shikiji): add support for
于 2024/11/11twoslashOptions
(#335)docs: improve docs (#332)
于 2024/11/8perf(plugin-md-power): add unset
于 2024/10/3languages
warning (#251)chore: tweak
于 2024/6/18docs: update docs
于 2024/6/18chore: update docs
于 2024/5/27docs: update doc
于 2024/4/4docs: lint fix md
于 2024/3/21docs: update theme docs
于 2024/3/17docs: update docs
于 2024/3/13