├── .npmrc ├── lib ├── types.js ├── shingle.js ├── types.d.ts ├── annotate.js ├── index.js └── render.js ├── .prettierignore ├── index.js ├── screenshot-dark.jpg ├── screenshot-light.jpg ├── test ├── fixtures │ ├── tag-without-text │ │ ├── input.html │ │ └── output.html │ ├── extract │ │ ├── input.html │ │ └── output.html │ ├── highlight │ │ ├── input.html │ │ └── output.html │ ├── empty │ │ ├── input.html │ │ └── output.html │ ├── errors │ │ ├── input.html │ │ └── output.html │ ├── cut │ │ ├── input.html │ │ └── output.html │ ├── import-and-node-types │ │ ├── input.html │ │ └── output.html │ ├── handbook-options │ │ ├── input.html │ │ └── output.html │ ├── completion │ │ ├── input.html │ │ └── output.html │ ├── basic │ │ ├── input.html │ │ └── output.html │ └── options │ │ ├── input.html │ │ └── output.html └── index.js ├── demo ├── index.md ├── index.js ├── build.js └── index.css ├── index.d.ts ├── .gitignore ├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── tsconfig.json ├── license ├── package.json └── readme.md /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `types.d.ts`. 2 | export {} 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | test/fixtures/ 3 | *.json 4 | *.html 5 | *.md 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {default} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /screenshot-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rehypejs/rehype-twoslash/HEAD/screenshot-dark.jpg -------------------------------------------------------------------------------- /screenshot-light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rehypejs/rehype-twoslash/HEAD/screenshot-light.jpg -------------------------------------------------------------------------------- /test/fixtures/tag-without-text/input.html: -------------------------------------------------------------------------------- 1 |
console.log(name)
2 | -------------------------------------------------------------------------------- /demo/index.md: -------------------------------------------------------------------------------- 1 | # Jupiter 2 | 3 | ```js twoslash 4 | const name = 'Jupiter' 5 | console.log('Hello, ' + name + '!') 6 | ``` 7 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export {default} from './lib/index.js' 2 | export type {Options, RenderResult, Renderers, Render} from './lib/types.js' 3 | -------------------------------------------------------------------------------- /test/fixtures/extract/input.html: -------------------------------------------------------------------------------- 1 |
const hi = 'Hello'
2 | const msg = `${hi}, world`
3 | //    ^?
4 | 
5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.d.ts.map 3 | *.d.ts 4 | *.log 5 | coverage/ 6 | node_modules/ 7 | yarn.lock 8 | demo/index.html 9 | !/index.d.ts 10 | !/lib/types.d.ts 11 | -------------------------------------------------------------------------------- /test/fixtures/highlight/input.html: -------------------------------------------------------------------------------- 1 |
function add(a: number, b: number) {
2 |   //     ^^^
3 |   return a + b
4 | }
5 | 
6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /test/fixtures/empty/input.html: -------------------------------------------------------------------------------- 1 |
old pond
 2 | frog leaps in
 3 | water’s sound
4 | 5 | alert(1) 6 | 7 |
alert(1)
 8 | 
9 | 10 |
alert(1)
11 | 
12 | 13 |
alert(1)
14 | 
15 | -------------------------------------------------------------------------------- /test/fixtures/empty/output.html: -------------------------------------------------------------------------------- 1 |
old pond
 2 | frog leaps in
 3 | water’s sound
4 | 5 | alert(1) 6 | 7 |
alert(1)
 8 | 
