├── .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 sound4 | 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 sound4 | 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 | Errors// @errors: 2322 2588
4 | const str: string = 1
5 | str = 'Hello'
6 |
7 |
8 | noErrors// @noErrors
10 | const str: string = 1
11 | str = 'Hello'
12 |
13 |
14 | noErrorsCutted// @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// @noErrorValidation
23 | const str: string = 1
24 |
25 |
26 | keepNotations// @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 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 | noImplicitAny
// @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
// @showEmit
12 | const level: string = 'Danger'
13 |
14 | showEmittedFile
// @declaration
16 | // @showEmit
17 | // @showEmittedFile: index.d.ts
18 | export const hello = 'world'
19 |
20 | showEmittedFile (index.js.map)
// @sourceMap
22 | // @showEmit
23 | // @showEmittedFile: index.js.map
24 | export const hello = 'world'
25 |
26 | showEmittedFile (index.d.ts.map)
// @declaration
28 | // @declarationMap
29 | // @showEmit
30 | // @showEmittedFile: index.d.ts.map
31 | export const hello: string = 'world'
32 |
33 | showEmittedFile (js)
// @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 | /// console.log(name)
3 | var console: Console(method) Console.log(...data: any[]): voidconst name: voidconst hi = 'Hello'
3 | const msg = `${hi}, world`
4 |
5 | const hi: "Hello"const msg: "Hello, world"const msg: "Hello, world"const hi: "Hello"function add(a: number, b: number) {
3 | return a + b
4 | }
5 |
6 | function add(a: number, b: number): number(parameter) a: number(parameter) b: number(parameter) a: number(parameter) b: numbernoImplicitAny
// 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(parameter) a: any(parameter) a: anyshowEmit
const level = 'Danger';
15 | export {};
16 |
17 | showEmittedFile
export declare const hello = "world";
21 |
22 | showEmittedFile (index.js.map)
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,KAAK,GAAG,OAAO,CAAA"}
26 | showEmittedFile (index.d.ts.map)
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,EAAE,MAAgB,CAAA"}
30 | showEmittedFile (js)
// @filename: b.ts
34 | import { helloWorld } from './a';
35 | console.log(helloWorld);
36 |
37 | ` 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 | - 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 | - error
- err
20 | var console: Console
21 | any
22 | - error
- err
23 | var console: Console
24 | - assert
- clear
- count
- countReset
- debug
- dir
- dirxml
- error
- group
- groupCollapsed
- groupEnd
- …
25 | var console: Console
26 | - 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 | - exa
40 | var console: Console
41 | any
42 | - error
- exa
43 | var console: Console
44 | - assert
- clear
- count
- countReset
- debug
- dir
- dirxml
- error
- group
- groupCollapsed
- groupEnd
- …
45 | var console: Console
46 | - 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 |
58 | var console: Console
59 | (method) Console.log(...data: any[]): void
60 | const rule: CSSRule
61 | any
62 | - type
63 |
64 |
65 | A completion annotation, not at the end.
66 |
67 | console.t
68 |
69 | var console: Console
70 | - confirm
- console
- const
- 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 |
42 | - @see
43 | source
44 |
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"' && 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 |
--------------------------------------------------------------------------------