Two Slash
约 3317 字
概述
为代码块添加支持 TypeScript TwoSlash 支持。 在代码块内提供内联类型提示。
该功能由 shiki 与 @shikijs/twoslash 提供支持, 主题通过 codeHighlighter 配置集成。
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 { createHighlighter } from 'shiki'
const highlighter = await createHighlighter({ themes: ['nord'], langs: ['javascript'] })
//
const a = 1Custom log messageconst b = 1Custom error messageconst c = 1Custom warning messageCustom annotation message配置
启用功能
在启用功能前,您需要先安装 @vuepress/shiki-twoslash 包:
npm i @vuepress/shiki-twoslashpnpm add @vuepress/shiki-twoslashyarn add @vuepress/shiki-twoslash在主题配置中,启用 twoslash 选项。
export default defineUserConfig({
theme: plumeTheme({
codeHighlighter: {
twoslash: true,
},
}),
})重要
twoslash 对于大多数用户而言,不是必要的功能,且 twoslash 相关的依赖包体积较大, 因此将 twoslash 的相关实现均迁移到了 @vuepress/shiki-twoslash 中, 这可以有效减少主题的初始安装体积。 仅在您需要使用 twoslash 功能时,才需要安装 @vuepress/shiki-twoslash。
从 node_modules 中导入类型文件
twoslash 最大的特点就是对代码块进行类型编译,默认支持从项目的 node_modules 中导入类型文件。
例如,如果您需要使用 express的类型提示,你需要在项目中安装 @types/express 依赖:
npm i -D @types/expresspnpm add -D @types/expressyarn add -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({
codeHighlighter: {
twoslash: {
compilerOptions: {
paths: {
// 相对于工作目录 `process.cwd()`
'@/*': ['./src/*'],
// 使用绝对路径
'@@/*': [path.resolve(process.cwd(), './src/*')],
}
}
}
},
}),
})你可以直接在代码块中使用 @/ 开头的路径,导入 src 目录下的类型文件,如下所示:
```ts twoslash
import type { Foo } from '@/foo'
const foo: Foo = 1
```重要
使用 codeHighlighter.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贡献者
更新日志
e4dae-docs: improve docs (#1071)于60619-docs: improve docs (#1069)于97a5b-docs: fix typo于4d236-feat(theme)!: add collections support (#704)于0fd6c-refactor(theme): improve types and flat config (#524)于fb08a-feat(plugin-md-power): cleanup theenvpassed intorenderInline(#519)于6e6f9-docs: improve docs (#509)于df306-perf: update deps to latest于17517-feat(plugin-shikiji): add support fortwoslashOptions(#335)于0c53b-docs: improve docs (#332)于