9 | 10 |
alert(1)
11 | 
12 | 13 |
alert(1)
14 | 
15 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | name: bb 2 | on: 3 | issues: 4 | types: [opened, reopened, edited, closed, labeled, unlabeled] 5 | pull_request_target: 6 | types: [opened, reopened, edited, closed, labeled, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /test/fixtures/errors/input.html: -------------------------------------------------------------------------------- 1 |
// @errors: 2322
 2 | // Declare a tuple type
 3 | let x: [string, number]
 4 | 
 5 | // Initialize it
 6 | x = ['hello', 10]
 7 | // Initialize it incorrectly
 8 | x = [10, 'hello']
 9 | 
10 | 11 |
// @errors: 2339
12 | let x: [string, number]
13 | x = ['hello', 10] // OK
14 | // ---cut---
15 | console.log(x[0].substring(1))
16 | console.log(x[1].substring(1))
17 | 
18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v4 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/hydrogen 21 | - node 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "noUncheckedIndexedAccess": true, 12 | "strict": true, 13 | "target": "es2022" 14 | }, 15 | "exclude": ["coverage/", "node_modules/"], 16 | "include": ["**/*.js", "lib/types.d.ts", "index.d.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/cut/input.html: -------------------------------------------------------------------------------- 1 |
const level: string = 'Danger'
 2 | // ---cut---
 3 | console.log(level)
 4 | 
5 | 6 |
// @filename: a.ts
 7 | export const helloWorld: string = 'Hi'
 8 | // ---cut---
 9 | // @filename: b.ts
10 | import { helloWorld } from './a'
11 | 
12 | console.log(helloWorld)
13 | 
14 | 15 |
const level: string = 'Danger'
16 | // ---cut-before---
17 | console.log(level)
18 | // ---cut-after---
19 | console.log('This is not shown')
20 | 
21 | 22 |
const level: string = 'Danger'
23 | // ---cut-start---
24 | console.log(level) // This is not shown.
25 | // ---cut-end---
26 | console.log('This is shown')
27 | 
28 | -------------------------------------------------------------------------------- /test/fixtures/import-and-node-types/input.html: -------------------------------------------------------------------------------- 1 |
/// <reference types="node" />
 2 | 
 3 | // @ts-check
 4 | import fs from "fs"
 5 | import { execSync } from "child_process"
 6 | 
 7 | const fileToEdit = process.env.HUSKY_GIT_PARAMS!.split(" ")[0]
 8 | const files = execSync("git status --porcelain", { encoding: "utf8" })
 9 | 
10 | const maps: any = {
11 |   "spelltower/": "SPTWR",
12 |   "typeshift/": "TPSFT",
13 | }
14 | 
15 | const prefixes = new Set()
16 | files.split("\n").forEach(f => {
17 |   const found = Object.keys(maps).find(prefix => f.includes(prefix))
18 |   if (found) prefixes.add(maps[found])
19 | })
20 | 
21 | if (prefixes.size) {
22 |   const prefix = [...prefixes.values()].sort().join(", ")
23 |   const msg = fs.readFileSync(fileToEdit, "utf8")
24 |   if (!msg.includes(prefix)) {
25 |     fs.writeFileSync(fileToEdit, `[${prefix}] ${msg}`)
26 |   }
27 | }
28 | 
29 | -------------------------------------------------------------------------------- /test/fixtures/handbook-options/input.html: -------------------------------------------------------------------------------- 1 |

Handbook options

2 |

Errors

3 |
// @errors: 2322 2588
 4 | const str: string = 1
 5 | str = 'Hello'
 6 | 
7 | 8 |

noErrors

9 |
// @noErrors
10 | const str: string = 1
11 | str = 'Hello'
12 | 
13 | 14 |

noErrorsCutted

15 |
// @noErrorsCutted
16 | const hello = 'world'
17 | // ---cut-after---
18 | hello = 'hi' // Supposed to be an error, but ignored because it's cutted.
19 | 
20 | 21 |

noErrorValidation

22 |
// @noErrorValidation
23 | const str: string = 1
24 | 
25 | 26 |

keepNotations

27 |
// @keepNotations
28 | // @module: esnext
29 | // @errors: 2322
30 | const str: string = 1
31 | 
32 | -------------------------------------------------------------------------------- /test/fixtures/completion/input.html: -------------------------------------------------------------------------------- 1 |

A regular completion annotation.

2 |
// @noErrors
 3 | console.e
 4 | //       ^|
 5 | 
6 | 7 |

Another, with existing extra characters.

8 |
// @noErrors
 9 | console.err
10 | //        ^|
11 | console.err
12 | //       ^|
13 | console.err
14 | //      ^|
15 | console.err
16 | //     ^|
17 | 
18 | 19 |

Another, with non-existing extra characters.

20 |
// @noErrors
21 | console.exa
22 | //        ^|
23 | console.exa
24 | //       ^|
25 | console.exa
26 | //      ^|
27 | console.exa
28 | //     ^|
29 | 
30 | 31 |

A completion annotation that completes a deprecated value.

32 |
// @noErrors
33 | const rule = new CSSRule()
34 | console.log(rule.ty)
35 | //                ^|
36 | 
37 | 38 |

A completion annotation, not at the end.

39 |
// @noErrors
40 | console.t
41 | // ^|
42 | 
43 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2024 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/fixtures/basic/input.html: -------------------------------------------------------------------------------- 1 |
console.log(1)
 2 | 
3 |
console.log(1)
 4 | 
5 |
console.log(1)
 6 | 
7 |
em { color: red }
 8 | 
9 |
# hi
10 | 
11 |
# hi
12 | 
13 |
# hi
14 | 
15 |
# hi
16 | 
17 |
x
18 | 
19 |

20 | 
21 |
type A<B> = {
22 |     str: string
23 |     b: B
24 | }
25 | 
26 |
type A<B> = {
27 |     str: "one" | "two"
28 |     b: B
29 | }
30 | 
31 |
let fragment = <jsx />
32 | 
33 |
let fragment = <jsx />
34 | 
35 |
// @errors: 1005 1161 2304
36 | let fragment = <jsx />
37 | 
38 |
// @errors: 1005 1161 2304 7026
39 | let fragment = <jsx />
40 | 
41 | -------------------------------------------------------------------------------- /test/fixtures/options/input.html: -------------------------------------------------------------------------------- 1 |

Options

2 |

noImplicitAny

3 |
// @noImplicitAny: false
 4 | // @target: esnext
 5 | // @lib: esnext
 6 | // This suppose to throw an error,
 7 | // but it won’t because we disabled noImplicitAny.
 8 | const fn = a => a + 1
 9 | 
10 |

showEmit

11 |
// @showEmit
12 | const level: string = 'Danger'
13 | 
14 |

showEmittedFile

15 |
// @declaration
16 | // @showEmit
17 | // @showEmittedFile: index.d.ts
18 | export const hello = 'world'
19 | 
20 |

showEmittedFile (index.js.map)

21 |
// @sourceMap
22 | // @showEmit
23 | // @showEmittedFile: index.js.map
24 | export const hello = 'world'
25 | 
26 |

showEmittedFile (index.d.ts.map)

27 |
// @declaration
28 | // @declarationMap
29 | // @showEmit
30 | // @showEmittedFile: index.d.ts.map
31 | export const hello: string = 'world'
32 | 
33 |

showEmittedFile (js)

34 |
// @showEmit
35 | // @showEmittedFile: b.js
36 | // @filename: a.ts
37 | export const helloWorld: string = 'Hi'
38 | 
39 | // @filename: b.ts
40 | import { helloWorld } from './a'
41 | console.log(helloWorld)
42 | 
43 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prefer-query-selector */ 2 | /* eslint-env browser */ 3 | /// 4 | 5 | // @ts-ignore -- types aren’t inferred in VS Code for me. 6 | import {computePosition, shift} from 'https://esm.sh/@floating-ui/dom@1' 7 | 8 | const popoverTargets = /** @type {Array} */ ( 9 | Array.from(document.querySelectorAll('.rehype-twoslash-popover-target')) 10 | ) 11 | 12 | for (const popoverTarget of popoverTargets) { 13 | /** @type {NodeJS.Timeout | number} */ 14 | let timeout = 0 15 | 16 | popoverTarget.addEventListener('click', function () { 17 | show(popoverTarget) 18 | }) 19 | 20 | popoverTarget.addEventListener('mouseenter', function () { 21 | clearTimeout(timeout) 22 | timeout = setTimeout(function () { 23 | show(popoverTarget) 24 | }, 300) 25 | }) 26 | 27 | popoverTarget.addEventListener('mouseleave', function () { 28 | clearTimeout(timeout) 29 | }) 30 | 31 | if (popoverTarget.classList.contains('rehype-twoslash-autoshow')) { 32 | show(popoverTarget) 33 | } 34 | } 35 | 36 | /** 37 | * @param {HTMLElement} popoverTarget 38 | */ 39 | function show(popoverTarget) { 40 | const id = popoverTarget.dataset.popoverTarget 41 | if (!id) return 42 | const popover = document.getElementById(id) 43 | if (!popover) return 44 | 45 | popover.showPopover() 46 | 47 | computePosition(popoverTarget, popover, { 48 | placement: 'bottom', 49 | middleware: [shift({padding: 5})] 50 | }).then( 51 | /** 52 | * @param {{x: number, y: number}} value 53 | */ 54 | function (value) { 55 | popover.style.left = value.x + 'px' 56 | popover.style.top = value.y + 'px' 57 | } 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /test/fixtures/tag-without-text/output.html: -------------------------------------------------------------------------------- 1 |
2 |
console.log(name)
3 |
var console: Console
4 |
(method) Console.log(...data: any[]): void
5 |
const name: void
    6 |
  • @deprecated
  • 7 |
8 |
9 | -------------------------------------------------------------------------------- /demo/build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {ElementContent, Root} from 'hast' 3 | */ 4 | 5 | import rehypeDocument from 'rehype-document' 6 | import rehypeStringify from 'rehype-stringify' 7 | import rehypeTwoslash from 'rehype-twoslash' 8 | import remarkParse from 'remark-parse' 9 | import remarkRehype from 'remark-rehype' 10 | import {unified} from 'unified' 11 | import {read, write} from 'to-vfile' 12 | import {reporter} from 'vfile-reporter' 13 | 14 | const css = String(await read(new URL('index.css', import.meta.url))) 15 | const js = String(await read(new URL('index.js', import.meta.url))) 16 | const file = await read(new URL('index.md', import.meta.url)) 17 | 18 | await unified() 19 | .use(remarkParse) 20 | .use(remarkRehype) 21 | .use(rehypeTwoslash) 22 | .use(main) 23 | .use(rehypeDocument, { 24 | css: [ 25 | 'https://esm.sh/github-markdown-css@5/github-markdown.css', 26 | 'https://esm.sh/@wooorm/starry-night@3/style/both.css' 27 | ], 28 | style: css, 29 | title: '`rehype-twoslash` demo' 30 | }) 31 | .use(rehypeStringify) 32 | .process(file) 33 | 34 | file.extname = '.html' 35 | await write(file) 36 | file.stored = true 37 | 38 | console.error(reporter(file)) 39 | 40 | function main() { 41 | /** 42 | * @param {Root} tree 43 | */ 44 | return function (tree) { 45 | tree.children = [ 46 | { 47 | type: 'comment', 48 | value: 49 | ' note: this file is generated by `build.js`, which generates HTML from `index.md` and includes `index.css` and `index.js`. ' 50 | }, 51 | { 52 | type: 'element', 53 | tagName: 'main', 54 | properties: {}, 55 | children: [ 56 | { 57 | type: 'element', 58 | tagName: 'div', 59 | properties: {className: ['markdown-body']}, 60 | children: /** @type {Array} */ (tree.children) 61 | } 62 | ] 63 | }, 64 | { 65 | type: 'element', 66 | tagName: 'script', 67 | properties: {type: 'module'}, 68 | children: [{type: 'text', value: js}] 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/shingle.js: -------------------------------------------------------------------------------- 1 | import {unicodePunctuation, unicodeWhitespace} from 'micromark-util-character' 2 | 3 | /** 4 | * @param {string} value 5 | * Value. 6 | * @param {Map} counts 7 | * Counts. 8 | * @returns {string} 9 | * Hash. 10 | */ 11 | export function smallShingleHash(value, counts) { 12 | let hash = smallShingle(value) 13 | let count = counts.get(hash) 14 | 15 | if (count === undefined) { 16 | count = 0 17 | } else { 18 | count++ 19 | } 20 | 21 | counts.set(hash, count) 22 | if (count) hash += '-' + count 23 | return hash 24 | } 25 | 26 | /** 27 | * @param {string} value 28 | * Value. 29 | * @returns {string} 30 | * Hash. 31 | */ 32 | function smallShingle(value) { 33 | const size = 4 34 | /** @type {Array} */ 35 | const firstLettersOfFirstWords = [] 36 | /** @type {Array} */ 37 | const firstLettersOfLastWords = [] 38 | let index = -1 39 | /** @type {boolean | undefined} */ 40 | let atWord = true 41 | 42 | while (++index < value.length) { 43 | const code = value.charCodeAt(index) 44 | 45 | if (unicodePunctuation(code)) { 46 | // Empty. 47 | } else if (unicodeWhitespace(code)) { 48 | atWord = true 49 | } else if (atWord) { 50 | firstLettersOfFirstWords.push(String.fromCharCode(code)) 51 | if (firstLettersOfFirstWords.length >= size) break 52 | atWord = false 53 | } else { 54 | // Ignore other letters. 55 | } 56 | } 57 | 58 | index = value.length 59 | atWord = true 60 | /** @type {number | undefined} */ 61 | let lastLetterCode 62 | 63 | while (index--) { 64 | const code = value.charCodeAt(index) 65 | 66 | if (unicodePunctuation(code)) { 67 | // Empty. 68 | } else if (unicodeWhitespace(code)) { 69 | if (lastLetterCode) { 70 | firstLettersOfLastWords.push(String.fromCharCode(lastLetterCode)) 71 | lastLetterCode = undefined 72 | if (firstLettersOfLastWords.length >= size) break 73 | } 74 | } else { 75 | lastLetterCode = code 76 | } 77 | } 78 | 79 | if (lastLetterCode) { 80 | firstLettersOfLastWords.push(String.fromCharCode(lastLetterCode)) 81 | } 82 | 83 | firstLettersOfLastWords.reverse() 84 | 85 | return firstLettersOfFirstWords.join('') + firstLettersOfLastWords.join('') 86 | } 87 | -------------------------------------------------------------------------------- /test/fixtures/extract/output.html: -------------------------------------------------------------------------------- 1 |
2 |
const hi = 'Hello'
 3 | const msg = `${hi}, world`
 4 | 
5 |
const hi: "Hello"
6 |
const msg: "Hello, world"
7 |
const msg: "Hello, world"
8 |
const hi: "Hello"
9 |
10 | -------------------------------------------------------------------------------- /test/fixtures/highlight/output.html: -------------------------------------------------------------------------------- 1 |
2 |
function add(a: number, b: number) {
 3 |   return a + b
 4 | }
 5 | 
6 |
function add(a: number, b: number): number
7 |
(parameter) a: number
8 |
(parameter) b: number
9 |
(parameter) a: number
10 |
(parameter) b: number
11 |
12 | -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-canvas-back: #f6f8fa; 3 | --color-canvas-front: #ffffff; 4 | --color-border: #d0d7de; 5 | --color-hl: #0969da; 6 | --color-fg: #0d1117; 7 | 8 | color-scheme: light dark; 9 | font-family: system-ui; 10 | background-color: var(--color-canvas-back); 11 | color: var(--color-fg); 12 | word-break: break-word; 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --color-canvas-back: #0d1117; 18 | --color-canvas-front: #161b22; 19 | --color-border: #30363d; 20 | --color-hl: #58a6ff; 21 | --color-fg: #f6f8fa; 22 | } 23 | } 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | main { 30 | background-color: var(--color-canvas-back); 31 | position: relative; 32 | margin: 0 auto; 33 | padding: calc(2 * (1em + 1ex)); 34 | max-width: calc(40 * (1em + 1ex)); 35 | } 36 | 37 | @media (min-width: calc(10 * (1em + 1ex))) and (min-height: calc(10 * (1em + 1ex))) { 38 | main { 39 | /* Go all Tschichold when supported */ 40 | margin: 11vh 22.2vw 22.2vh 11.1vw; 41 | border: 1px solid var(--color-border); 42 | border-radius: 3px; 43 | } 44 | } 45 | 46 | * { 47 | line-height: calc(1 * (1em + 1ex)); 48 | box-sizing: border-box; 49 | } 50 | 51 | .markdown-body { 52 | background-color: transparent; 53 | } 54 | 55 | /* Reset the list for `github-markdown-css`. */ 56 | .rehype-twoslash-completions { 57 | list-style-type: none !important; 58 | padding-left: 0 !important; 59 | margin-bottom: 0 !important; 60 | } 61 | 62 | /* Lowlight deprecated suggestions. */ 63 | .rehype-twoslash-completion-deprecated { 64 | opacity: 0.5; 65 | } 66 | 67 | /* Regular “button” cursor instead of text selection cursor. */ 68 | .rehype-twoslash-popover-target { 69 | cursor: default; 70 | } 71 | 72 | /* Show what can be interacted with. */ 73 | .highlight:is(:hover, :focus-within) .rehype-twoslash-popover-target { 74 | background-color: var(--bgColor-neutral-muted); 75 | } 76 | 77 | /* Wavy underline for errors. */ 78 | .rehype-twoslash-error-target { 79 | background-repeat: repeat-x; 80 | background-position: bottom left; 81 | background-image: url('data:image/svg+xml,'); 82 | } 83 | 84 | /* The content that will be shown in the tooltip. */ 85 | .rehype-twoslash-popover { 86 | position: absolute; 87 | max-width: calc(45 * (1em + 1ex)); 88 | padding: calc(0.5 * (1em + 1ex)); 89 | margin: 0; 90 | background-color: var(--color-canvas-front); 91 | border: 1px solid var(--color-border); 92 | border-radius: 3px; 93 | } 94 | 95 | /* No padding if we have a padded code block (and perhaps more blocks) */ 96 | .rehype-twoslash-popover:has(.rehype-twoslash-popover-code) { 97 | padding: 0; 98 | } 99 | 100 | /* Docs for type info. */ 101 | .rehype-twoslash-popover-description { 102 | background-color: var(--color-canvas-back); 103 | padding: 1em; 104 | } 105 | 106 | /* Reset extra space for `github-markdown-css`. */ 107 | .rehype-twoslash-popover-description 108 | > :last-child:is(p, blockquote, ul, ol, dl, table, pre, details) { 109 | margin-bottom: 0; 110 | } 111 | 112 | /* Extra highlight. */ 113 | .rehype-twoslash-completion-swap, 114 | .rehype-twoslash-highlight { 115 | background-color: var(--borderColor-attention-emphasis); 116 | } 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rehype-twoslash", 3 | "version": "1.1.1", 4 | "description": "rehype plugin to process TypeScript and JavaScript code with `twoslash` and highlight it with `starry-night`", 5 | "license": "MIT", 6 | "keywords": [ 7 | "hast", 8 | "highlight", 9 | "html", 10 | "plugin", 11 | "rehype", 12 | "rehype-plugin", 13 | "syntax", 14 | "twoslash", 15 | "typescript", 16 | "unified" 17 | ], 18 | "repository": "rehypejs/rehype-twoslash", 19 | "bugs": "https://github.com/rehypejs/rehype-twoslash/issues", 20 | "funding": { 21 | "type": "opencollective", 22 | "url": "https://opencollective.com/unified" 23 | }, 24 | "author": "Titus Wormer (https://wooorm.com)", 25 | "contributors": [ 26 | "Titus Wormer (https://wooorm.com)" 27 | ], 28 | "sideEffects": false, 29 | "type": "module", 30 | "exports": "./index.js", 31 | "files": [ 32 | "lib/", 33 | "index.d.ts", 34 | "index.js" 35 | ], 36 | "dependencies": { 37 | "@types/hast": "^3.0.0", 38 | "@types/mdast": "^4.0.0", 39 | "@wooorm/starry-night": "^3.0.0", 40 | "devlop": "^1.0.0", 41 | "hast-util-to-string": "^3.0.0", 42 | "mdast-util-from-markdown": "^2.0.0", 43 | "mdast-util-gfm": "^3.0.0", 44 | "mdast-util-to-hast": "^13.0.0", 45 | "micromark-extension-gfm": "^3.0.0", 46 | "micromark-util-character": "^2.0.0", 47 | "twoslash": "^0.2.0", 48 | "unist-util-remove-position": "^5.0.0", 49 | "unist-util-visit-parents": "^6.0.0", 50 | "vfile": "^6.0.0" 51 | }, 52 | "#": "`@typescript/vfs` currently barfs when building because `lz-string` is missing, so it’s installed here.", 53 | "devDependencies": { 54 | "@types/node": "^22.0.0", 55 | "c8": "^10.0.0", 56 | "lz-string": "^1.0.0", 57 | "prettier": "^3.0.0", 58 | "rehype-document": "^7.0.0", 59 | "rehype-parse": "^9.0.0", 60 | "rehype-stringify": "^10.0.0", 61 | "remark-api": "^1.0.0", 62 | "remark-cli": "^12.0.0", 63 | "remark-parse": "^11.0.0", 64 | "remark-preset-wooorm": "^10.0.0", 65 | "remark-rehype": "^11.0.0", 66 | "to-vfile": "^8.0.0", 67 | "type-coverage": "^2.0.0", 68 | "typescript": "^5.0.0", 69 | "vfile-reporter": "^8.0.0", 70 | "xo": "^0.59.0" 71 | }, 72 | "scripts": { 73 | "build": "tsc --build --clean && tsc --build && type-coverage", 74 | "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", 75 | "prepack": "npm run build && npm run format", 76 | "test": "npm run build && npm run format && npm run test-coverage", 77 | "test-api": "node --conditions development test/index.js", 78 | "test-coverage": "c8 --100 --check-coverage --reporter lcov npm run test-api" 79 | }, 80 | "prettier": { 81 | "bracketSpacing": false, 82 | "singleQuote": true, 83 | "semi": false, 84 | "tabWidth": 2, 85 | "trailingComma": "none", 86 | "useTabs": false 87 | }, 88 | "remarkConfig": { 89 | "plugins": [ 90 | "remark-preset-wooorm", 91 | "remark-api", 92 | [ 93 | "remark-lint-no-html", 94 | false 95 | ] 96 | ] 97 | }, 98 | "typeCoverage": { 99 | "atLeast": 100, 100 | "detail": true, 101 | "ignoreCatch": true, 102 | "strict": true 103 | }, 104 | "xo": { 105 | "overrides": [ 106 | { 107 | "files": [ 108 | "**/*.d.ts" 109 | ], 110 | "rules": { 111 | "@typescript-eslint/array-type": [ 112 | "error", 113 | { 114 | "default": "generic" 115 | } 116 | ], 117 | "@typescript-eslint/ban-types": [ 118 | "error", 119 | { 120 | "extendDefaults": true 121 | } 122 | ], 123 | "@typescript-eslint/consistent-type-definitions": [ 124 | "error", 125 | "interface" 126 | ] 127 | } 128 | }, 129 | { 130 | "files": [ 131 | "test/**/*.js" 132 | ], 133 | "rules": { 134 | "no-await-in-loop": "off" 135 | } 136 | } 137 | ], 138 | "prettier": true, 139 | "rules": { 140 | "complexity": "off", 141 | "max-depth": "off", 142 | "unicorn/prefer-code-point": "off", 143 | "unicorn/prefer-string-replace-all": "off" 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /test/fixtures/options/output.html: -------------------------------------------------------------------------------- 1 |

Options

2 |

noImplicitAny

3 |
4 |
// This suppose to throw an error,
 5 | // but it won’t because we disabled noImplicitAny.
 6 | const fn = a => a + 1
 7 | 
8 |
const fn: (a: any) => any
9 |
(parameter) a: any
10 |
(parameter) a: any
11 |
12 |

showEmit

13 |
14 |
const level = 'Danger';
15 | export {};
16 | 
17 |
18 |

showEmittedFile

19 |
20 |
export declare const hello = "world";
21 | 
22 |
23 |

showEmittedFile (index.js.map)

24 |
25 |
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,KAAK,GAAG,OAAO,CAAA"}
26 |
27 |

showEmittedFile (index.d.ts.map)

28 |
29 |
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,EAAE,MAAgB,CAAA"}
30 |
31 |

showEmittedFile (js)

32 |
33 |
// @filename: b.ts
34 | import { helloWorld } from './a';
35 | console.log(helloWorld);
36 | 
37 |
38 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | import type {Grammar, createStarryNight} from '@wooorm/starry-night' 2 | import type { 3 | NodeCompletion, 4 | NodeError, 5 | NodeHighlight, 6 | NodeHover, 7 | NodeQuery, 8 | TwoslashOptions 9 | } from 'twoslash' 10 | import type {ElementContent, Element, Text, Root} from 'hast' 11 | 12 | /** 13 | * Twoslash annotations (called nodes there) by their `type` field, 14 | * excluding `tag` (which I can’t reproduce). 15 | */ 16 | export interface AnnotationsMap { 17 | /** 18 | * Completion. 19 | */ 20 | completion: NodeCompletion 21 | /** 22 | * Error. 23 | */ 24 | error: NodeError 25 | /** 26 | * Highlight. 27 | */ 28 | highlight: NodeHighlight 29 | /** 30 | * Hover. 31 | */ 32 | hover: NodeHover 33 | /** 34 | * Query. 35 | */ 36 | query: NodeQuery 37 | } 38 | 39 | /** 40 | * Match. 41 | */ 42 | export interface MatchText { 43 | /** 44 | * Node that includes match. 45 | */ 46 | node: Text 47 | /** 48 | * Indices. 49 | */ 50 | range: Range 51 | /** 52 | * Parents. 53 | */ 54 | stack: [Root, ...Array] 55 | } 56 | 57 | /** 58 | * Match. 59 | */ 60 | export interface MatchParent { 61 | /** 62 | * Indices. 63 | */ 64 | range: Range 65 | /** 66 | * Parents. 67 | */ 68 | stack: [Root, ...Array, Text] | [Root, ...Array] 69 | } 70 | 71 | /** 72 | * Configuration for `rehype-twoslash`. 73 | * 74 | * ###### Notes 75 | * 76 | * `rehype-twoslash` runs on `` elements with a `twoslash` directive. 77 | * That directive can be passed as a word in markdown (` ```ts twoslash `) or 78 | * as a class in HTML (``). 79 | * 80 | * The inverse occurs when `directive` is `false`. 81 | * All `` where the language class is JavaScript or TypeScript is 82 | * processed. 83 | * Then `no-twoslash` (` ```ts no-twoslash `, 84 | * ``) can be used to prevent processing. 85 | */ 86 | export interface Options { 87 | /** 88 | * Whether to require a `twoslash` directive (default: `true`). 89 | */ 90 | directive?: boolean | null | undefined 91 | /** 92 | * Prefix before IDs (default: `'rehype-twoslash-'`). 93 | */ 94 | idPrefix?: string | null | undefined 95 | /** 96 | * Grammars for `starry-night` to support (default: 97 | * `[sourceJson, sourceJs, sourceTsx, sourceTs]`). 98 | */ 99 | grammars?: ReadonlyArray | null | undefined 100 | /** 101 | * Renderers for `twoslash` annotations (optional). 102 | */ 103 | renderers?: Renderers | null | undefined 104 | /** 105 | * Options passed to `twoslash` (optional); 106 | * this includes fields such as `cache`, 107 | * `customTransformers`, 108 | * and `filterNode`; 109 | * see 110 | * [`TwoslashOptions` from `twoslash`](https://github.com/twoslashes/twoslash/blob/1eb3af3/packages/twoslash/src/types/options.ts#L18) 111 | * for more info. 112 | */ 113 | twoslash?: TwoslashOptions | null | undefined 114 | } 115 | 116 | /** 117 | * Two indices. 118 | */ 119 | export type Range = [from: number, to: number] 120 | 121 | /** 122 | * Render function. 123 | * 124 | * Takes a particular annotation from the TypeScript compiler (such as an error) 125 | * and turns it into `hast` (HTML) content. 126 | * See `lib/render.js` for examples. 127 | * 128 | * ###### Notes 129 | * 130 | * You can return `Array` directly instead of a {@linkcode RenderResult} 131 | * when you don’t have content for a footer. 132 | * 133 | * @param state 134 | * Current state. 135 | * @param annotation 136 | * Annotation. 137 | * @param children 138 | * Matched children. 139 | * @returns 140 | * New children. 141 | */ 142 | export type Render = ( 143 | state: State, 144 | annotation: Annotation, 145 | children: Array 146 | ) => Array | RenderResult 147 | 148 | /** 149 | * Result from {@linkcode Render}. 150 | */ 151 | export interface RenderResult { 152 | /** 153 | * Main inline content to use in the code block; 154 | * for example a `` that causes a tooltip to show. 155 | */ 156 | content?: Array | undefined 157 | /** 158 | * Extra content to use that relates to the code block; 159 | * for example a `
` for a tooltip. 160 | */ 161 | footer?: Array | undefined 162 | } 163 | 164 | /** 165 | * Renderers. 166 | * 167 | * Each key is a type of an annotation (such as `error` or `hover`) and each 168 | * value a corresponding render function. 169 | */ 170 | export type Renderers = { 171 | [Type in keyof AnnotationsMap]?: 172 | | Render 173 | | null 174 | | undefined 175 | } 176 | 177 | /** 178 | * `starryNight` instance. 179 | */ 180 | type StarryNight = Awaited> 181 | 182 | /** 183 | * Info passed around. 184 | */ 185 | export interface State { 186 | /** 187 | * Current unique ID count. 188 | */ 189 | count: number 190 | /** 191 | * Prefix for all IDs relating to this code block on this page. 192 | */ 193 | idPrefix: string 194 | /** 195 | * Renderers. 196 | */ 197 | renderers: { 198 | [Type in keyof AnnotationsMap]: Render 199 | } 200 | /** 201 | * Current `starryNight` instance. 202 | */ 203 | starryNight: StarryNight 204 | } 205 | -------------------------------------------------------------------------------- /lib/annotate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {ElementContent, Element, Root} from 'hast' 3 | * @import {TwoslashNode} from 'twoslash' 4 | * @import {MatchText, MatchParent, State, RenderResult} from './types.js' 5 | */ 6 | 7 | import {ok as assert} from 'devlop' 8 | import {EXIT, visitParents} from 'unist-util-visit-parents' 9 | 10 | /** 11 | * @param {Root} tree 12 | * Tree. 13 | * @param {TwoslashNode} annotation 14 | * Annotation. 15 | * @param {State} state 16 | * Info pased around. 17 | * @returns {Array} 18 | * Extra content for footer. 19 | */ 20 | export function annotate(tree, annotation, state) { 21 | /** @type {Array} */ 22 | const allFooter = [] 23 | const matches = normalizeMatches(findMatches(tree, annotation)) 24 | 25 | let match = matches.pop() 26 | while (match) { 27 | const parent = match.stack.at(-1) 28 | assert(parent) 29 | 30 | /** @type {Array} */ 31 | const before = 32 | 'value' in parent && match.range[0] !== 0 33 | ? [{type: 'text', value: parent.value.slice(0, match.range[0])}] 34 | : [] 35 | /** @type {Array} */ 36 | const between = 37 | 'value' in parent 38 | ? [ 39 | { 40 | type: 'text', 41 | value: parent.value.slice(match.range[0], match.range[1]) 42 | } 43 | ] 44 | : // Cast because we never have comments. 45 | /** @type {Array} */ ( 46 | parent.children.slice(match.range[0], match.range[1]) 47 | ) 48 | /** @type {Array} */ 49 | const after = 50 | 'value' in parent && match.range[1] !== parent.value.length 51 | ? [{type: 'text', value: parent.value.slice(match.range[1])}] 52 | : [] 53 | 54 | const renderResult = render(state, annotation, between) 55 | 56 | /** @type {Array} */ 57 | const allContent = [...before] 58 | 59 | if (renderResult.content) { 60 | allContent.push(...renderResult.content) 61 | } 62 | 63 | if (renderResult.footer) { 64 | allFooter.push({type: 'text', value: '\n'}, ...renderResult.footer) 65 | } 66 | 67 | allContent.push(...after) 68 | 69 | if (parent.type === 'text') { 70 | const grandParent = match.stack.at(-2) 71 | assert(grandParent) 72 | assert('children' in grandParent) 73 | grandParent.children.splice( 74 | grandParent.children.indexOf(parent), 75 | 1, 76 | ...allContent 77 | ) 78 | } else { 79 | parent.children.splice( 80 | match.range[0], 81 | match.range[1] - match.range[0], 82 | ...allContent 83 | ) 84 | } 85 | 86 | match = matches.pop() 87 | } 88 | 89 | return allFooter 90 | } 91 | 92 | /** 93 | * @param {Root} tree 94 | * @param {TwoslashNode} annotation 95 | * @returns {Array} 96 | */ 97 | function findMatches(tree, annotation) { 98 | let nodeStart = 0 99 | const annotationStart = annotation.start 100 | const annotationEnd = 101 | // Use a single character for empty annotations. 102 | annotationStart + (annotation.length === 0 ? 1 : annotation.length) 103 | /** @type {Array} */ 104 | const matches = [] 105 | 106 | visitParents(tree, 'text', function (node, stack_) { 107 | const stack = /** @type {[Root, ...Array]} */ (stack_) 108 | const nodeEnd = nodeStart + node.value.length 109 | 110 | if (annotationStart < nodeEnd && annotationEnd > nodeStart) { 111 | matches.push({ 112 | node, 113 | stack, 114 | range: [ 115 | Math.max(annotationStart - nodeStart, 0), 116 | Math.min(annotationEnd - nodeStart, node.value.length) 117 | ] 118 | }) 119 | } 120 | 121 | // Done if we’re past the annotation. 122 | if (nodeEnd > annotationEnd) { 123 | return EXIT 124 | } 125 | 126 | nodeStart = nodeEnd 127 | }) 128 | 129 | return matches 130 | } 131 | 132 | /** 133 | * @param {ReadonlyArray} matches 134 | * @returns {Array} 135 | */ 136 | function normalizeMatches(matches) { 137 | /** @type {Array} */ 138 | const result = [] 139 | 140 | for (const match_ of matches) { 141 | /** @type {MatchParent} */ 142 | let match = {stack: [...match_.stack, match_.node], range: match_.range} 143 | let node = match.stack.at(-1) 144 | 145 | while ( 146 | node && 147 | node.type !== 'root' && 148 | match.range[0] === 0 && 149 | ('children' in node 150 | ? match.range[1] === node.children.length 151 | : match.range[1] === node.value.length) 152 | ) { 153 | // Cannot be `text`. 154 | const nextStack = /** @type {[Root, ...Array]} */ ( 155 | match.stack.slice(0, -1) 156 | ) 157 | // Cannot be undefined either (as it’s not a `root`). 158 | const nextNode = nextStack.at(-1) 159 | assert(nextNode) 160 | const position = nextNode.children.indexOf(node) 161 | match = { 162 | range: [position, position + 1], 163 | stack: nextStack 164 | } 165 | node = nextNode 166 | } 167 | 168 | // See if we can merge: 169 | const previous = result.at(-1) 170 | const previousParent = previous ? previous.stack.at(-1) : undefined 171 | const parent = match.stack.at(-1) 172 | 173 | if (previous && previousParent && parent === previousParent) { 174 | previous.range[1] = match.range[1] 175 | } else { 176 | result.push(match) 177 | } 178 | } 179 | 180 | return result 181 | } 182 | 183 | /** 184 | * @param {State} state 185 | * @param {TwoslashNode} annotation 186 | * @param {Array} between 187 | * @returns {RenderResult} 188 | */ 189 | function render(state, annotation, between) { 190 | assert(annotation.type !== 'tag') // Seems to never happen. 191 | // @ts-expect-error: renderer matches annotation. 192 | const result = state.renderers[annotation.type](state, annotation, between) 193 | return Array.isArray(result) ? {content: result, footer: undefined} : result 194 | } 195 | -------------------------------------------------------------------------------- /test/fixtures/handbook-options/output.html: -------------------------------------------------------------------------------- 1 |

Handbook options

2 |

Errors

3 |
4 |
const str: string = 1
 5 | str = 'Hello'
 6 | 
7 |
Type 'number' is not assignable to type 'string'. (2322)
8 |
const str: string
9 |
Cannot assign to 'str' because it is a constant. (2588)
10 |
const str: any
11 |
12 | 13 |

noErrors

14 |
15 |
const str: string = 1
16 | str = 'Hello'
17 | 
18 |
const str: string
19 |
const str: any
20 |
21 | 22 |

noErrorsCutted

23 |
24 |
const hello = 'world'
25 | 
26 |
const hello: "world"
27 |
28 | 29 |

noErrorValidation

30 |
31 |
const str: string = 1
32 | 
33 |
Type 'number' is not assignable to type 'string'. (2322)
34 |
const str: string
35 |
36 | 37 |

keepNotations

38 |
39 |
// @keepNotations
40 | // @module: esnext
41 | // @errors: 2322
42 | const str: string = 1
43 | 
44 |
Type 'number' is not assignable to type 'string'. (2322)
45 |
const str: string
46 |
47 | -------------------------------------------------------------------------------- /test/fixtures/cut/output.html: -------------------------------------------------------------------------------- 1 |
2 |
console.log(level)
 3 | 
4 |
var console: Console
5 |
(method) Console.log(...data: any[]): void
6 |
const level: string
7 |
8 | 9 |
10 |
// @filename: b.ts
11 | import { helloWorld } from './a'
12 | 
13 | console.log(helloWorld)
14 | 
15 |
(alias) const helloWorld: string
16 | import helloWorld
17 |
var console: Console
18 |
(method) Console.log(...data: any[]): void
19 |
(alias) const helloWorld: string
20 | import helloWorld
21 |
22 | 23 |
24 |
console.log(level)
25 | 
26 |
var console: Console
27 |
(method) Console.log(...data: any[]): void
28 |
const level: string
29 |
30 | 31 |
32 |
const level: string = 'Danger'
33 | console.log('This is shown')
34 | 
35 |
const level: string
36 |
var console: Console
37 |
(method) Console.log(...data: any[]): void
38 |
39 | -------------------------------------------------------------------------------- /test/fixtures/errors/output.html: -------------------------------------------------------------------------------- 1 |
2 |
// Declare a tuple type
 3 | let x: [string, number]
 4 | 
 5 | // Initialize it
 6 | x = ['hello', 10]
 7 | // Initialize it incorrectly
 8 | x = [10, 'hello']
 9 | 
10 |
let x: [string, number]
11 |
let x: [string, number]
12 |
let x: [string, number]
13 |
Type 'number' is not assignable to type 'string'. (2322)
14 |
Type 'string' is not assignable to type 'number'. (2322)
15 |
16 | 17 |
18 |
console.log(x[0].substring(1))
19 | console.log(x[1].substring(1))
20 | 
21 |
var console: Console
22 |
(method) Console.log(...data: any[]): void
23 |
let x: [string, number]
24 |
(method) String.substring(start: number, end?: number): string

Returns the substring at the specified location within a String object.

25 |
    26 |
  • @param 27 | start The zero-based index number indicating the beginning of the substring.
  • 28 |
  • @param 29 | end Zero-based index number indicating the end of the substring. The substring includes the characters up to, but not including, the character indicated by end. 30 | If end is omitted, the characters from start through the end of the original string are returned.
  • 31 |
32 |
var console: Console
33 |
(method) Console.log(...data: any[]): void
34 |
let x: [string, number]
35 |
Property 'substring' does not exist on type 'number'. (2339)
36 |
any
37 |
38 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Grammar} from '@wooorm/starry-night' 3 | * @import {ElementContent, Root} from 'hast' 4 | * @import {} from 'mdast-util-to-hast' // Augmentation. 5 | * @import {Options} from 'rehype-twoslash' 6 | * @import {TwoslashNode} from 'twoslash' 7 | * @import {VFile} from 'vfile' 8 | * @import {State} from './types.js' 9 | */ 10 | 11 | import {createStarryNight} from '@wooorm/starry-night' 12 | import sourceJson from '@wooorm/starry-night/source.json' 13 | import sourceJs from '@wooorm/starry-night/source.js' 14 | import sourceTsx from '@wooorm/starry-night/source.tsx' 15 | import sourceTs from '@wooorm/starry-night/source.ts' 16 | import {ok as assert} from 'devlop' 17 | import {toString} from 'hast-util-to-string' 18 | import {createTwoslasher} from 'twoslash' 19 | import {SKIP, visitParents} from 'unist-util-visit-parents' 20 | import {annotate} from './annotate.js' 21 | import {smallShingleHash} from './shingle.js' 22 | import { 23 | completion as defaultCompletion, 24 | error as defaultError, 25 | highlight as defaultHighlight, 26 | hover as defaultHover, 27 | query as defaultQuery 28 | } from './render.js' 29 | 30 | /** @type {Readonly} */ 31 | const defaultOptions = {} 32 | 33 | /** 34 | * Default grammars. 35 | * 36 | * @type {ReadonlyArray>} 37 | */ 38 | // @ts-expect-error: `source.json` is incorrectly seen by TypeScript as JSON. 39 | const defaultGrammars = [sourceJson, sourceJs, sourceTsx, sourceTs] 40 | 41 | /** 42 | * Map files generated by the TS compiler to `starry-night` scopes. 43 | */ 44 | const extensionToScopeMap = new Map([ 45 | ['json', 'source.json'], 46 | ['js', 'source.js'], 47 | ['tsx', 'source.tsx'], 48 | ['ts', 'source.ts'] 49 | ]) 50 | 51 | /** 52 | * Prefix for language classes. 53 | */ 54 | const prefix = 'language-' 55 | 56 | /** 57 | * Plugin to process JavaScript and TypeScript code with `twoslash` 58 | * and highlight it with `starry-night`. 59 | * 60 | * @param {Readonly | null | undefined} [options] 61 | * Configuration (optional). 62 | * @returns 63 | * Transform. 64 | */ 65 | export default function rehypeTwoslash(options) { 66 | const settings = options || defaultOptions 67 | const renderers = settings.renderers || {} 68 | const directive = 69 | typeof settings.directive === 'boolean' ? settings.directive : true 70 | const idPrefix = settings.idPrefix || 'rehype-twoslash-' 71 | const starryNightPromise = createStarryNight( 72 | settings.grammars || defaultGrammars 73 | ) 74 | const twoslash = createTwoslasher(settings.twoslash || undefined) 75 | 76 | /** 77 | * Transform. 78 | * 79 | * @param {Root} tree 80 | * Tree. 81 | * @param {VFile} file 82 | * File. 83 | * @returns {Promise} 84 | * Given tree. 85 | */ 86 | return async function (tree, file) { 87 | const starryNight = await starryNightPromise 88 | /** @type {Map} */ 89 | const hashToCount = new Map() 90 | 91 | visitParents(tree, 'element', function (node, parents) { 92 | const parent = parents.at(-1) 93 | /* c8 ignore next - element at root never happens with `unified`; only if you do it manually. */ 94 | const index = parent ? parent.children.indexOf(node) : undefined 95 | 96 | if (!parent || index === undefined || node.tagName !== 'pre') { 97 | return 98 | } 99 | 100 | const head = node.children[0] 101 | 102 | if (!head || head.type !== 'element' || head.tagName !== 'code') { 103 | return SKIP 104 | } 105 | 106 | const classes = head.properties.className 107 | 108 | if (!Array.isArray(classes)) return 109 | 110 | const meta = head.data?.meta || '' 111 | const directiveTwoslash = 112 | classes.includes('twoslash') || meta.startsWith('twoslash') 113 | const directiveNoTwoslash = 114 | classes.includes('no-twoslash') || meta.startsWith('no-twoslash') 115 | 116 | if (classes.includes('notwoslash')) { 117 | file.message('Unexpected `notwoslash` class, expected `no-twoslash`', { 118 | ancestors: [...parents, node], 119 | place: node.position, 120 | ruleId: 'missing-dash-class', 121 | source: 'rehype-twoslash' 122 | }) 123 | } 124 | 125 | if (meta.startsWith('notwoslash')) { 126 | file.message( 127 | 'Unexpected `notwoslash` directive, expected `no-twoslash`', 128 | { 129 | ancestors: [...parents, node], 130 | place: node.position, 131 | ruleId: 'missing-dash-directive', 132 | source: 'rehype-twoslash' 133 | } 134 | ) 135 | } 136 | 137 | if (directiveNoTwoslash || (directive && !directiveTwoslash)) return 138 | 139 | // Cast as we check if it’s a string in `find`. 140 | const language = /** @type {string | undefined} */ ( 141 | classes.find(function (d) { 142 | return typeof d === 'string' && d.startsWith(prefix) 143 | }) 144 | ) 145 | 146 | let scope = language 147 | ? starryNight.flagToScope(language.slice(prefix.length)) 148 | : undefined 149 | 150 | if ( 151 | scope !== 'source.js' && 152 | scope !== 'source.ts' && 153 | scope !== 'source.tsx' 154 | ) { 155 | if (directiveTwoslash) { 156 | file.message( 157 | 'Unexpected non-js/ts code' + 158 | (scope ? ' (`' + scope + '`)' : '') + 159 | ' with twoslash directive, expected JavaScript or TypeScript code', 160 | { 161 | ancestors: [...parents, node], 162 | place: node.position, 163 | ruleId: 'non-js-ts-with-twoslash', 164 | source: 'rehype-twoslash' 165 | } 166 | ) 167 | } 168 | 169 | return SKIP 170 | } 171 | 172 | let value = toString(head) 173 | /** @type {State} */ 174 | const state = { 175 | count: -1, 176 | idPrefix: idPrefix + smallShingleHash(value, hashToCount) + '-', 177 | renderers: { 178 | completion: renderers.completion || defaultCompletion, 179 | error: renderers.error || defaultError, 180 | highlight: renderers.highlight || defaultHighlight, 181 | hover: renderers.hover || defaultHover, 182 | query: renderers.query || defaultQuery 183 | }, 184 | starryNight 185 | } 186 | /** @type {Array} */ 187 | let annotations = [] 188 | 189 | try { 190 | const result = twoslash( 191 | value, 192 | scope === 'source.js' ? 'js' : scope === 'source.tsx' ? 'tsx' : 'ts' 193 | ) 194 | value = result.code 195 | annotations = result.nodes 196 | scope = extensionToScopeMap.get(result.meta.extension) 197 | } catch (error) { 198 | const cause = /** @type {Error} */ (error) 199 | file.message('Unexpected error running twoslash', { 200 | ancestors: [...parents, node], 201 | cause, 202 | place: node.position, 203 | ruleId: 'twoslash', 204 | source: 'rehype-twoslash' 205 | }) 206 | } 207 | 208 | assert(scope) 209 | const fragment = starryNight.highlight(value, scope) 210 | /** @type {Array} */ 211 | const footer = [] 212 | /** @type {TwoslashNode | undefined} */ 213 | let previous 214 | 215 | for (const annotation of annotations) { 216 | let skip = false 217 | 218 | // Tags are zero length, so not sure how to render them. 219 | if (annotation.type === 'tag') { 220 | skip = true 221 | } 222 | 223 | // Drop the `hover`, which is likely `any` or at least presumably irrelevant, 224 | // when a completion follows. 225 | // For example: 226 | // ```ts 227 | // console.e 228 | // ^| 229 | // ``` 230 | // There would be a hover of `any` on the `e` and a completion right 231 | // after it, which is likely relevant. 232 | if ( 233 | previous && 234 | previous.type === 'completion' && 235 | annotation.type === 'hover' && 236 | previous.length === 0 && 237 | previous.start >= annotation.start && 238 | previous.start <= annotation.start + annotation.length 239 | ) { 240 | skip = true 241 | } 242 | 243 | if (!skip) footer.push(...annotate(fragment, annotation, state)) 244 | 245 | previous = annotation 246 | } 247 | 248 | const name = scope.replace(/^source\./, '').replace(/\./g, '-') 249 | 250 | head.properties.className = [ 251 | // With the output language class. 252 | 'language-' + name, 253 | // Without the input language class: 254 | ...classes.filter((d) => d !== language) 255 | ] 256 | 257 | parent.children.splice(index, 1, { 258 | type: 'element', 259 | tagName: 'div', 260 | properties: {className: ['highlight', 'highlight-' + name]}, 261 | children: [ 262 | {type: 'text', value: '\n'}, 263 | { 264 | // The `
`.
265 |             ...node,
266 |             children: [
267 |               {
268 |                 // The ``.
269 |                 ...head,
270 |                 children: /** @type {Array} */ (
271 |                   fragment.children
272 |                 )
273 |               }
274 |             ]
275 |           },
276 |           ...footer,
277 |           {type: 'text', value: '\n'}
278 |         ]
279 |       })
280 | 
281 |       return SKIP
282 |     })
283 | 
284 |     // Note: `Promise` or `Promise` as return type seem to fail
285 |     // `unified`’s types.
286 |     return tree
287 |   }
288 | }
289 | 


--------------------------------------------------------------------------------
/lib/render.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * @import {ElementContent} from 'hast'
  3 |  * @import {BlockContent, DefinitionContent, List, Root} from 'mdast'
  4 |  * @import {NodeCompletion, NodeError, NodeHighlight, NodeHover, NodeQuery} from 'twoslash'
  5 |  * @import {RenderResult, Render, State} from './types.js'
  6 |  */
  7 | 
  8 | import {ok as assert} from 'devlop'
  9 | import {toString} from 'hast-util-to-string'
 10 | import {fromMarkdown} from 'mdast-util-from-markdown'
 11 | import {gfmFromMarkdown} from 'mdast-util-gfm'
 12 | import {toHast} from 'mdast-util-to-hast'
 13 | import {gfm} from 'micromark-extension-gfm'
 14 | import {removePosition} from 'unist-util-remove-position'
 15 | import {SKIP, visitParents} from 'unist-util-visit-parents'
 16 | 
 17 | /**
 18 |  * Prefix for language classes.
 19 |  */
 20 | const prefix = 'language-'
 21 | 
 22 | /**
 23 |  * @param {State} state
 24 |  * @param {NodeCompletion} annotation
 25 |  * @param {Array} between
 26 |  * @returns {RenderResult}
 27 |  * @satisfies {Render}
 28 |  */
 29 | export function completion(state, annotation, between) {
 30 |   const id = state.idPrefix + ++state.count
 31 |   /** @type {Array} */
 32 |   const items = []
 33 | 
 34 |   for (const completion of annotation.completions) {
 35 |     if (items.length > 10) {
 36 |       items.push({
 37 |         type: 'element',
 38 |         tagName: 'li',
 39 |         properties: {className: ['rehype-twoslash-completions-more']},
 40 |         children: [{type: 'text', value: '…'}]
 41 |       })
 42 |       break
 43 |     }
 44 | 
 45 |     const className = ['rehype-twoslash-completion']
 46 |     /** @type {Array} */
 47 |     const children =
 48 |       completion.name.startsWith(annotation.completionsPrefix) &&
 49 |       completion.name !== annotation.completionsPrefix
 50 |         ? [
 51 |             {
 52 |               type: 'element',
 53 |               tagName: 'span',
 54 |               properties: {className: ['rehype-twoslash-match']},
 55 |               children: [{type: 'text', value: annotation.completionsPrefix}]
 56 |             },
 57 |             {
 58 |               type: 'element',
 59 |               tagName: 'span',
 60 |               properties: {className: ['rehype-twoslash-completion-swap']},
 61 |               children: [
 62 |                 {
 63 |                   type: 'text',
 64 |                   value: completion.name.slice(
 65 |                     annotation.completionsPrefix.length
 66 |                   )
 67 |                 }
 68 |               ]
 69 |             }
 70 |           ]
 71 |         : [
 72 |             {
 73 |               type: 'element',
 74 |               tagName: 'span',
 75 |               properties: {className: ['rehype-twoslash-completion-swap']},
 76 |               children: [{type: 'text', value: completion.name}]
 77 |             }
 78 |           ]
 79 | 
 80 |     if (
 81 |       'kindModifiers' in completion &&
 82 |       typeof completion.kindModifiers === 'string' &&
 83 |       completion.kindModifiers.split(',').includes('deprecated')
 84 |     ) {
 85 |       className.push('rehype-twoslash-completion-deprecated')
 86 |     }
 87 | 
 88 |     items.push({
 89 |       type: 'element',
 90 |       tagName: 'li',
 91 |       properties: {className},
 92 |       children
 93 |     })
 94 |   }
 95 | 
 96 |   return {
 97 |     content: [
 98 |       {
 99 |         type: 'element',
100 |         tagName: 'span',
101 |         properties: {
102 |           className: [
103 |             'rehype-twoslash-autoshow',
104 |             'rehype-twoslash-completion-swap',
105 |             'rehype-twoslash-popover-target'
106 |           ],
107 |           dataPopoverTarget: id
108 |         },
109 |         children: between
110 |       }
111 |     ],
112 |     footer: [
113 |       {
114 |         type: 'element',
115 |         tagName: 'div',
116 |         properties: {
117 |           className: ['rehype-twoslash-completion', 'rehype-twoslash-popover'],
118 |           id,
119 |           popover: ''
120 |         },
121 |         children: [
122 |           {
123 |             type: 'element',
124 |             tagName: 'ol',
125 |             properties: {className: ['rehype-twoslash-completions']},
126 |             children: items
127 |           }
128 |         ]
129 |       }
130 |     ]
131 |   }
132 | }
133 | 
134 | /**
135 |  * @param {State} state
136 |  * @param {NodeError} annotation
137 |  * @param {Array} between
138 |  * @returns {RenderResult}
139 |  * @satisfies {Render}
140 |  */
141 | export function error(state, annotation, between) {
142 |   const id = state.idPrefix + ++state.count
143 | 
144 |   return {
145 |     content: [
146 |       {
147 |         type: 'element',
148 |         tagName: 'span',
149 |         properties: {
150 |           className: [
151 |             'rehype-twoslash-error-target',
152 |             'rehype-twoslash-popover-target'
153 |           ],
154 |           dataPopoverTarget: id
155 |         },
156 |         children: between
157 |       }
158 |     ],
159 |     footer: [
160 |       {
161 |         type: 'element',
162 |         tagName: 'div',
163 |         properties: {
164 |           className: ['rehype-twoslash-error', 'rehype-twoslash-popover'],
165 |           id,
166 |           popover: ''
167 |         },
168 |         children: [
169 |           {
170 |             type: 'element',
171 |             tagName: 'pre',
172 |             properties: {className: ['rehype-twoslash-popover-code']},
173 |             children: [
174 |               {
175 |                 type: 'element',
176 |                 tagName: 'code',
177 |                 properties: {},
178 |                 children: [
179 |                   {
180 |                     type: 'text',
181 |                     value:
182 |                       annotation.text +
183 |                       /* c8 ignore next -- errors we get back currently always have a code */
184 |                       (annotation.code ? ' (' + annotation.code + ')' : '')
185 |                   }
186 |                 ]
187 |               }
188 |             ]
189 |           }
190 |         ]
191 |       }
192 |     ]
193 |   }
194 | }
195 | 
196 | /**
197 |  * @param {State} state
198 |  * @param {NodeHighlight} annotation
199 |  * @param {Array} between
200 |  * @returns {Array}
201 |  * @satisfies {Render}
202 |  */
203 | export function highlight(state, annotation, between) {
204 |   return [
205 |     {
206 |       type: 'element',
207 |       tagName: 'span',
208 |       properties: {className: ['rehype-twoslash-highlight']},
209 |       children: between
210 |     }
211 |   ]
212 | }
213 | 
214 | /**
215 |  * @param {State} state
216 |  * @param {NodeHover} annotation
217 |  * @param {Array} between
218 |  * @returns {RenderResult}
219 |  * @satisfies {Render}
220 |  */
221 | export function hover(state, annotation, between) {
222 |   const id = state.idPrefix + ++state.count
223 | 
224 |   return {
225 |     content: [
226 |       {
227 |         type: 'element',
228 |         tagName: 'span',
229 |         properties: {
230 |           className: ['rehype-twoslash-popover-target'],
231 |           dataPopoverTarget: id
232 |         },
233 |         children: between
234 |       }
235 |     ],
236 |     footer: [
237 |       {
238 |         type: 'element',
239 |         tagName: 'div',
240 |         properties: {
241 |           className: ['rehype-twoslash-hover', 'rehype-twoslash-popover'],
242 |           id,
243 |           popover: ''
244 |         },
245 |         children: createInfo(state, annotation)
246 |       }
247 |     ]
248 |   }
249 | }
250 | 
251 | /**
252 |  * @param {State} state
253 |  * @param {NodeQuery} annotation
254 |  * @param {Array} between
255 |  * @returns {RenderResult}
256 |  * @satisfies {Render}
257 |  */
258 | export function query(state, annotation, between) {
259 |   const id = state.idPrefix + ++state.count
260 | 
261 |   return {
262 |     content: [
263 |       {
264 |         type: 'element',
265 |         tagName: 'span',
266 |         properties: {
267 |           className: [
268 |             'rehype-twoslash-autoshow',
269 |             'rehype-twoslash-popover-target'
270 |           ],
271 |           dataPopoverTarget: id
272 |         },
273 |         children: between
274 |       }
275 |     ],
276 |     footer: [
277 |       {
278 |         type: 'element',
279 |         tagName: 'div',
280 |         properties: {
281 |           className: ['rehype-twoslash-popover', 'rehype-twoslash-query'],
282 |           id,
283 |           popover: ''
284 |         },
285 |         children: createInfo(state, annotation)
286 |       }
287 |     ]
288 |   }
289 | }
290 | 
291 | /**
292 |  * @param {State} state
293 |  * @param {NodeHover | NodeQuery} annotation
294 |  * @returns {Array}
295 |  */
296 | function createInfo(state, annotation) {
297 |   /** @type {Root} */
298 |   const tree = annotation.docs
299 |     ? fromMarkdown(annotation.docs, {
300 |         extensions: [gfm()],
301 |         mdastExtensions: [gfmFromMarkdown()]
302 |       })
303 |     : {type: 'root', children: []}
304 |   const tags = annotation.tags || []
305 |   /** @type {List} */
306 |   const list = {type: 'list', spread: false, ordered: false, children: []}
307 | 
308 |   removePosition(tree, {force: true})
309 | 
310 |   for (const [name, text] of tags) {
311 |     // Idea: support `{@link}` stuff.
312 |     //
313 |     // Use a `\n` here to join so that it’ll work when it is fenced code for
314 |     // example.
315 |     const value = '**@' + name + '**' + (text ? '\n' + text : '')
316 |     const fragment = fromMarkdown(value, {
317 |       extensions: [gfm()],
318 |       mdastExtensions: [gfmFromMarkdown()]
319 |     })
320 |     removePosition(fragment, {force: true})
321 | 
322 |     list.children.push({
323 |       type: 'listItem',
324 |       spread: false,
325 |       children: /** @type {Array} */ (
326 |         fragment.children
327 |       )
328 |     })
329 |   }
330 | 
331 |   if (list.children.length > 0) {
332 |     tree.children.push(list)
333 |   }
334 | 
335 |   const hastTree = toHast(tree)
336 |   assert(hastTree.type === 'root')
337 | 
338 |   visitParents(hastTree, 'element', function (node) {
339 |     if (node.tagName !== 'pre') return
340 |     const head = node.children[0]
341 |     /* c8 ignore next -- we work with plain markdown here, so the `pre` always contains `code`. */
342 |     if (!head || head.type !== 'element' || head.tagName !== 'code') return SKIP
343 |     const classes = head.properties.className
344 |     /* c8 ignore next -- type docs we get back currently always have a language. */
345 |     if (!Array.isArray(classes)) return SKIP
346 | 
347 |     // Cast as we check if it’s a string in `find`.
348 |     const language = /** @type {string | undefined} */ (
349 |       classes.find(function (d) {
350 |         return typeof d === 'string' && d.startsWith(prefix)
351 |       })
352 |     )
353 | 
354 |     const scope = language
355 |       ? state.starryNight.flagToScope(language.slice(prefix.length))
356 |       : /* c8 ignore next -- type docs we get back currently always have a language we know. */
357 |         undefined
358 | 
359 |     if (!scope) return SKIP
360 | 
361 |     const fragment = state.starryNight.highlight(toString(head), scope)
362 | 
363 |     head.children = /** @type {Array} */ (fragment.children)
364 | 
365 |     return SKIP
366 |   })
367 | 
368 |   /** @type {Array} */
369 |   const result = [
370 |     {
371 |       type: 'element',
372 |       tagName: 'pre',
373 |       properties: {className: ['rehype-twoslash-popover-code']},
374 |       children: [
375 |         {
376 |           type: 'element',
377 |           tagName: 'code',
378 |           properties: {className: ['language-ts']},
379 |           children: /** @type {Array} */ (
380 |             state.starryNight.highlight(annotation.text, 'source.ts').children
381 |           )
382 |         }
383 |       ]
384 |     }
385 |   ]
386 | 
387 |   if (hastTree.children.length > 0) {
388 |     result.push({
389 |       type: 'element',
390 |       tagName: 'div',
391 |       properties: {className: ['rehype-twoslash-popover-description']},
392 |       children: /** @type {Array} */ (hastTree.children)
393 |     })
394 |   }
395 | 
396 |   return result
397 | }
398 | 


--------------------------------------------------------------------------------
/test/fixtures/basic/output.html:
--------------------------------------------------------------------------------
 1 | 
2 |
console.log(1)
 3 | 
4 |
var console: Console
5 |
(method) Console.log(...data: any[]): void
6 |
7 |
8 |
console.log(1)
 9 | 
10 |
var console: Console
11 |
(method) Console.log(...data: any[]): void
12 |
13 |
14 |
console.log(1)
15 | 
16 |
var console: Console
17 |
(method) Console.log(...data: any[]): void
18 |
19 |
em { color: red }
20 | 
21 |
# hi
22 | 
23 |
# hi
24 | 
25 |
# hi
26 | 
27 |
# hi
28 | 
29 |
30 |
x
31 | 
32 |
any
33 |
34 |
35 |

36 | 
37 |
38 |
39 |
type A<B> = {
40 |     str: string
41 |     b: B
42 | }
43 | 
44 |
type A<B> = {
45 |     str: string;
46 |     b: B;
47 | }
48 |
(type parameter) B in type A<B>
49 |
(property) str: string
50 |
(property) b: B
51 |
(type parameter) B in type A<B>
52 |
53 |
54 |
type A<B> = {
55 |     str: "one" | "two"
56 |     b: B
57 | }
58 | 
59 |
type A<B> = {
60 |     str: "one" | "two";
61 |     b: B;
62 | }
63 |
(type parameter) B in type A<B>
64 |
(property) str: "one" | "two"
65 |
(property) b: B
66 |
(type parameter) B in type A<B>
67 |
68 |
69 |
let fragment = <jsx />
70 | 
71 |
let fragment: any
72 |
any
73 |
74 |
75 |
let fragment = <jsx />
76 | 
77 |
let fragment: any
78 |
any
79 |
80 |
81 |
let fragment = <jsx />
82 | 
83 |
let fragment: jsx
84 |
Cannot find name 'jsx'. (2304)
85 |
type jsx = /*unresolved*/ any
86 |
'>' expected. (1005)
87 |
88 |
89 |
let fragment = <jsx />
90 | 
91 |
let fragment: any
92 |
JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. (7026)
93 |
JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. (7026)
94 |
JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. (7026)
95 |
Cannot find name 'React'. (2304)
96 |
any
97 |
98 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # rehype-twoslash 2 | 3 | [![Build][badge-build-image]][badge-build-url] 4 | [![Coverage][badge-coverage-image]][badge-coverage-url] 5 | [![Downloads][badge-downloads-image]][badge-downloads-url] 6 | [![Size][badge-size-image]][badge-size-url] 7 | [![Sponsors][badge-sponsors-image]][badge-collective-url] 8 | [![Backers][badge-backers-image]][badge-collective-url] 9 | [![Chat][badge-chat-image]][badge-chat-url] 10 | 11 | **[rehype][github-rehype]** plugin to process JavaScript and TypeScript code 12 | with [`twoslash`][twoslash] and highlight it with 13 | [`starry-night`][github-starry-night]. 14 | 15 | 19 | 20 | ## Contents 21 | 22 | * [What is this?](#what-is-this) 23 | * [When should I use this?](#when-should-i-use-this) 24 | * [Install](#install) 25 | * [Use](#use) 26 | * [API](#api) 27 | * [`Options`](#options) 28 | * [`Render`](#render) 29 | * [`RenderResult`](#renderresult) 30 | * [`Renderers`](#renderers) 31 | * [`rehypeTwoslash(options) (default)`](#rehypetwoslashoptions-default) 32 | * [HTML](#html) 33 | * [Markdown](#markdown) 34 | * [CSS](#css) 35 | * [JavaScript](#javascript) 36 | * [Compatibility](#compatibility) 37 | * [Security](#security) 38 | * [Related](#related) 39 | * [Contribute](#contribute) 40 | * [License](#license) 41 | 42 | ## What is this? 43 | 44 | This package is a [unified][github-unified] ([rehype][github-rehype]) plugin to 45 | process JavaScript and TypeScript code with [`twoslash`][twoslash] and 46 | highlight it with [`starry-night`][github-starry-night]. 47 | 48 | `twoslash` is a tool to run code through the TypeScript compiler and extract 49 | info about that code. 50 | Info you also see in your editor. 51 | This info can for example be type errors or type info that is shown on hover. 52 | `twoslash` also supports a command syntax through comments in the code, 53 | so an author can highlight a particular piece of code, 54 | ignore certain errors, 55 | or show a specific file. 56 | 57 | `starry-night` is a beautiful syntax highlighter, 58 | like what GitHub uses to highlight code, 59 | but free and in JavaScript. 60 | 61 | ## When should I use this? 62 | 63 | This plugin is particularly useful for your own website or blog, 64 | or any place where you want to talk about JavaScript-y code, 65 | and want to improve the experience of your readers by showing them more 66 | info about the code. 67 | 68 | You can combine this package with 69 | [`rehype-starry-night`][github-rehype-starry-night]. 70 | That applies syntax highlighting with `starry-night` to all code. 71 | 72 | If you are not using remark or rehype, 73 | you can instead use [`twoslash`][twoslash] directly. 74 | If you don’t care for [`starry-night`][github-starry-night], 75 | you can use [`@shikijs/twoslash`][github-shikijs-twoslash]. 76 | 77 | ## Install 78 | 79 | This package is [ESM only][github-gist-esm]. 80 | In Node.js (version 16+), install with [npm][npm-install]: 81 | 82 | ```sh 83 | npm install rehype-twoslash 84 | ``` 85 | 86 | In Deno with [`esm.sh`][esmsh]: 87 | 88 | ```js 89 | import rehypeTwoslash from 'https://esm.sh/rehype-twoslash@1' 90 | ``` 91 | 92 | In browsers with [`esm.sh`][esmsh]: 93 | 94 | ```html 95 | 98 | ``` 99 | 100 | ## Use 101 | 102 | Say we have the following file `example.md`: 103 | 104 | ````markdown 105 | # Jupiter 106 | 107 | ```js twoslash 108 | const name = 'Jupiter' 109 | console.log('Hello, ' + name + '!') 110 | ``` 111 | ```` 112 | 113 | …and our module `example.js` contains: 114 | 115 | ```js 116 | import rehypeStringify from 'rehype-stringify' 117 | import rehypeTwoslash from 'rehype-twoslash' 118 | import remarkParse from 'remark-parse' 119 | import remarkRehype from 'remark-rehype' 120 | import {read} from 'to-vfile' 121 | import {unified} from 'unified' 122 | 123 | const file = await read('example.md') 124 | 125 | await unified() 126 | .use(remarkParse) 127 | .use(remarkRehype) 128 | .use(rehypeTwoslash) 129 | .use(rehypeStringify) 130 | .process(file) 131 | 132 | console.log(String(file)) 133 | ``` 134 | 135 | …then running `node example.js` yields: 136 | 137 | ```html 138 |

Jupiter

139 |
140 |
const name = 'Jupiter'
141 | console.log('Hello, ' + name + '!')
142 | 
143 |
const name: "Jupiter"
144 |
var console: Console
145 |
(method) Console.log(...data: any[]): void
146 |
const name: "Jupiter"
147 |
148 | ``` 149 | 150 | With some [CSS][section-css] and [JavaScript][section-javascript] that could 151 | look like this: 152 | 153 | 154 | 155 | 156 | 157 | 158 | ## API 159 | 160 | ### `Options` 161 | 162 | Configuration for `rehype-twoslash`. 163 | 164 | ###### Notes 165 | 166 | `rehype-twoslash` runs on `` elements with a `twoslash` directive. 167 | That directive can be passed as a word in markdown (` ```ts twoslash `) or 168 | as a class in HTML (``). 169 | 170 | The inverse occurs when `directive` is `false`. 171 | All `` where the language class is JavaScript or TypeScript is 172 | processed. 173 | Then `no-twoslash` (` ```ts no-twoslash `, 174 | ``) can be used to prevent processing. 175 | 176 | ###### Fields 177 | 178 | * `directive?` (`boolean | null | undefined`) 179 | — whether to require a `twoslash` directive (default: `true`) 180 | * `grammars?` (`ReadonlyArray | null | undefined`) 181 | — grammars for `starry-night` to support (default: 182 | `[sourceJson, sourceJs, sourceTsx, sourceTs]`) 183 | * `idPrefix?` (`string | null | undefined`) 184 | — prefix before IDs (default: `'rehype-twoslash-'`) 185 | * `renderers?` (`Renderers | null | undefined`) 186 | — renderers for `twoslash` annotations (optional) 187 | * `twoslash?` (`TwoslashOptions | null | undefined`) 188 | — options passed to `twoslash` (optional); 189 | this includes fields such as `cache`, 190 | `customTransformers`, 191 | and `filterNode`; 192 | see 193 | [`TwoslashOptions` from `twoslash`](https://github.com/twoslashes/twoslash/blob/1eb3af3/packages/twoslash/src/types/options.ts#L18) 194 | for more info 195 | 196 | ### `Render` 197 | 198 | Render function. 199 | 200 | Takes a particular annotation from the TypeScript compiler (such as an error) 201 | and turns it into `hast` (HTML) content. 202 | See `lib/render.js` for examples. 203 | 204 | ###### Notes 205 | 206 | You can return `Array` directly instead of a `RenderResult` 207 | when you don’t have content for a footer. 208 | 209 | ###### Type 210 | 211 | ```ts 212 | ( 213 | state: State, 214 | annotation: Annotation, 215 | children: Array 216 | ) => Array | RenderResult 217 | ``` 218 | 219 | ### `RenderResult` 220 | 221 | Result from `Render`. 222 | 223 | ###### Fields 224 | 225 | * `content?` (`Array | undefined`) 226 | — main inline content to use in the code block; 227 | for example a `` that causes a tooltip to show 228 | * `footer?` (`Array | undefined`) 229 | — extra content to use that relates to the code block; 230 | for example a `
` for a tooltip 231 | 232 | ### `Renderers` 233 | 234 | Renderers. 235 | 236 | Each key is a type of an annotation (such as `error` or `hover`) and each 237 | value a corresponding render function. 238 | 239 | ###### Type 240 | 241 | ```ts 242 | { completion?: Render | null | undefined; error?: Render | null | undefined; highlight?: Render | null | undefined; hover?: Render<...> | ... 1 more ... | undefined; query?: Render<...> | ... 1 more ... | undefined; } 243 | ``` 244 | 245 | ### `rehypeTwoslash(options) (default)` 246 | 247 | Plugin to process JavaScript and TypeScript code with `twoslash` 248 | and highlight it with `starry-night`. 249 | 250 | ###### Parameters 251 | 252 | * `options?` (`Readonly | null | undefined`) 253 | — configuration (optional) 254 | 255 | ###### Returns 256 | 257 | Transform (`(tree: Root, file: VFile) => Promise`). 258 | 259 | ## HTML 260 | 261 | On the input side, 262 | this plugin looks for code blocks with a `twoslash` class. 263 | So: 264 | 265 | ```html 266 |
console.log('Hello, Mercury!')
267 | ``` 268 | 269 | It will warn when that class is used with a programming language that 270 | `twoslash` does not understand (such as Rust). 271 | 272 | If you want to process all JavaScript and TypeScript code blocks, 273 | you can set `directive: false` in options. 274 | Then the `language-*` class is enough and no directive is needed. 275 | You can still prevent processing of a particular block with a `no-twoslash` 276 | class: 277 | 278 | ```html 279 |
console.log('Hello, Mars!')
280 | ``` 281 | 282 | On the output side, 283 | this plugin generates markup that can be enhanced with 284 | [CSS][section-css] and [JavaScript][section-javascript] into tooltips and 285 | more. 286 | You can also choose to generate different HTML by passing custom render 287 | functions in `options.renderers`. 288 | 289 | To illustrate, 290 | here is an example of a tooltip target for the identifier in a variable 291 | declaration (`const name = …`): 292 | 293 | ```html 294 | name 298 | ``` 299 | 300 | It has a corresponding tooltip: 301 | 302 | ```html 303 |
308 |
const name: "Jupiter"
309 |
310 | ``` 311 | 312 | Observe that there are sufficient classes to hook into with CSS and JavaScript 313 | and that unique identifiers connect the popover and its popover target together. 314 | 315 | ## Markdown 316 | 317 | When combined with [`remark-parse`][github-remark-parse] and 318 | [`remark-rehype`][github-remark-rehype], 319 | this plugin works similarly on markdown to how it does on HTML as described 320 | above. 321 | It then understands the `twoslash` and `no-twoslash` word in the info string, 322 | right after the language. 323 | To illustrate: 324 | 325 | ````markdown 326 | ```ts twoslash 327 | console.log('Hello, Venus!') 328 | ``` 329 | 330 | ```ts no-twoslash 331 | console.log('Hello, Earth!') 332 | ``` 333 | ```` 334 | 335 | ## CSS 336 | 337 | This plugin generates sufficient classes that can be styled with CSS. 338 | Which ones to use and how to style them depends on the rest of your website 339 | and your heart’s desire. 340 | To illustrate, 341 | see [`demo/index.css`][file-demo-css]. 342 | But get creative! 343 | 344 | ## JavaScript 345 | 346 | This plugin generates markup that needs to be made interactive with JavaScript. 347 | What to do exactly, 348 | and how to do it, 349 | depends on your website and your preferences. 350 | For inspiration, 351 | see [`demo/index.js`][file-demo-js]. 352 | 353 | ## Compatibility 354 | 355 | Projects maintained by the unified collective are compatible with maintained 356 | versions of Node.js. 357 | 358 | When we cut a new major release, we drop support for unmaintained versions of 359 | Node. 360 | This means we try to keep the current release line, `rehype-twoslash@1`, 361 | compatible with Node.js 16. 362 | 363 | ## Security 364 | 365 | Use of `rehype-twoslash` is likely not safe on arbitrary user content, 366 | as it passes code through the TypeScript compiler, 367 | which I assume has some access to the file system and there might be ways to 368 | exploit it. 369 | 370 | ## Related 371 | 372 | * [`rehype-starry-night`](https://github.com/rehypejs/rehype-starry-night) 373 | — apply syntax highlighting with `starry-night` to all code 374 | 375 | ## Contribute 376 | 377 | See [`contributing.md`][health-contributing] in [`rehypejs/.github`][health] 378 | for ways to get started. 379 | See [`support.md`][health-support] for ways to get help. 380 | 381 | This project has a [code of conduct][health-coc]. 382 | By interacting with this repository, organization, or community you agree to 383 | abide by its terms. 384 | 385 | ## License 386 | 387 | [MIT][file-license] © [Titus Wormer][wooorm] 388 | 389 | 390 | 391 | [badge-backers-image]: https://opencollective.com/unified/backers/badge.svg 392 | 393 | [badge-build-image]: https://github.com/rehypejs/rehype-twoslash/actions/workflows/main.yml/badge.svg 394 | 395 | [badge-build-url]: https://github.com/rehypejs/rehype-twoslash/actions 396 | 397 | [badge-collective-url]: https://opencollective.com/unified 398 | 399 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/rehypejs/rehype-twoslash.svg 400 | 401 | [badge-coverage-url]: https://codecov.io/github/rehypejs/rehype-twoslash 402 | 403 | [badge-downloads-image]: https://img.shields.io/npm/dm/rehype-twoslash.svg 404 | 405 | [badge-downloads-url]: https://www.npmjs.com/package/rehype-twoslash 406 | 407 | [badge-size-image]: https://img.shields.io/bundlejs/size/rehype-twoslash 408 | 409 | [badge-size-url]: https://bundlejs.com/?q=rehype-twoslash 410 | 411 | [badge-sponsors-image]: https://opencollective.com/unified/sponsors/badge.svg 412 | 413 | [badge-chat-image]: https://img.shields.io/badge/chat-discussions-success.svg 414 | 415 | [badge-chat-url]: https://github.com/rehypejs/rehype/discussions 416 | 417 | [esmsh]: https://esm.sh 418 | 419 | [file-demo-css]: demo/index.css 420 | 421 | [file-demo-js]: demo/index.js 422 | 423 | [file-license]: license 424 | 425 | [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 426 | 427 | [github-rehype]: https://github.com/rehypejs/rehype 428 | 429 | [github-rehype-starry-night]: https://github.com/rehypejs/rehype-starry-night 430 | 431 | [github-remark-parse]: https://github.com/remarkjs/remark/tree/main/packages/remark-parse 432 | 433 | [github-remark-rehype]: https://github.com/remarkjs/remark-rehype 434 | 435 | [github-shikijs-twoslash]: https://github.com/shikijs/shiki/tree/main/packages/twoslash 436 | 437 | [github-starry-night]: https://github.com/wooorm/starry-night 438 | 439 | [github-unified]: https://github.com/unifiedjs/unified 440 | 441 | [health-coc]: https://github.com/rehypejs/.github/blob/main/code-of-conduct.md 442 | 443 | [health-contributing]: https://github.com/rehypejs/.github/blob/main/contributing.md 444 | 445 | [health-support]: https://github.com/rehypejs/.github/blob/main/support.md 446 | 447 | [health]: https://github.com/rehypejs/.github 448 | 449 | [npm-install]: https://docs.npmjs.com/cli/install 450 | 451 | [section-css]: #css 452 | 453 | [section-javascript]: #javascript 454 | 455 | [twoslash]: https://twoslash.netlify.app 456 | 457 | [wooorm]: https://wooorm.com 458 | -------------------------------------------------------------------------------- /test/fixtures/completion/output.html: -------------------------------------------------------------------------------- 1 |

A regular completion annotation.

2 |
3 |
console.e
 4 | 
5 |
var console: Console
6 |
any
7 |
  1. error
8 |
9 | 10 |

Another, with existing extra characters.

11 |
12 |
console.err
13 | console.err
14 | console.err
15 | console.err
16 | 
17 |
var console: Console
18 |
any
19 |
  1. error
  2. err
20 |
var console: Console
21 |
any
22 |
  1. error
  2. err
23 |
var console: Console
24 |
  1. assert
  2. clear
  3. count
  4. countReset
  5. debug
  6. dir
  7. dirxml
  8. error
  9. group
  10. groupCollapsed
  11. groupEnd
25 |
var console: Console
26 |
  1. console
27 |
any
28 |
29 | 30 |

Another, with non-existing extra characters.

31 |
32 |
console.exa
33 | console.exa
34 | console.exa
35 | console.exa
36 | 
37 |
var console: Console
38 |
any
39 |
  1. exa
40 |
var console: Console
41 |
any
42 |
  1. error
  2. exa
43 |
var console: Console
44 |
  1. assert
  2. clear
  3. count
  4. countReset
  5. debug
  6. dir
  7. dirxml
  8. error
  9. group
  10. groupCollapsed
  11. groupEnd
45 |
var console: Console
46 |
  1. console
47 |
any
48 |
49 | 50 |

A completion annotation that completes a deprecated value.

51 |
52 |
const rule = new CSSRule()
53 | console.log(rule.ty)
54 | 
55 |
const rule: CSSRule
56 |
var CSSRule: new () => CSSRule

A single CSS rule. There are several types of rules, listed in the Type constants section below.

57 |

MDN Reference

58 |
var console: Console
59 |
(method) Console.log(...data: any[]): void
60 |
const rule: CSSRule
61 |
any
62 |
  1. type
63 |
64 | 65 |

A completion annotation, not at the end.

66 |
67 |
console.t
68 | 
69 |
var console: Console
70 |
  1. confirm
  2. console
  3. const
  4. continue
71 |
any
72 |
73 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import fs from 'node:fs/promises' 3 | import process from 'node:process' 4 | import test from 'node:test' 5 | import rehypeParse from 'rehype-parse' 6 | import rehypeStringify from 'rehype-stringify' 7 | import rehypeTwoslash from 'rehype-twoslash' 8 | import remarkParse from 'remark-parse' 9 | import remarkRehype from 'remark-rehype' 10 | import {read, write} from 'to-vfile' 11 | import {unified} from 'unified' 12 | import {VFile} from 'vfile' 13 | 14 | test('rehypeTwoslash', async function (t) { 15 | await t.test('should expose the public api', async function () { 16 | assert.deepEqual(Object.keys(await import('rehype-twoslash')).sort(), [ 17 | 'default' 18 | ]) 19 | }) 20 | 21 | await t.test('should work w/ `twoslash` class', async function () { 22 | const file = await unified() 23 | .use(rehypeParse, {fragment: true}) 24 | .use(rehypeTwoslash) 25 | .use(rehypeStringify) 26 | .process( 27 | ` 28 |
const hi = 'Hello'
 29 | alert(hi)
 30 | 
31 | ` 32 | ) 33 | 34 | assert.equal( 35 | String(file), 36 | ` 37 |
38 |
const hi = 'Hello'
 39 | alert(hi)
 40 | 
41 |
const hi: "Hello"
42 |
function alert(message?: any): void
43 |
const hi: "Hello"
44 |
45 | ` 46 | ) 47 | assert.deepEqual(file.messages.map(String), []) 48 | }) 49 | 50 | await t.test( 51 | 'should warn w/ `twoslash` class, w/o language', 52 | async function () { 53 | const file = await unified() 54 | .use(rehypeParse, {fragment: true}) 55 | .use(rehypeTwoslash, {directive: false}) 56 | .use(rehypeStringify) 57 | .process('
# hi
') 58 | 59 | assert.equal( 60 | String(file), 61 | '
# hi
' 62 | ) 63 | assert.deepEqual(file.messages.map(String), [ 64 | '1:1-1:46: Unexpected non-js/ts code with twoslash directive, expected JavaScript or TypeScript code' 65 | ]) 66 | } 67 | ) 68 | 69 | await t.test( 70 | 'should warn w/ `twoslash` class, w/ non-js/ts', 71 | async function () { 72 | const file = await unified() 73 | .use(rehypeParse, {fragment: true}) 74 | .use(rehypeTwoslash, {directive: false}) 75 | .use(rehypeStringify) 76 | .process( 77 | '
{"value":3.14}
' 78 | ) 79 | 80 | assert.equal( 81 | String(file), 82 | '
{"value":3.14}
' 83 | ) 84 | assert.deepEqual(file.messages.map(String), [ 85 | '1:1-1:70: Unexpected non-js/ts code (`source.json`) with twoslash directive, expected JavaScript or TypeScript code' 86 | ]) 87 | } 88 | ) 89 | 90 | await t.test('should do nothing w/o `twoslash` class', async function () { 91 | const file = await unified() 92 | .use(rehypeParse, {fragment: true}) 93 | .use(rehypeTwoslash) 94 | .use(rehypeStringify) 95 | .process('
console.log(3.14)
') 96 | 97 | assert.doesNotMatch(String(file), /class="rehype-twoslash-popover-target"/) 98 | assert.deepEqual(file.messages.map(String), []) 99 | }) 100 | 101 | await t.test( 102 | 'should support `directive: false` w/o `twoslash` class', 103 | async function () { 104 | const file = await unified() 105 | .use(rehypeParse, {fragment: true}) 106 | .use(rehypeTwoslash, {directive: false}) 107 | .use(rehypeStringify) 108 | .process( 109 | '
console.log(3.14)
' 110 | ) 111 | 112 | assert.match(String(file), /class="rehype-twoslash-popover-target"/) 113 | assert.deepEqual(file.messages.map(String), []) 114 | } 115 | ) 116 | 117 | await t.test( 118 | 'should support `directive: false` w/ `no-twoslash` class', 119 | async function () { 120 | const file = await unified() 121 | .use(rehypeParse, {fragment: true}) 122 | .use(rehypeTwoslash, {directive: false}) 123 | .use(rehypeStringify) 124 | .process( 125 | '
console.log(3.14)
' 126 | ) 127 | 128 | assert.doesNotMatch( 129 | String(file), 130 | /class="rehype-twoslash-popover-target"/ 131 | ) 132 | assert.deepEqual(file.messages.map(String), []) 133 | } 134 | ) 135 | 136 | await t.test('should support `twoslash` options', async function () { 137 | /** @type {Array} */ 138 | const calls = [] 139 | const file = await unified() 140 | .use(rehypeParse, {fragment: true}) 141 | .use(rehypeTwoslash, { 142 | twoslash: { 143 | filterNode(node) { 144 | calls.push(node.type) 145 | return false 146 | } 147 | } 148 | }) 149 | .use(rehypeStringify) 150 | .process( 151 | `
const message = "hi"
152 | console.log(message)
153 | 
` 154 | ) 155 | 156 | assert.equal( 157 | String(file), 158 | `
159 |
const message = "hi"
160 | console.log(message)
161 | 
162 |
` 163 | ) 164 | 165 | assert.deepEqual(calls, ['hover', 'hover', 'hover', 'hover']) 166 | assert.deepEqual(file.messages.map(String), []) 167 | }) 168 | 169 | await t.test( 170 | 'should support custom renderers returning element content', 171 | async function () { 172 | const file = await unified() 173 | .use(rehypeParse, {fragment: true}) 174 | .use(rehypeTwoslash, { 175 | renderers: { 176 | hover(state, annotation, children) { 177 | return [ 178 | { 179 | type: 'element', 180 | tagName: 'span', 181 | properties: {title: annotation.text, className: ['hover']}, 182 | children 183 | } 184 | ] 185 | } 186 | } 187 | }) 188 | .use(rehypeStringify) 189 | .process( 190 | '
console.log("hi")
' 191 | ) 192 | 193 | assert.equal( 194 | String(file), 195 | `
196 |
console.log("hi")
197 |
` 198 | ) 199 | assert.deepEqual(file.messages.map(String), []) 200 | } 201 | ) 202 | 203 | await t.test( 204 | 'should support custom renderers returning a content/footer object', 205 | async function () { 206 | const file = await unified() 207 | .use(rehypeParse, {fragment: true}) 208 | .use(rehypeTwoslash, { 209 | renderers: { 210 | hover(state, annotation, children) { 211 | const id = state.idPrefix + ++state.count 212 | 213 | return { 214 | content: [ 215 | { 216 | type: 'element', 217 | tagName: 'my-tooltip-reference', 218 | properties: {dataTooltipId: id}, 219 | children 220 | } 221 | ], 222 | footer: [ 223 | { 224 | type: 'element', 225 | tagName: 'my-tooltip', 226 | properties: {id}, 227 | children: [{type: 'text', value: annotation.text}] 228 | } 229 | ] 230 | } 231 | } 232 | } 233 | }) 234 | .use(rehypeStringify) 235 | .process( 236 | '
console.log("hi")
' 237 | ) 238 | 239 | assert.equal( 240 | String(file), 241 | `
242 |
console.log("hi")
243 | var console: Console 244 | (method) Console.log(...data: any[]): void 245 |
` 246 | ) 247 | assert.deepEqual(file.messages.map(String), []) 248 | } 249 | ) 250 | 251 | await t.test('should support custom tags', async function () { 252 | const file = await unified() 253 | .use(rehypeParse, {fragment: true}) 254 | .use(rehypeTwoslash, {twoslash: {customTags: ['thing']}}) 255 | .use(rehypeStringify) 256 | .process( 257 | `
// @thing: OK, sure
258 | const a = "123"
259 | // @thingTwo - This should stay (note the no ':')
260 | const b = 12331234
261 | 
` 262 | ) 263 | 264 | assert.equal( 265 | String(file), 266 | `
267 |
const a = "123"
268 | // @thingTwo - This should stay (note the no ':')
269 | const b = 12331234
270 | 
271 |
const a: "123"
272 |
const b: 12331234
273 |
` 274 | ) 275 | 276 | assert.deepEqual(file.messages.map(String), []) 277 | }) 278 | 279 | await t.test('should support a custom id prefix', async function () { 280 | const file = await unified() 281 | .use(rehypeParse, {fragment: true}) 282 | .use(rehypeTwoslash, {idPrefix: 'custom-'}) 283 | .use(rehypeStringify) 284 | .process( 285 | `
console.log('hi')
` 286 | ) 287 | 288 | assert.equal( 289 | String(file), 290 | `
291 |
console.log('hi')
292 |
var console: Console
293 |
(method) Console.log(...data: any[]): void
294 |
` 295 | ) 296 | 297 | assert.deepEqual(file.messages.map(String), []) 298 | }) 299 | 300 | await t.test('should report `twoslash` errors', async function () { 301 | const file = await unified() 302 | .use(rehypeParse, {fragment: true}) 303 | .use(rehypeTwoslash) 304 | .use(rehypeStringify) 305 | .process( 306 | `
// @annotate: left
307 | 
` 308 | ) 309 | 310 | assert.deepEqual(file.messages.map(String), [ 311 | '1:1-2:14: Unexpected error running twoslash' 312 | ]) 313 | }) 314 | 315 | await t.test('should warn w/ `notwoslash` class', async function () { 316 | const file = await unified() 317 | .use(rehypeParse, {fragment: true}) 318 | .use(rehypeTwoslash, {directive: false}) 319 | .use(rehypeStringify) 320 | .process('
# hi
') 321 | 322 | assert.equal( 323 | String(file), 324 | '
# hi
' 325 | ) 326 | assert.deepEqual(file.messages.map(String), [ 327 | '1:1-1:48: Unexpected `notwoslash` class, expected `no-twoslash`' 328 | ]) 329 | }) 330 | 331 | await t.test('should warn w/ `notwoslash` directive', async function () { 332 | const file = await unified() 333 | .use(remarkParse) 334 | .use(remarkRehype) 335 | .use(rehypeTwoslash) 336 | .use(rehypeStringify) 337 | .process('```markdown notwoslash\n# hi') 338 | 339 | assert.equal( 340 | String(file), 341 | '
# hi\n
' 342 | ) 343 | assert.deepEqual(file.messages.map(String), [ 344 | '1:1-2:5: Unexpected `notwoslash` directive, expected `no-twoslash`' 345 | ]) 346 | }) 347 | 348 | await t.test( 349 | 'should integrate w/ remark w/ twoslash directive', 350 | async function () { 351 | const file = await unified() 352 | .use(remarkParse) 353 | .use(remarkRehype) 354 | .use(rehypeTwoslash) 355 | .use(rehypeStringify) 356 | .process( 357 | ` 358 | ~~~ts twoslash 359 | console.log(3.14) 360 | ~~~ 361 | ` 362 | ) 363 | 364 | assert.equal( 365 | String(file), 366 | `
367 |
console.log(3.14)
368 | 
369 |
var console: Console
370 |
(method) Console.log(...data: any[]): void
371 |
` 372 | ) 373 | assert.deepEqual(file.messages.map(String), []) 374 | } 375 | ) 376 | 377 | await t.test( 378 | 'should integrate w/ remark, do nothing w/o twoslash directive', 379 | async function () { 380 | const file = await unified() 381 | .use(remarkParse) 382 | .use(remarkRehype) 383 | .use(rehypeTwoslash) 384 | .use(rehypeStringify) 385 | .process( 386 | ` 387 | ~~~ts 388 | console.log(3.14) 389 | ~~~ 390 | ` 391 | ) 392 | 393 | assert.doesNotMatch( 394 | String(file), 395 | /class="rehype-twoslash-popover-target"/ 396 | ) 397 | assert.deepEqual(file.messages.map(String), []) 398 | } 399 | ) 400 | 401 | await t.test( 402 | 'should integrate w/ remark, support `directive: false` w/o `twoslash` directive', 403 | async function () { 404 | const file = await unified() 405 | .use(remarkParse) 406 | .use(remarkRehype) 407 | .use(rehypeTwoslash, {directive: false}) 408 | .use(rehypeStringify) 409 | .process( 410 | ` 411 | ~~~ts 412 | console.log(3.14) 413 | ~~~ 414 | ` 415 | ) 416 | 417 | assert.match(String(file), /class="rehype-twoslash-popover-target"/) 418 | assert.deepEqual(file.messages.map(String), []) 419 | } 420 | ) 421 | 422 | await t.test( 423 | 'should integrate w/ remark, support `directive: false` w/ `no-twoslash` directive', 424 | async function () { 425 | const file = await unified() 426 | .use(remarkParse) 427 | .use(remarkRehype) 428 | .use(rehypeTwoslash, {directive: false}) 429 | .use(rehypeStringify) 430 | .process( 431 | ` 432 | ~~~ts no-twoslash 433 | console.log(3.14) 434 | ~~~ 435 | ` 436 | ) 437 | 438 | assert.doesNotMatch( 439 | String(file), 440 | /class="rehype-twoslash-popover-target"/ 441 | ) 442 | assert.deepEqual(file.messages.map(String), []) 443 | } 444 | ) 445 | 446 | await t.test('should support an ast', async function () { 447 | const tree = await unified() 448 | .use(rehypeTwoslash) 449 | .run({ 450 | type: 'root', 451 | children: [ 452 | { 453 | type: 'element', 454 | tagName: 'pre', 455 | properties: {}, 456 | children: [ 457 | { 458 | type: 'element', 459 | tagName: 'code', 460 | properties: {className: ['language-ts', 'twoslash']}, 461 | children: [{type: 'text', value: 'console.log(3.14)\n'}] 462 | } 463 | ] 464 | } 465 | ] 466 | }) 467 | 468 | assert.deepEqual(tree, { 469 | type: 'root', 470 | children: [ 471 | { 472 | type: 'element', 473 | tagName: 'div', 474 | properties: {className: ['highlight', 'highlight-ts']}, 475 | children: [ 476 | {type: 'text', value: '\n'}, 477 | { 478 | type: 'element', 479 | tagName: 'pre', 480 | properties: {}, 481 | children: [ 482 | { 483 | type: 'element', 484 | tagName: 'code', 485 | properties: {className: ['language-ts', 'twoslash']}, 486 | children: [ 487 | { 488 | type: 'element', 489 | tagName: 'span', 490 | properties: { 491 | className: ['rehype-twoslash-popover-target'], 492 | dataPopoverTarget: 'rehype-twoslash-cc-0' 493 | }, 494 | children: [ 495 | { 496 | type: 'element', 497 | tagName: 'span', 498 | properties: {className: ['pl-c1']}, 499 | children: [{type: 'text', value: 'console'}] 500 | } 501 | ] 502 | }, 503 | {type: 'text', value: '.'}, 504 | { 505 | type: 'element', 506 | tagName: 'span', 507 | properties: { 508 | className: ['rehype-twoslash-popover-target'], 509 | dataPopoverTarget: 'rehype-twoslash-cc-1' 510 | }, 511 | children: [ 512 | { 513 | type: 'element', 514 | tagName: 'span', 515 | properties: {className: ['pl-c1']}, 516 | children: [{type: 'text', value: 'log'}] 517 | } 518 | ] 519 | }, 520 | {type: 'text', value: '('}, 521 | { 522 | type: 'element', 523 | tagName: 'span', 524 | properties: {className: ['pl-c1']}, 525 | children: [{type: 'text', value: '3.14'}] 526 | }, 527 | {type: 'text', value: ')\n'} 528 | ] 529 | } 530 | ] 531 | }, 532 | {type: 'text', value: '\n'}, 533 | { 534 | type: 'element', 535 | tagName: 'div', 536 | properties: { 537 | className: ['rehype-twoslash-hover', 'rehype-twoslash-popover'], 538 | id: 'rehype-twoslash-cc-0', 539 | popover: '' 540 | }, 541 | children: [ 542 | { 543 | type: 'element', 544 | tagName: 'pre', 545 | properties: {className: ['rehype-twoslash-popover-code']}, 546 | children: [ 547 | { 548 | type: 'element', 549 | tagName: 'code', 550 | properties: {className: ['language-ts']}, 551 | children: [ 552 | { 553 | type: 'element', 554 | tagName: 'span', 555 | properties: {className: ['pl-k']}, 556 | children: [{type: 'text', value: 'var'}] 557 | }, 558 | {type: 'text', value: ' '}, 559 | { 560 | type: 'element', 561 | tagName: 'span', 562 | properties: {className: ['pl-smi']}, 563 | children: [{type: 'text', value: 'console'}] 564 | }, 565 | { 566 | type: 'element', 567 | tagName: 'span', 568 | properties: {className: ['pl-k']}, 569 | children: [{type: 'text', value: ':'}] 570 | }, 571 | {type: 'text', value: ' '}, 572 | { 573 | type: 'element', 574 | tagName: 'span', 575 | properties: {className: ['pl-en']}, 576 | children: [{type: 'text', value: 'Console'}] 577 | } 578 | ] 579 | } 580 | ] 581 | } 582 | ] 583 | }, 584 | {type: 'text', value: '\n'}, 585 | { 586 | type: 'element', 587 | tagName: 'div', 588 | properties: { 589 | className: ['rehype-twoslash-hover', 'rehype-twoslash-popover'], 590 | id: 'rehype-twoslash-cc-1', 591 | popover: '' 592 | }, 593 | children: [ 594 | { 595 | type: 'element', 596 | tagName: 'pre', 597 | properties: {className: ['rehype-twoslash-popover-code']}, 598 | children: [ 599 | { 600 | type: 'element', 601 | tagName: 'code', 602 | properties: {className: ['language-ts']}, 603 | children: [ 604 | {type: 'text', value: '('}, 605 | { 606 | type: 'element', 607 | tagName: 'span', 608 | properties: {className: ['pl-smi']}, 609 | children: [{type: 'text', value: 'method'}] 610 | }, 611 | {type: 'text', value: ') '}, 612 | { 613 | type: 'element', 614 | tagName: 'span', 615 | properties: {className: ['pl-c1']}, 616 | children: [{type: 'text', value: 'Console'}] 617 | }, 618 | {type: 'text', value: '.'}, 619 | { 620 | type: 'element', 621 | tagName: 'span', 622 | properties: {className: ['pl-en']}, 623 | children: [{type: 'text', value: 'log'}] 624 | }, 625 | {type: 'text', value: '('}, 626 | { 627 | type: 'element', 628 | tagName: 'span', 629 | properties: {className: ['pl-k']}, 630 | children: [{type: 'text', value: '...'}] 631 | }, 632 | { 633 | type: 'element', 634 | tagName: 'span', 635 | properties: {className: ['pl-smi']}, 636 | children: [{type: 'text', value: 'data'}] 637 | }, 638 | {type: 'text', value: ': '}, 639 | { 640 | type: 'element', 641 | tagName: 'span', 642 | properties: {className: ['pl-smi']}, 643 | children: [{type: 'text', value: 'any'}] 644 | }, 645 | {type: 'text', value: '[]): '}, 646 | { 647 | type: 'element', 648 | tagName: 'span', 649 | properties: {className: ['pl-k']}, 650 | children: [{type: 'text', value: 'void'}] 651 | } 652 | ] 653 | } 654 | ] 655 | }, 656 | { 657 | type: 'element', 658 | tagName: 'div', 659 | properties: { 660 | className: ['rehype-twoslash-popover-description'] 661 | }, 662 | children: [ 663 | { 664 | type: 'element', 665 | tagName: 'p', 666 | properties: {}, 667 | children: [ 668 | { 669 | type: 'element', 670 | tagName: 'a', 671 | properties: { 672 | href: 'https://developer.mozilla.org/docs/Web/API/console/log_static' 673 | }, 674 | children: [{type: 'text', value: 'MDN Reference'}] 675 | } 676 | ] 677 | } 678 | ] 679 | } 680 | ] 681 | }, 682 | {type: 'text', value: '\n'} 683 | ] 684 | } 685 | ] 686 | }) 687 | }) 688 | }) 689 | 690 | test('fixtures', async function (t) { 691 | const base = new URL('fixtures/', import.meta.url) 692 | const folders = await fs.readdir(base) 693 | 694 | for (const folder of folders) { 695 | if (folder.charAt(0) === '.') continue 696 | 697 | await t.test(folder, async function () { 698 | const folderUrl = new URL(folder + '/', base) 699 | const outputUrl = new URL('output.html', folderUrl) 700 | const input = await read(new URL('input.html', folderUrl)) 701 | const processor = await unified() 702 | .use(rehypeParse, {fragment: true}) 703 | .use(rehypeTwoslash, {directive: false}) 704 | .use(rehypeStringify) 705 | 706 | await processor.process(input) 707 | 708 | /** @type {VFile} */ 709 | let output 710 | 711 | try { 712 | if ('UPDATE' in process.env) { 713 | throw new Error('Updating…') 714 | } 715 | 716 | output = await read(outputUrl) 717 | output.value = String(output) 718 | } catch { 719 | output = new VFile({ 720 | path: outputUrl, 721 | value: String(input) 722 | }) 723 | await write(output) 724 | } 725 | 726 | assert.equal(String(input), String(output)) 727 | assert.deepEqual(input.messages.map(String), []) 728 | }) 729 | } 730 | }) 731 | -------------------------------------------------------------------------------- /test/fixtures/import-and-node-types/output.html: -------------------------------------------------------------------------------- 1 |
2 |
/// <reference types="node" />
  3 | 
  4 | // @ts-check
  5 | import fs from "fs"
  6 | import { execSync } from "child_process"
  7 | 
  8 | const fileToEdit = process.env.HUSKY_GIT_PARAMS!.split(" ")[0]
  9 | const files = execSync("git status --porcelain", { encoding: "utf8" })
 10 | 
 11 | const maps: any = {
 12 |   "spelltower/": "SPTWR",
 13 |   "typeshift/": "TPSFT",
 14 | }
 15 | 
 16 | const prefixes = new Set()
 17 | files.split("\n").forEach(f => {
 18 |   const found = Object.keys(maps).find(prefix => f.includes(prefix))
 19 |   if (found) prefixes.add(maps[found])
 20 | })
 21 | 
 22 | if (prefixes.size) {
 23 |   const prefix = [...prefixes.values()].sort().join(", ")
 24 |   const msg = fs.readFileSync(fileToEdit, "utf8")
 25 |   if (!msg.includes(prefix)) {
 26 |     fs.writeFileSync(fileToEdit, `[${prefix}] ${msg}`)
 27 |   }
 28 | }
 29 | 
30 |
(alias) module "fs"
 31 | import fs

The node:fs module enables interacting with the file system in a 32 | way modeled on standard POSIX functions.

33 |

To use the promise-based APIs:

34 |
import * as fs from 'node:fs/promises';
 35 | 
36 |

To use the callback and sync APIs:

37 |
import * as fs from 'node:fs';
 38 | 
39 |

All file system operations have synchronous, callback, and promise-based 40 | forms, and are accessible using both CommonJS syntax and ES6 Modules (ESM).

41 |
45 |
(alias) function execSync(command: string): Buffer (+3 overloads)
 46 | import execSync

The child_process.execSync() method is generally identical to 47 | {@link 48 | exec 49 | } 50 | with the exception that the method will not return 51 | until the child process has fully closed. When a timeout has been encountered 52 | and killSignal is sent, the method won't return until the process has 53 | completely exited. If the child process intercepts and handles the SIGTERM signal and doesn't exit, the parent process will wait until the child process 54 | has exited.

55 |

If the process times out or has a non-zero exit code, this method will throw. 56 | The Error object will contain the entire result from 57 | {@link 58 | spawnSync 59 | } 60 | .

61 |

Never pass unsanitized user input to this function. Any input containing shell 62 | metacharacters may be used to trigger arbitrary command execution.

63 |
    64 |
  • @since 65 | v0.11.12
  • 66 |
  • @param 67 | command The command to run.
  • 68 |
  • @return 69 | The stdout from the command.
  • 70 |
71 |
const fileToEdit: string
72 |
var process: NodeJS.Process
73 |
(property) NodeJS.Process.env: NodeJS.ProcessEnv

The process.env property returns an object containing the user environment. 74 | See environ(7).

75 |

An example of this object looks like:

76 |
{
 77 |   TERM: 'xterm-256color',
 78 |   SHELL: '/usr/local/bin/bash',
 79 |   USER: 'maciej',
 80 |   PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
 81 |   PWD: '/Users/maciej',
 82 |   EDITOR: 'vim',
 83 |   SHLVL: '1',
 84 |   HOME: '/Users/maciej',
 85 |   LOGNAME: 'maciej',
 86 |   _: '/usr/local/bin/node'
 87 | }
 88 | 
89 |

It is possible to modify this object, but such modifications will not be 90 | reflected outside the Node.js process, or (unless explicitly requested) 91 | to other Worker threads. 92 | In other words, the following example would not work:

93 |
node -e 'process.env.foo = "bar"' &#x26;&#x26; echo $foo
 94 | 
95 |

While the following will:

96 |
import { env } from 'node:process';
 97 | 
 98 | env.foo = 'bar';
 99 | console.log(env.foo);
100 | 
101 |

Assigning a property on process.env will implicitly convert the value 102 | to a string. This behavior is deprecated. Future versions of Node.js may 103 | throw an error when the value is not a string, number, or boolean.

104 |
import { env } from 'node:process';
105 | 
106 | env.test = null;
107 | console.log(env.test);
108 | // => 'null'
109 | env.test = undefined;
110 | console.log(env.test);
111 | // => 'undefined'
112 | 
113 |

Use delete to delete a property from process.env.

114 |
import { env } from 'node:process';
115 | 
116 | env.TEST = 1;
117 | delete env.TEST;
118 | console.log(env.TEST);
119 | // => undefined
120 | 
121 |

On Windows operating systems, environment variables are case-insensitive.

122 |
import { env } from 'node:process';
123 | 
124 | env.TEST = 1;
125 | console.log(env.test);
126 | // => 1
127 | 
128 |

Unless explicitly specified when creating a Worker instance, 129 | each Worker thread has its own copy of process.env, based on its 130 | parent thread's process.env, or whatever was specified as the env option 131 | to the Worker constructor. Changes to process.env will not be visible 132 | across Worker threads, and only the main thread can make changes that 133 | are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner 134 | unlike the main thread.

135 |
    136 |
  • @since 137 | v0.1.27
  • 138 |
139 |
string | undefined
140 |
(method) String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)

Split a string into substrings using the specified separator and return them as an array.

141 |
    142 |
  • @param 143 | separator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned.
  • 144 |
  • @param 145 | limit A value used to limit the number of elements returned in the array.
  • 146 |
147 |
const files: string
148 |
(alias) execSync(command: string, options: ExecSyncOptionsWithStringEncoding): string (+3 overloads)
149 | import execSync

The child_process.execSync() method is generally identical to 150 | {@link 151 | exec 152 | } 153 | with the exception that the method will not return 154 | until the child process has fully closed. When a timeout has been encountered 155 | and killSignal is sent, the method won't return until the process has 156 | completely exited. If the child process intercepts and handles the SIGTERM signal and doesn't exit, the parent process will wait until the child process 157 | has exited.

158 |

If the process times out or has a non-zero exit code, this method will throw. 159 | The Error object will contain the entire result from 160 | {@link 161 | spawnSync 162 | } 163 | .

164 |

Never pass unsanitized user input to this function. Any input containing shell 165 | metacharacters may be used to trigger arbitrary command execution.

166 |
    167 |
  • @since 168 | v0.11.12
  • 169 |
  • @param 170 | command The command to run.
  • 171 |
  • @return 172 | The stdout from the command.
  • 173 |
174 |
(property) ExecSyncOptionsWithStringEncoding.encoding: BufferEncoding
175 |
const maps: any
176 |
const prefixes: Set<unknown>
177 |
var Set: SetConstructor
178 | new <unknown>(iterable?: Iterable<unknown> | null | undefined) => Set<unknown> (+1 overload)
179 |
const files: string
180 |
(method) String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)

Split a string into substrings using the specified separator and return them as an array.

181 |
    182 |
  • @param 183 | separator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned.
  • 184 |
  • @param 185 | limit A value used to limit the number of elements returned in the array.
  • 186 |
187 |
(method) Array<string>.forEach(callbackfn: (value: string, index: number, array: string[]) => void, thisArg?: any): void

Performs the specified action for each element in an array.

188 |
    189 |
  • @param 190 | callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.
  • 191 |
  • @param 192 | thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
  • 193 |
194 |
(parameter) f: string
195 |
const found: string | undefined
196 |
var Object: ObjectConstructor

Provides functionality common to all JavaScript objects.

197 |
(method) ObjectConstructor.keys(o: {}): string[] (+1 overload)

Returns the names of the enumerable string properties and methods of an object.

198 |
    199 |
  • @param 200 | o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
  • 201 |
202 |
const maps: any
203 |
(method) Array<string>.find(predicate: (value: string, index: number, obj: string[]) => unknown, thisArg?: any): string | undefined (+1 overload)

Returns the value of the first element in the array where predicate is true, and undefined 204 | otherwise.

205 |
    206 |
  • @param 207 | predicate find calls predicate once for each element of the array, in ascending 208 | order, until it finds one where predicate returns true. If such an element is found, find 209 | immediately returns that element value. Otherwise, find returns undefined.
  • 210 |
  • @param 211 | thisArg If provided, it will be used as the this value for each invocation of 212 | predicate. If it is not provided, undefined is used instead.
  • 213 |
214 |
(parameter) prefix: string
215 |
(parameter) f: string
216 |
(method) String.includes(searchString: string, position?: number): boolean

Returns true if searchString appears as a substring of the result of converting this 217 | object to a String, at one or more positions that are 218 | greater than or equal to position; otherwise, returns false.

219 |
    220 |
  • @param 221 | searchString search string
  • 222 |
  • @param 223 | position If position is undefined, 0 is assumed, so as to search all of the String.
  • 224 |
225 |
(parameter) prefix: string
226 |
const found: string | undefined
227 |
const prefixes: Set<unknown>
228 |
(method) Set<unknown>.add(value: unknown): Set<unknown>

Appends a new element with a specified value to the end of the Set.

229 |
const maps: any
230 |
const found: string
231 |
const prefixes: Set<unknown>
232 |
(property) Set<unknown>.size: number
    233 |
  • @returns 234 | the number of (unique) elements in Set.
  • 235 |
236 |
const prefix: string
237 |
const prefixes: Set<unknown>
238 |
(method) Set<unknown>.values(): IterableIterator<unknown>

Returns an iterable of values in the set.

239 |
(method) Array<unknown>.sort(compareFn?: ((a: unknown, b: unknown) => number) | undefined): unknown[]

Sorts an array in place. 240 | This method mutates the array and returns a reference to the same array.

241 |
    242 |
  • @param 243 | compareFn Function used to determine the order of the elements. It is expected to return 244 | a negative value if the first argument is less than the second argument, zero if they're equal, and a positive 245 | value otherwise. If omitted, the elements are sorted in ascending, ASCII character order. 246 |
    [11,2,22,1].sort((a, b) => a - b)
    247 | 
    248 |
  • 249 |
250 |
(method) Array<unknown>.join(separator?: string): string

Adds all the elements of an array into a string, separated by the specified separator string.

251 |
    252 |
  • @param 253 | separator A string used to separate one element of the array from the next in the resulting string. If omitted, the array elements are separated with a comma.
  • 254 |
255 |
const msg: string
256 |
(alias) module "fs"
257 | import fs

The node:fs module enables interacting with the file system in a 258 | way modeled on standard POSIX functions.

259 |

To use the promise-based APIs:

260 |
import * as fs from 'node:fs/promises';
261 | 
262 |

To use the callback and sync APIs:

263 |
import * as fs from 'node:fs';
264 | 
265 |

All file system operations have synchronous, callback, and promise-based 266 | forms, and are accessible using both CommonJS syntax and ES6 Modules (ESM).

267 |
    268 |
  • @see 269 | source
  • 270 |
271 |
function readFileSync(path: fs.PathOrFileDescriptor, options: {
272 |     encoding: BufferEncoding;
273 |     flag?: string | undefined;
274 | } | BufferEncoding): string (+2 overloads)

Synchronously reads the entire contents of a file.

275 |
    276 |
  • @param 277 | path A path to a file. If a URL is provided, it must use the file: protocol. 278 | If a file descriptor is provided, the underlying file will not be closed automatically.
  • 279 |
  • @param 280 | options Either the encoding for the result, or an object that contains the encoding and an optional flag. 281 | If a flag is not provided, it defaults to 'r'.
  • 282 |
283 |
const fileToEdit: string
284 |
const msg: string
285 |
(method) String.includes(searchString: string, position?: number): boolean

Returns true if searchString appears as a substring of the result of converting this 286 | object to a String, at one or more positions that are 287 | greater than or equal to position; otherwise, returns false.

288 |
    289 |
  • @param 290 | searchString search string
  • 291 |
  • @param 292 | position If position is undefined, 0 is assumed, so as to search all of the String.
  • 293 |
294 |
const prefix: string
295 |
(alias) module "fs"
296 | import fs

The node:fs module enables interacting with the file system in a 297 | way modeled on standard POSIX functions.

298 |

To use the promise-based APIs:

299 |
import * as fs from 'node:fs/promises';
300 | 
301 |

To use the callback and sync APIs:

302 |
import * as fs from 'node:fs';
303 | 
304 |

All file system operations have synchronous, callback, and promise-based 305 | forms, and are accessible using both CommonJS syntax and ES6 Modules (ESM).

306 |
    307 |
  • @see 308 | source
  • 309 |
310 |
function writeFileSync(file: fs.PathOrFileDescriptor, data: string | NodeJS.ArrayBufferView, options?: fs.WriteFileOptions): void

Returns undefined.

311 |

The mode option only affects the newly created file. See 312 | {@link 313 | open 314 | } 315 | for more details.

316 |

For detailed information, see the documentation of the asynchronous version of 317 | this API: 318 | {@link 319 | writeFile 320 | } 321 | .

322 |
    323 |
  • @since 324 | v0.1.29
  • 325 |
  • @param 326 | file filename or file descriptor
  • 327 |
328 |
const fileToEdit: string
329 |
const prefix: string
330 |
const msg: string
331 |
332 | --------------------------------------------------------------------------------