├── apps └── vscode-extension │ ├── .gitignore │ ├── assets │ └── icon.png │ ├── scripts │ ├── process-shim.js │ └── build.js │ ├── src │ ├── provider │ │ ├── uriStore.ts │ │ ├── hoverProvider.ts │ │ └── selectedTextHoverProvider.ts │ ├── test │ │ ├── suite │ │ │ ├── extension.test.ts │ │ │ └── index.ts │ │ └── runTest.ts │ └── extension.ts │ ├── tsconfig.json │ ├── syntaxes │ └── type.tmGrammar.json │ ├── LICENSE │ └── package.json ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── assets ├── empty.png ├── icon.png ├── this.png ├── errors-hover.png ├── mentions │ ├── vote.png │ ├── js-nation.png │ ├── theo-dark.png │ ├── tanner-dark.png │ ├── theo-light.png │ ├── theo-video.png │ ├── johnson-dark.png │ ├── johnson-light.png │ └── tanner-light.png └── instead-of-that.png ├── packages ├── vscode-formatter │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── miniLine.ts │ │ │ ├── plainCodeBlock.ts │ │ │ ├── spanBreak.ts │ │ │ ├── anyCodeBlock.ts │ │ │ ├── codeBlock.ts │ │ │ └── title.ts │ │ └── format │ │ │ ├── formatDiagnostic.ts │ │ │ ├── embedSymbolLinks.ts │ │ │ └── identSentences.ts │ ├── tsconfig.json │ └── package.json ├── formatter │ ├── src │ │ ├── index.ts │ │ ├── prettify.ts │ │ ├── addMissingParentheses.ts │ │ ├── formatTypeBlock.ts │ │ └── formatDiagnosticMessage.ts │ ├── tsconfig.json │ ├── README.md │ ├── vitest.config.ts │ ├── package.json │ └── test │ │ ├── formatter.vitest.ts │ │ └── errorMessageMocks.ts └── utils │ ├── tsconfig.json │ ├── package.json │ └── src │ └── index.ts ├── .prettierrc ├── .gitignore ├── examples ├── examples.type ├── errors.vue ├── errors.ts └── errors.js ├── CONTRIBUTING.md ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── turbo.json ├── tsconfig.json ├── package.json ├── docs ├── pretty-ts-errors-hack.css └── hide-original-errors.md ├── tsconfig.base.json ├── eslint.config.mjs ├── LICENSE └── README.md /apps/vscode-extension/.gitignore: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [yoavbls, kevinramharak] 2 | -------------------------------------------------------------------------------- /assets/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/empty.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/this.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/this.png -------------------------------------------------------------------------------- /packages/vscode-formatter/README.md: -------------------------------------------------------------------------------- 1 | # Pretty TypeScript Errors - Formatter for VSCode hovers 2 | -------------------------------------------------------------------------------- /assets/errors-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/errors-hover.png -------------------------------------------------------------------------------- /assets/mentions/vote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/mentions/vote.png -------------------------------------------------------------------------------- /packages/formatter/src/index.ts: -------------------------------------------------------------------------------- 1 | export { formatDiagnosticMessage } from "./formatDiagnosticMessage"; 2 | -------------------------------------------------------------------------------- /packages/vscode-formatter/src/index.ts: -------------------------------------------------------------------------------- 1 | export { formatDiagnostic } from "./format/formatDiagnostic"; 2 | -------------------------------------------------------------------------------- /assets/instead-of-that.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/instead-of-that.png -------------------------------------------------------------------------------- /assets/mentions/js-nation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/mentions/js-nation.png -------------------------------------------------------------------------------- /assets/mentions/theo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/mentions/theo-dark.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /assets/mentions/tanner-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/mentions/tanner-dark.png -------------------------------------------------------------------------------- /assets/mentions/theo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/mentions/theo-light.png -------------------------------------------------------------------------------- /assets/mentions/theo-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/mentions/theo-video.png -------------------------------------------------------------------------------- /assets/mentions/johnson-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/mentions/johnson-dark.png -------------------------------------------------------------------------------- /assets/mentions/johnson-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/mentions/johnson-light.png -------------------------------------------------------------------------------- /assets/mentions/tanner-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/assets/mentions/tanner-light.png -------------------------------------------------------------------------------- /apps/vscode-extension/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavbls/pretty-ts-errors/HEAD/apps/vscode-extension/assets/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .turbo 7 | .vercel 8 | .idea 9 | **/.DS_Store 10 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /packages/vscode-formatter/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { inlineCodeBlock, multiLineCodeBlock } from "./codeBlock"; 2 | export { unstyledCodeBlock } from "./plainCodeBlock"; 3 | -------------------------------------------------------------------------------- /examples/examples.type: -------------------------------------------------------------------------------- 1 | { 2 | fistName: string; 3 | lastName: string; 4 | age: number; 5 | } | undefined | null; 6 | 7 | { 8 | x: string; 9 | y: number; 10 | }[] -------------------------------------------------------------------------------- /packages/vscode-formatter/src/components/miniLine.ts: -------------------------------------------------------------------------------- 1 | import { spanBreak } from "./spanBreak"; 2 | 3 | /** May be useful for line separations */ 4 | export const miniLine = spanBreak(/*html*/ `

`); 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contribute good stuff 2 | 3 | If you're looking for ideas, check out our [board](https://github.com/users/yoavbls/projects/3) and [open issues](https://github.com/yoavbls/pretty-ts-errors/issues) 4 | -------------------------------------------------------------------------------- /apps/vscode-extension/scripts/process-shim.js: -------------------------------------------------------------------------------- 1 | // https://esbuild.github.io/api/#inject 2 | 3 | let _cwd = "/"; 4 | 5 | export let process = { 6 | cwd: () => _cwd, 7 | chdir: (newCwd) => (_cwd = newCwd), 8 | env: {}, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/provider/uriStore.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString, Range, Uri } from "vscode"; 2 | 3 | export const uriStore: Record< 4 | Uri["path"], 5 | { 6 | range: Range; 7 | contents: MarkdownString[]; 8 | }[] 9 | > = {}; 10 | -------------------------------------------------------------------------------- /examples/errors.vue: -------------------------------------------------------------------------------- 1 | 10 | 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "connor4312.esbuild-problem-matchers" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /apps/vscode-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "ES2022", 6 | "rootDir": "src", 7 | "outDir": "out", 8 | "moduleResolution": "node", 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "composite": true, 8 | "declaration": true, 9 | "declarationMap": true 10 | }, 11 | "include": ["src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "tasks": { 5 | "build": { 6 | "outputs": ["dist/**"] 7 | }, 8 | "lint": {}, 9 | "dev": { 10 | "cache": false, 11 | "persistent": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/vscode-formatter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "composite": true, 8 | "declaration": true, 9 | "declarationMap": true 10 | }, 11 | "include": ["src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.base.json", 4 | "files": [], 5 | "references": [ 6 | { "path": "packages/utils" }, 7 | { "path": "packages/formatter" }, 8 | { "path": "packages/vscode-formatter" }, 9 | { "path": "apps/vscode-extension" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@pretty-ts-errors/utils", 4 | "files": [ 5 | "dist/**" 6 | ], 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "build": "tsc -p ." 11 | }, 12 | "devDependencies": {}, 13 | "dependencies": { 14 | "ts-dedent": "^2.2.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/formatter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "composite": true, 8 | "declaration": true, 9 | "declarationMap": true 10 | }, 11 | "include": ["src/**/*"], 12 | "references": [{ "path": "../utils" }] 13 | } 14 | -------------------------------------------------------------------------------- /packages/vscode-formatter/src/components/plainCodeBlock.ts: -------------------------------------------------------------------------------- 1 | import { d } from "@pretty-ts-errors/utils"; 2 | 3 | /** 4 | * Code block without syntax highlighting like. 5 | * For syntax highlighting, use {@link inlineCodeBlock} or {@link multiLineCodeBlock} 6 | */ 7 | export const unstyledCodeBlock = (content: string) => d/*html*/ ` 8 | ${content} 9 | `; 10 | -------------------------------------------------------------------------------- /packages/formatter/src/prettify.ts: -------------------------------------------------------------------------------- 1 | import parserTypescript from "prettier/parser-typescript"; 2 | import { format } from "prettier/standalone"; 3 | 4 | export function prettify(text: string) { 5 | return format(text, { 6 | plugins: [parserTypescript], 7 | parser: "typescript", 8 | printWidth: 60, 9 | singleAttributePerLine: false, 10 | arrowParens: "avoid", 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /apps/vscode-extension/syntaxes/type.tmGrammar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TypeScript Type", 3 | "scopeName": "source.type", 4 | "patterns": [ 5 | { 6 | "include": "source.ts#type" 7 | }, 8 | { 9 | "include": "source.ts#directives" 10 | }, 11 | { 12 | "include": "source.ts#statements" 13 | }, 14 | { 15 | "include": "source.ts#shebang" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/vscode-formatter/src/components/spanBreak.ts: -------------------------------------------------------------------------------- 1 | import { d } from "@pretty-ts-errors/utils"; 2 | 3 | /** 4 | * Since every thing in the extension hover split into spans, 5 | * we need to close the previous span before we're opening a new one 6 | * Note: the line breaks is important here 7 | */ 8 | export const spanBreak = (children: string) => d/*html*/ ` 9 | 10 | ${children} 11 | 12 | `; 13 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | // You can import and use all API from the 'vscode' module 2 | // as well as import your extension to test it 3 | import * as vscode from "vscode"; 4 | 5 | suite("Extension Test Suite", () => { 6 | /** 7 | * The tests moved to the formatter package, I'm leaving 8 | * this here for future tests to the VSCode extension 9 | */ 10 | vscode.window.showInformationMessage("Start all tests."); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/vscode-formatter/src/components/anyCodeBlock.ts: -------------------------------------------------------------------------------- 1 | import { inlineCodeBlock, multiLineCodeBlock } from "./codeBlock"; 2 | import { unstyledCodeBlock } from "./plainCodeBlock"; 3 | 4 | export const anyCodeBlock = ( 5 | code: string, 6 | language?: string, 7 | multiLine?: boolean 8 | ) => { 9 | if (!language) { 10 | return unstyledCodeBlock(code); 11 | } 12 | if (multiLine) { 13 | return multiLineCodeBlock(code, language); 14 | } 15 | return inlineCodeBlock(code, language); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/formatter/README.md: -------------------------------------------------------------------------------- 1 | # Pretty TypeScript Error formatter 2 | 3 | The formatting package of [pretty-ts-errors](https://github.com/yoavbls/pretty-ts-errors) 4 | 5 | # Usage 6 | 7 | ```typescript 8 | import { formatDiagnosticMessage } from "@pretty-ts-errors/formatter"; 9 | 10 | function codeBlock(code: string, language?: string, multiLine?: boolean) { 11 | return `\`\`\`${language} 12 | ${code} 13 | \`\`\` 14 | `; 15 | } 16 | 17 | formatDiagnosticMessage( 18 | `Type 'string' is not assignable to type 'number'.`, 19 | codeBlock 20 | ); 21 | ``` 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Expected behavior** 13 | A clear and concise description of what you expected to happen. 14 | 15 | **Original error** 16 | If this bug is related to an error that is not formatting well, please 17 | attach the original error in a code block: 18 | 19 | Type 'number' is not assignable to type 'string'.ts(2322) 20 | 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "turbo run build", 5 | "dev": "turbo run dev", 6 | "lint": "turbo run lint", 7 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 8 | }, 9 | "engines": { 10 | "node": ">=20" 11 | }, 12 | "devDependencies": { 13 | "@eslint/js": "^9.15.0", 14 | "@types/node": "^20.19.21", 15 | "eslint": "^9.15.0", 16 | "globals": "^16.4.0", 17 | "prettier": "^2.8.8", 18 | "turbo": "^2.1.3", 19 | "typescript": "^5.7.2", 20 | "typescript-eslint": "^8.15.0" 21 | }, 22 | "name": "pretty-ts-errors-mono", 23 | "packageManager": "npm@10.0.0", 24 | "workspaces": [ 25 | "packages/*", 26 | "apps/*" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/provider/hoverProvider.ts: -------------------------------------------------------------------------------- 1 | import { HoverProvider } from "vscode"; 2 | import { uriStore } from "./uriStore"; 3 | 4 | export const hoverProvider: HoverProvider = { 5 | provideHover(document, position, _token) { 6 | const itemsInUriStore = uriStore[document.uri.fsPath]; 7 | 8 | if (!itemsInUriStore) { 9 | return null; 10 | } 11 | 12 | const itemInRange = itemsInUriStore.filter((item) => 13 | item.range.contains(position) 14 | ); 15 | 16 | if (itemInRange.length === 0) { 17 | return null; 18 | } 19 | 20 | const first = itemInRange[0]; 21 | if (!first) { 22 | return null; 23 | } 24 | return { 25 | range: first.range, 26 | contents: itemInRange.flatMap((item) => item.contents), 27 | }; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /packages/vscode-formatter/src/format/formatDiagnostic.ts: -------------------------------------------------------------------------------- 1 | import { formatDiagnosticMessage } from "@pretty-ts-errors/formatter"; 2 | import { Diagnostic } from "vscode-languageserver-types"; 3 | import { title } from "../components/title"; 4 | import { anyCodeBlock } from "../components/anyCodeBlock"; 5 | import { d } from "@pretty-ts-errors/utils"; 6 | import { embedSymbolLinks } from "./embedSymbolLinks"; 7 | import { identSentences } from "./identSentences"; 8 | 9 | export function formatDiagnostic(diagnostic: Diagnostic) { 10 | const newDiagnostic = embedSymbolLinks(diagnostic); 11 | 12 | return d/*html*/ ` 13 | ${title(diagnostic)} 14 | 15 | ${formatDiagnosticMessage( 16 | identSentences(newDiagnostic.message), 17 | anyCodeBlock 18 | )} 19 | 20 | `; 21 | } 22 | -------------------------------------------------------------------------------- /docs/pretty-ts-errors-hack.css: -------------------------------------------------------------------------------- 1 | /* Allow copying */ 2 | .codicon-none { 3 | user-select: text !important; 4 | -webkit-user-select: text !important; 5 | } 6 | 7 | /* Hide errors */ 8 | div.monaco-hover-content:has(.codicon-none) > .hover-row:first-child { 9 | display: none !important; 10 | } 11 | div.monaco-hover-content:has([style="color:#f96363;"]) > .hover-row:first-child { 12 | display: none !important 13 | } 14 | 15 | /* Change order */ 16 | .monaco-hover .monaco-hover-content { 17 | display: flex; 18 | flex-direction: column; 19 | } 20 | .monaco-hover .monaco-hover-content .hover-row { 21 | order: 2; 22 | } 23 | .monaco-hover .monaco-hover-content .hover-row:has(.codicon-none) { 24 | order: 1; 25 | } 26 | .monaco-hover .monaco-hover-content .hover-row:has([style="color:#f96363;"]) { 27 | order: 1; 28 | } -------------------------------------------------------------------------------- /packages/formatter/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import path from "path"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | environment: "node", 7 | globals: true, 8 | include: ["test/**/*.vitest.{ts,tsx}", "test/**/*.spec.{ts,tsx}"], 9 | }, 10 | resolve: { 11 | alias: [ 12 | { 13 | // Map direct src imports of the sibling vscode-formatter package 14 | find: "@pretty-ts-errors/vscode-formatter/src", 15 | replacement: path.resolve(__dirname, "../vscode-formatter/src"), 16 | }, 17 | { 18 | // Map the package root to its src index for local testing 19 | find: "@pretty-ts-errors/vscode-formatter", 20 | replacement: path.resolve( 21 | __dirname, 22 | "../vscode-formatter/src/index.ts" 23 | ), 24 | }, 25 | ], 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import Mocha from "mocha"; 3 | import { glob } from "glob"; 4 | 5 | export function run( 6 | testsRoot: string, 7 | cb: (error: unknown | null, failures?: number) => void 8 | ): void { 9 | // Create the mocha test 10 | const mocha = new Mocha({ 11 | ui: "tdd", 12 | color: true, 13 | }); 14 | 15 | glob("**/**.test.js", { cwd: testsRoot }) 16 | .then((files) => { 17 | // Add files to the test suite 18 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 19 | 20 | try { 21 | // Run the mocha test 22 | mocha.run((failures) => { 23 | cb(null, failures); 24 | }); 25 | } catch (err) { 26 | console.error(err); 27 | cb(err); 28 | } 29 | }) 30 | .catch((err) => { 31 | return cb(err); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "@vscode/test-electron"; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ 17 | version: "1.77.0", 18 | extensionDevelopmentPath, 19 | extensionTestsPath, 20 | launchArgs: ["--disable-extensions"], 21 | }); 22 | } catch (err) { 23 | console.error("Failed to run tests"); 24 | process.exit(1); 25 | } 26 | } 27 | 28 | main(); 29 | -------------------------------------------------------------------------------- /packages/vscode-formatter/src/format/embedSymbolLinks.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic } from "vscode-languageserver-types"; 2 | 3 | export function embedSymbolLinks(diagnostic: Diagnostic): Diagnostic { 4 | if ( 5 | !diagnostic?.relatedInformation?.[0]?.message?.includes("is declared here") 6 | ) { 7 | return diagnostic; 8 | } 9 | const ref = diagnostic.relatedInformation[0]; 10 | const symbol = ref?.message.match(/(?'.*?') is declared here./) 11 | ?.groups?.["symbol"]; 12 | 13 | if (!symbol) { 14 | return diagnostic; 15 | } 16 | 17 | return { 18 | ...diagnostic, 19 | message: diagnostic.message.replaceAll( 20 | symbol, 21 | `${symbol}  ` 26 | ), 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/vscode-formatter/src/format/identSentences.ts: -------------------------------------------------------------------------------- 1 | import { d } from "@pretty-ts-errors/utils"; 2 | 3 | export const identSentences = (message: string): string => 4 | message 5 | .split("\n") 6 | .map((line) => { 7 | let whiteSpacesCount = line.search(/\S/); 8 | if (whiteSpacesCount === -1) { 9 | whiteSpacesCount = 0; 10 | } 11 | if (whiteSpacesCount === 0) { 12 | return line; 13 | } 14 | if (whiteSpacesCount >= 2) { 15 | whiteSpacesCount -= 2; 16 | } 17 | 18 | return d/*html*/ ` 19 | 20 |

21 | 22 | 23 | 24 | 29 | 30 | 31 |
25 | ${" ".repeat(3).repeat(whiteSpacesCount)} 26 | 27 |    28 | ${line}
32 | `; 33 | }) 34 | .join(""); 35 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "CommonJS", 5 | "moduleResolution": "Node", 6 | "lib": ["ES2022", "ES2021.String"], 7 | "baseUrl": ".", 8 | "strict": true, 9 | "noUncheckedIndexedAccess": true, 10 | "exactOptionalPropertyTypes": true, 11 | "noImplicitOverride": true, 12 | "noPropertyAccessFromIndexSignature": true, 13 | "useDefineForClassFields": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "skipLibCheck": true, 20 | "esModuleInterop": true, 21 | "resolveJsonModule": true, 22 | "sourceMap": true, 23 | "allowJs": true, 24 | "checkJs": true, 25 | "moduleDetection": "force", 26 | "declaration": true, 27 | "declarationMap": true, 28 | "composite": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/errors.ts: -------------------------------------------------------------------------------- 1 | interface Person { 2 | name: string; 3 | age: number; 4 | address: { 5 | street: string; 6 | city: string; 7 | country: string; 8 | }; 9 | } 10 | 11 | const john: Person = { 12 | name: "John Doe", 13 | age: 30, 14 | address: { 15 | street: "123 Main St", 16 | city: "New York", 17 | }, 18 | }; 19 | 20 | type GetUserFunction = () => { 21 | user: { 22 | name: string; 23 | email: `${string}@${string}.${string}`; 24 | age: number; 25 | }; 26 | }; 27 | 28 | const getPerson: GetUserFunction = () => ({ 29 | person: { 30 | username: "usr", 31 | email: "usr@usr.io", 32 | }, 33 | }); 34 | 35 | interface Animal { 36 | name: string; 37 | age: number; 38 | } 39 | 40 | function run(animal: T) { 41 | return animal; 42 | } 43 | 44 | run({ firstName: "John", weight: 20 }); 45 | 46 | type MyError = { 47 | code: number; 48 | }; 49 | 50 | try { 51 | // ... 52 | } catch (error: MyError) { 53 | console.log(error.code); 54 | } 55 | 56 | export {}; 57 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from "@eslint/js"; 4 | import tseslint from "typescript-eslint"; 5 | import globals from "globals"; 6 | 7 | export default tseslint.config( 8 | eslint.configs.recommended, 9 | ...tseslint.configs.strict, 10 | ...tseslint.configs.stylistic, 11 | { 12 | ignores: [ 13 | "apps/*/scripts/*", 14 | "apps/*/dist/*", 15 | "packages/*/scripts/*", 16 | "packages/*/dist/*", 17 | "examples/*", 18 | ".vscode-test/*", 19 | ], 20 | }, 21 | { 22 | languageOptions: { 23 | parser: tseslint.parser, 24 | parserOptions: { 25 | ecmaVersion: "latest", 26 | sourceType: "module", 27 | }, 28 | globals: { 29 | ...globals.node, 30 | }, 31 | }, 32 | plugins: { 33 | "@typescript-eslint": tseslint.plugin, 34 | }, 35 | rules: { 36 | "@typescript-eslint/no-unused-vars": "off", 37 | "@typescript-eslint/no-non-null-assertion": "off", 38 | curly: "warn", 39 | eqeqeq: "warn", 40 | "no-throw-literal": "warn", 41 | semi: "off", 42 | }, 43 | } 44 | ); 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yoav Balasiano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | import dedent from "ts-dedent"; 2 | 3 | export function objectKeys>( 4 | obj: T 5 | ): (keyof T & string)[] { 6 | return Object.keys(obj) as (keyof T & string)[]; 7 | } 8 | 9 | export function invert>( 10 | obj: T 11 | ): Partial<{ 12 | [K in T[keyof T]]: { [P in keyof T]: K extends T[P] ? P : never }[keyof T]; 13 | }> { 14 | const result: Partial<{ 15 | [K in T[keyof T]]: { [P in keyof T]: K extends T[P] ? P : never }[keyof T]; 16 | }> = {}; 17 | for (const key in obj) { 18 | const value = obj[key]; 19 | if (value !== undefined) { 20 | result[value] = key; 21 | } 22 | } 23 | return result; 24 | } 25 | 26 | /** 27 | * d stands for dedent. 28 | * it allow us to indent html in template literals without affecting the output 29 | */ 30 | export const d = dedent; 31 | 32 | /** 33 | * Check if an array contains a string. 34 | * Type guard the string if it does. 35 | */ 36 | export const has = ( 37 | array: unknown[], 38 | item: string 39 | ): item is Extract<(typeof array)[number], string> => array.includes(item); 40 | -------------------------------------------------------------------------------- /apps/vscode-extension/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yoav Balasiano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/vscode-formatter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pretty-ts-errors/vscode-formatter", 3 | "version": "0.1.0", 4 | "description": "Pretty TypeScript Errors Formatter for VSCode hovers", 5 | "files": [ 6 | "src/**", 7 | "dist/**" 8 | ], 9 | "main": "./dist/index.js", 10 | "types": "./dist/index.d.ts", 11 | "scripts": { 12 | "build": "tsdown src/index.ts --format cjs,esm --dts", 13 | "dev": "tsdown src/index.ts --format cjs,esm --dts --watch", 14 | "lint": "tsc -p . --noEmit", 15 | "publish": "npm run build && npm publish --access public" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/yoavbls/pretty-ts-errors.git" 20 | }, 21 | "keywords": [ 22 | "typescript", 23 | "errors" 24 | ], 25 | "author": "yoavbls", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/yoavbls/pretty-ts-errors/issues" 29 | }, 30 | "homepage": "https://github.com/yoavbls/pretty-ts-errors#readme", 31 | "devDependencies": { 32 | "tsdown": "^0.15.6" 33 | }, 34 | "dependencies": { 35 | "@pretty-ts-errors/formatter": "*", 36 | "@pretty-ts-errors/utils": "*", 37 | "lz-string": "^1.5.0", 38 | "vscode-languageserver-types": "^3.17.5" 39 | } 40 | } -------------------------------------------------------------------------------- /examples/errors.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {Object} Person 5 | * @property {string} name 6 | * @property {number} age 7 | * @property {Object} address 8 | * @property {string} address.street 9 | * @property {string} address.city 10 | * @property {string} address.country 11 | */ 12 | 13 | /** 14 | * @type {Person} 15 | */ 16 | const john = { 17 | name: "John Doe", 18 | age: 30, 19 | address: { 20 | street: "123 Main St", 21 | city: "New York", 22 | }, 23 | }; 24 | 25 | /** 26 | * @typedef {Function} GetUserFunction 27 | * @returns {{ user: { name: string, email: string, age: number } }} 28 | */ 29 | 30 | const getPerson = () => ({ 31 | person: { 32 | username: "usr", 33 | email: "usr@usr.io", 34 | }, 35 | }); 36 | 37 | 38 | /** 39 | * @typedef {Object} JSAnimal 40 | * @property {string} name 41 | * @property {number} age 42 | */ 43 | 44 | /** 45 | * @template {JSAnimal} T 46 | * @param {T} animal 47 | * @returns 48 | */ 49 | function run(animal) { 50 | return animal; 51 | } 52 | 53 | run({ firstName: "John", weight: 20 }); 54 | 55 | /** 56 | * @typedef {Object} MyError 57 | * @property {number} code 58 | */ 59 | 60 | try { 61 | // ... 62 | } catch (/** @type {MyError} */ error) { 63 | console.log(error.code); 64 | } 65 | 66 | export {} -------------------------------------------------------------------------------- /packages/formatter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pretty-ts-errors/formatter", 3 | "version": "0.1.8", 4 | "description": "Pretty TypeScript Errors Formatter", 5 | "files": [ 6 | "src/**", 7 | "dist/**" 8 | ], 9 | "main": "./dist/index.js", 10 | "types": "./dist/index.d.ts", 11 | "scripts": { 12 | "build": "tsdown src/index.ts --format cjs,esm --dts", 13 | "dev": "tsdown src/index.ts --format cjs,esm --dts --watch", 14 | "lint": "tsc -p . --noEmit", 15 | "test": "vitest run", 16 | "test:watch": "vitest", 17 | "publish": "npm run build && npm publish --access public" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/yoavbls/pretty-ts-errors.git" 22 | }, 23 | "keywords": [ 24 | "typescript", 25 | "errors" 26 | ], 27 | "author": "yoavbls", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/yoavbls/pretty-ts-errors/issues" 31 | }, 32 | "homepage": "https://github.com/yoavbls/pretty-ts-errors#readme", 33 | "dependencies": { 34 | "@pretty-ts-errors/utils": "*" 35 | }, 36 | "devDependencies": { 37 | "@types/prettier": "^2.7.3", 38 | "tsdown": "^0.15.6", 39 | "vitest": "^3.2.4" 40 | }, 41 | "peerDependencies": { 42 | "prettier": "^2.8.8" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/hide-original-errors.md: -------------------------------------------------------------------------------- 1 | To hide the original errors, display only the prettified ones, and make type blocks copyable, you can use the following hack: 2 | 3 | ## The Hack 4 | 5 | 1. Install the [Custom CSS and JS Loader](https://marketplace.visualstudio.com/items?itemName=be5invis.vscode-custom-css) extension from the VSCode marketplace. 6 | 7 | 2. Follow the installation instructions provided by the extension, and use [this CSS file](./pretty-ts-errors-hack.css). 8 | 9 | ## Why Do We Need This Hack? 10 | 11 | ### Hiding Original Errors 12 | 13 | Unfortunately, VSCode doesn't currently support formatted diagnostics. Once it does, we'll be able to convert the extension to a TypeScript LSP Plugin that replaces the original error with the prettified version. 14 | For updates on this feature, follow [this issue](https://github.com/yoavbls/pretty-ts-errors/issues/3). 15 | 16 | ### Making Type Blocks Copyable 17 | 18 | VSCode sanitizes and removes most CSS properties for security reasons. We've opened an [issue](https://github.com/microsoft/vscode/issues/180496) and submitted a [PR](https://github.com/microsoft/vscode/pull/180498) to allow the use of the `display` property. This would enable us to layout the types in a way that allows copying. 19 | 20 | Until this change is approved, you can use the hack described above as a workaround. 21 | -------------------------------------------------------------------------------- /packages/vscode-formatter/src/components/codeBlock.ts: -------------------------------------------------------------------------------- 1 | import { d } from "@pretty-ts-errors/utils"; 2 | import { miniLine } from "./miniLine"; 3 | import { spanBreak } from "./spanBreak"; 4 | 5 | /** 6 | * @returns markdown string that will be rendered as a code block (`supportHtml` required) 7 | * We're using codicon here since it's the only thing that can be `inline-block` 8 | * and have a background color in hovers due to strict sanitization of markdown on 9 | * VSCode [code](https://github.com/microsoft/vscode/blob/735aff6d962db49423e02c2344e60d418273ae39/src/vs/base/browser/markdownRenderer.ts#L372) 10 | */ 11 | const codeBlock = (code: string, language: string) => 12 | spanBreak(d/*html*/ ` 13 | 14 | 15 | \`\`\`${language} 16 | ${code} 17 | \`\`\` 18 | 19 | 20 | `); 21 | 22 | export const inlineCodeBlock = (code: string, language: string) => 23 | codeBlock(` ${code} `, language); 24 | 25 | export const multiLineCodeBlock = (code: string, language: string) => { 26 | const codeLines = code.split("\n"); 27 | //this line is finding the longest line 28 | const maxLineChars = codeLines.reduce( 29 | (acc, curr) => (curr.length > acc ? curr.length : acc), 30 | 0 31 | ); 32 | // codicon class align the code to the center, so we must pad it with spaces 33 | const paddedCode = codeLines 34 | .map((line) => line.padEnd(maxLineChars + 2)) 35 | .join("\n"); 36 | 37 | return d/*html*/ ` 38 | ${miniLine} 39 | ${codeBlock(paddedCode, language)} 40 | ${miniLine} 41 | `; 42 | }; 43 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch - apps/vscode-extension", 8 | "type": "shell", 9 | "command": "npm run watch --silent", 10 | "options": { 11 | "cwd": "${workspaceFolder}/apps/vscode-extension" 12 | }, 13 | "problemMatcher": { 14 | "owner": "esbuild", 15 | "fileLocation": "autoDetect", 16 | "pattern": [ 17 | { 18 | "regexp": "^✘ \\[ERROR\\] (.*)$", 19 | "message": 1 20 | }, 21 | { 22 | "regexp": "^\\s*(.*):(\\d+):(\\d+):$", 23 | "file": 1, 24 | "line": 2, 25 | "column": 3 26 | } 27 | ], 28 | "background": { 29 | "activeOnStart": true, 30 | "beginsPattern": "^\\[watch\\] build started$", 31 | "endsPattern": "^\\[watch\\] build finished$" 32 | } 33 | }, 34 | "isBackground": true, 35 | "presentation": { 36 | "reveal": "never", 37 | "group": "watchers" 38 | }, 39 | "group": { 40 | "kind": "build", 41 | "isDefault": true 42 | } 43 | }, 44 | { 45 | "type": "npm", 46 | "script": "watch-tests", 47 | "path": "apps/vscode-extension", 48 | "problemMatcher": "$tsc-watch", 49 | "isBackground": true, 50 | "presentation": { 51 | "reveal": "never", 52 | "group": "watchers" 53 | }, 54 | "group": "build" 55 | }, 56 | { 57 | "label": "tasks: watch-tests", 58 | "dependsOn": ["watch - apps/vscode-extension", "npm: watch-tests"], 59 | "problemMatcher": [] 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /packages/vscode-formatter/src/components/title.ts: -------------------------------------------------------------------------------- 1 | import { compressToEncodedURIComponent } from "lz-string"; 2 | import { Diagnostic } from "vscode-languageserver-types"; 3 | import { d } from "@pretty-ts-errors/utils"; 4 | import { miniLine } from "./miniLine"; 5 | 6 | export const title = (diagnostic: Diagnostic) => d/*html*/ ` 7 | ⚠ Error ${ 8 | typeof diagnostic.code === "number" 9 | ? d/*html*/ ` 10 | 11 | (TS${diagnostic.code}) 12 | ${errorCodeExplanationLink(diagnostic.code)} | 13 | ${errorMessageTranslationLink(diagnostic.message)} | 14 | ${copyErrorLink(diagnostic.message)} 15 | 16 | ` 17 | : "" 18 | } 19 |
20 | ${miniLine} 21 | `; 22 | 23 | const errorCodeExplanationLink = (errorCode: Diagnostic["code"]) => 24 | d/*html*/ ` 25 | 26 | 27 | 28 | `; 29 | 30 | const errorMessageTranslationLink = (message: Diagnostic["message"]) => { 31 | const encodedMessage = compressToEncodedURIComponent(message); 32 | 33 | return d/*html*/ ` 34 | 35 | 36 | 37 | `; 38 | }; 39 | 40 | const copyErrorLink = (message: Diagnostic["message"]) => { 41 | const args = encodeURIComponent(JSON.stringify(message)); 42 | return d/*html*/ ` 43 | 44 | 45 | 46 | `; 47 | }; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}/apps/vscode-extension" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/apps/vscode-extension/dist/**/*.js", 17 | "${workspaceFolder}/apps/vscode-extension/out/**/*.js" 18 | ], 19 | "preLaunchTask": "watch - apps/vscode-extension" 20 | }, 21 | { 22 | "name": "Run Extension (all extensions disabled)", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "args": [ 26 | "--disable-extensions", 27 | "--extensionDevelopmentPath=${workspaceFolder}/apps/vscode-extension" 28 | ], 29 | "outFiles": [ 30 | "${workspaceFolder}/apps/vscode-extension/dist/**/*.js", 31 | "${workspaceFolder}/apps/vscode-extension/out/**/*.js" 32 | ], 33 | "preLaunchTask": "watch - apps/vscode-extension" 34 | }, 35 | { 36 | "name": "Extension Tests", 37 | "type": "extensionHost", 38 | "request": "launch", 39 | "args": [ 40 | "--extensionDevelopmentPath=${workspaceFolder}/apps/vscode-extension", 41 | "--extensionTestsPath=${workspaceFolder}/apps/vscode-extension/out/test/suite/index" 42 | ], 43 | "outFiles": [ 44 | "${workspaceFolder}/apps/vscode-extension/out/**/*.js", 45 | "${workspaceFolder}/apps/vscode-extension/dist/**/*.js" 46 | ], 47 | "preLaunchTask": "tasks: watch-tests" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /packages/formatter/src/addMissingParentheses.ts: -------------------------------------------------------------------------------- 1 | import { has, invert, objectKeys } from "@pretty-ts-errors/utils"; 2 | 3 | const parentheses = { 4 | "(": ")", 5 | "{": "}", 6 | "[": "]", 7 | } as const; 8 | 9 | const openParentheses = objectKeys(parentheses); 10 | const closeParentheses = Object.values(parentheses); 11 | 12 | export function addMissingParentheses(type: string): string { 13 | const openStack: (typeof openParentheses)[number][] = []; 14 | const missingClosingChars: string[] = []; 15 | 16 | for (const char of type) { 17 | if (has(openParentheses, char)) { 18 | openStack.push(char); 19 | } else if (has(closeParentheses, char)) { 20 | const lastOpen = openStack[openStack.length - 1]; 21 | if (lastOpen === undefined || parentheses[lastOpen] !== char) { 22 | // Add the correct opening character before the current closing character 23 | openStack.push(invert(parentheses)[char]); 24 | } else { 25 | openStack.pop(); 26 | } 27 | } 28 | } 29 | 30 | // Add the missing closing characters at the end of the string 31 | while (openStack.length > 0) { 32 | const openChar = openStack.pop()!; 33 | const closingChar = parentheses[openChar]; 34 | missingClosingChars.push(closingChar); 35 | } 36 | 37 | let validType = type; 38 | 39 | // Close the last string if it's not closed 40 | const quoteMatches = validType.match(/['"]/g); 41 | if (quoteMatches) { 42 | const lastQuote = quoteMatches[quoteMatches.length - 1]; 43 | if (quoteMatches.length % 2 === 1) { 44 | validType += `...${lastQuote}`; 45 | } 46 | } 47 | 48 | if (validType.endsWith(":")) { 49 | validType += "..."; 50 | } 51 | 52 | validType = (validType + "\n..." + missingClosingChars.join("")).replace( 53 | // Change (param: ...) to (param) => __RETURN_TYPE__ if needed 54 | /(\([a-zA-Z0-9]*:.*\))/, 55 | (p1) => `${p1} => ...` 56 | ); 57 | 58 | return validType; 59 | } 60 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/provider/selectedTextHoverProvider.ts: -------------------------------------------------------------------------------- 1 | import { d } from "@pretty-ts-errors/utils"; 2 | import { formatDiagnostic } from "@pretty-ts-errors/vscode-formatter"; 3 | import { 4 | ExtensionContext, 5 | ExtensionMode, 6 | MarkdownString, 7 | languages, 8 | window, 9 | } from "vscode"; 10 | import { createConverter } from "vscode-languageclient/lib/common/codeConverter"; 11 | 12 | /** 13 | * Register an hover provider in debug only. 14 | * It format selected text and help test things visually easier. 15 | */ 16 | export function registerSelectedTextHoverProvider(context: ExtensionContext) { 17 | const converter = createConverter(); 18 | 19 | if (context.extensionMode !== ExtensionMode.Development) { 20 | return; 21 | } 22 | 23 | context.subscriptions.push( 24 | languages.registerHoverProvider( 25 | { 26 | language: "typescript", 27 | pattern: "**/test/**/*.ts", 28 | }, 29 | { 30 | provideHover(document, position) { 31 | const editor = window.activeTextEditor; 32 | const range = document.getWordRangeAtPosition(position); 33 | const message = editor ? document.getText(editor.selection) : ""; 34 | 35 | if (!range || !message) { 36 | return null; 37 | } 38 | 39 | const markdown = new MarkdownString( 40 | debugHoverHeader + 41 | formatDiagnostic( 42 | converter.asDiagnostic({ 43 | message, 44 | range, 45 | severity: 0, 46 | source: "ts", 47 | code: 1337, 48 | }) 49 | ) 50 | ); 51 | 52 | markdown.isTrusted = true; 53 | markdown.supportHtml = true; 54 | 55 | return { 56 | contents: [markdown], 57 | }; 58 | }, 59 | } 60 | ) 61 | ); 62 | } 63 | 64 | const debugHoverHeader = d/*html*/ ` 65 | 66 | 67 | Formatted selected text (debug only) 68 | 69 |
70 |
71 |

72 | `; 73 | -------------------------------------------------------------------------------- /packages/formatter/src/formatTypeBlock.ts: -------------------------------------------------------------------------------- 1 | import { addMissingParentheses } from "./addMissingParentheses"; 2 | import { prettify } from "./prettify"; 3 | 4 | export function formatTypeBlock( 5 | prefix: string, 6 | type: string, 7 | codeBlock: (code: string, language?: string, multiLine?: boolean) => string 8 | ) { 9 | // Return a simple code block if it's just a parenthesis 10 | if (type.match(/^(\[\]|\{\})$/)) { 11 | return `${prefix} ${codeBlock(type)}`; 12 | } 13 | 14 | if ( 15 | // Skip formatting if it's a simple type 16 | type.match( 17 | /^((void|null|undefined|any|number|string|bigint|symbol|readonly|typeof)(\[\])?)$/ 18 | ) 19 | ) { 20 | return `${prefix} ${codeBlock(type, "type")}`; 21 | } 22 | 23 | const prettyType = prettifyType(type); 24 | 25 | if (prettyType.includes("\n")) { 26 | return `${prefix}: ${codeBlock(prettyType, "type", true)}`; 27 | } else { 28 | return `${prefix} ${codeBlock(prettyType, "type")}`; 29 | } 30 | } 31 | /** 32 | * Try to make type prettier with prettier 33 | */ 34 | export function prettifyType( 35 | type: string, 36 | options?: { throwOnError?: boolean } 37 | ) { 38 | try { 39 | // Wrap type with valid statement, format it and extract the type back 40 | return convertToOriginalType(prettify(convertToValidType(type))); 41 | } catch (e) { 42 | if (options?.throwOnError) { 43 | throw e; 44 | } 45 | return type; 46 | } 47 | } 48 | 49 | const convertToValidType = (type: string) => 50 | `type x = ${type 51 | // Add missing parentheses when the type ends with "..."" 52 | .replace(/(.*)\.\.\.$/, (_, p1) => addMissingParentheses(p1)) 53 | // Replace single parameter function destructuring because it's not a valid type 54 | // .replaceAll(/\((\{.*\})\:/g, (_, p1) => `(param: /* ${p1} */`) 55 | // Change `(...): return` which is invalid to `(...) => return` 56 | .replace(/^(\(.*\)): /, (_, p1) => `${p1} =>`) 57 | .replaceAll(/... (\d{0,}) more .../g, (_, p1) => `___${p1}MORE___`) 58 | .replaceAll(/... (\d{0,}) more ...;/g, (_, p1) => `___MORE___: ${p1};`) 59 | .replaceAll("...;", "___KEY___: ___THREE_DOTS___;") 60 | .replaceAll("...", "__THREE_DOTS__")};`; 61 | 62 | const convertToOriginalType = (type: string) => 63 | type 64 | .replaceAll("___KEY___: ___THREE_DOTS___", "...;") 65 | .replaceAll("__THREE_DOTS__", "...") 66 | .replaceAll(/___MORE___: (\d{0,});/g, (_, p1) => `... ${p1} more ...;`) 67 | .replaceAll(/___(\d{0,})MORE___/g, (_, p1) => `... ${p1} more ...`) 68 | .replaceAll(/... (\d{0,}) more .../g, (_, p1) => `/* ${p1} more */`) // ... x more ... not shown sell 69 | // .replaceAll(/\(param\: \/\* (\{ .* \}) \*\//g, (_, p1) => `(${p1}: `) 70 | .replace(/type x =[ ]?((.|\n)*);.*/g, "$1") 71 | .trim(); 72 | -------------------------------------------------------------------------------- /packages/formatter/test/formatter.vitest.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { formatDiagnosticMessage } from "@pretty-ts-errors/formatter"; 3 | import { addMissingParentheses } from "@pretty-ts-errors/formatter/src/addMissingParentheses"; 4 | import { prettifyType } from "@pretty-ts-errors/formatter/src/formatTypeBlock"; 5 | import { 6 | inlineCodeBlock, 7 | multiLineCodeBlock, 8 | } from "@pretty-ts-errors/vscode-formatter/src/components"; 9 | import { d } from "@pretty-ts-errors/utils"; 10 | import { 11 | errorWithDashInObjectKeys, 12 | errorWithSpecialCharsInObjectKeys, 13 | } from "./errorMessageMocks"; 14 | 15 | describe("formatter", () => { 16 | it("adds missing parentheses", () => { 17 | expect(addMissingParentheses("Hello, {world! [This] is a (test.")).toBe( 18 | "Hello, {world! [This] is a (test.\n...)}" 19 | ); 20 | }); 21 | 22 | // Provide a codeBlock fn similar to the extension's renderer 23 | const htmlCodeBlock = ( 24 | code: string, 25 | language?: string, 26 | multiLine?: boolean 27 | ) => 28 | multiLine 29 | ? multiLineCodeBlock(code, language || "") 30 | : inlineCodeBlock(code, language || ""); 31 | 32 | it("formats Special characters in object keys", () => { 33 | expect( 34 | formatDiagnosticMessage(errorWithSpecialCharsInObjectKeys, htmlCodeBlock) 35 | ).toBe( 36 | "Type " + 37 | inlineCodeBlock("string", "type") + 38 | " is not assignable to type " + 39 | inlineCodeBlock(`{ "abc*bc": string }`, "type") + 40 | "." 41 | ); 42 | }); 43 | 44 | it("formats method's word in the error", () => { 45 | expect( 46 | formatDiagnosticMessage(errorWithDashInObjectKeys, htmlCodeBlock) 47 | ).toBe( 48 | "Type " + 49 | inlineCodeBlock(`{ person: { "first-name": string } }`, "type") + 50 | " is not assignable to type " + 51 | inlineCodeBlock("string", "type") + 52 | "." 53 | ); 54 | }); 55 | 56 | it("prettifies type with params destructuring", () => { 57 | expect(() => 58 | prettifyType( 59 | d` { $ref: null; ref: (ref: any) => any; columns: ({ label: string; prop: string; } | { label: string; formatter: ({ ip_type }: any) => any; } | { actions: { label: string; disabled: ({ contract_id }: any) => boolean; handler({ contract_id }: any): void; }[]; })[]; ... 4 more ...; load(): Promise<...>; } 60 | `, 61 | { throwOnError: true } 62 | ) 63 | ).not.toThrow(); 64 | }); 65 | 66 | it("prettifies truncated type", () => { 67 | expect(() => 68 | prettifyType( 69 | d` { b: { name: string; icon: undefined; }; c: { name: string; icon: undefined; }; d: { name: string; icon: undefined; }; e: { name: string; icon: undefined; }; f: { ...; }; g: { ...; }; h:...`, 70 | { throwOnError: true } 71 | ) 72 | ).not.toThrow(); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /apps/vscode-extension/scripts/build.js: -------------------------------------------------------------------------------- 1 | const process = require("node:process"); 2 | const console = require("node:console"); 3 | const production = process.argv.includes("--production"); 4 | const watch = process.argv.includes("--watch"); 5 | 6 | /** 7 | * @see https://code.visualstudio.com/api/working-with-extensions/bundling-extension#using-esbuild 8 | */ 9 | async function main() { 10 | const ctx = await require("esbuild").context({ 11 | entryPoints: { 12 | extension: "./src/extension.ts", 13 | }, 14 | bundle: true, 15 | outdir: "./dist", 16 | external: ["vscode"], 17 | format: "cjs", 18 | inject: ["./scripts/process-shim.js"], 19 | tsconfig: "./tsconfig.json", 20 | define: production ? { "process.env.NODE_ENV": '"production"' } : undefined, 21 | minify: production, 22 | sourcemap: !production, 23 | plugins: [workspacePackagesPlugin, esbuildProblemMatcherPlugin], 24 | }); 25 | if (watch) { 26 | await ctx.watch(); 27 | } else { 28 | await ctx.rebuild(); 29 | await ctx.dispose(); 30 | } 31 | } 32 | 33 | /** 34 | * @type {import('esbuild').Plugin} 35 | */ 36 | const esbuildProblemMatcherPlugin = { 37 | name: "esbuild-problem-matcher", 38 | setup(build) { 39 | build.onStart(() => { 40 | console.log("[watch] build started"); 41 | }); 42 | build.onEnd((result) => { 43 | result.errors.forEach(({ text, location }) => { 44 | console.error(`✘ [ERROR] ${text}`); 45 | console.error( 46 | ` ${location.file}:${location.line}:${location.column}:` 47 | ); 48 | }); 49 | console.log("[watch] build finished"); 50 | }); 51 | }, 52 | }; 53 | 54 | /** 55 | * Resolve internal workspace packages to their source files so we bundle them in watch/debug. 56 | * This makes changes in packages/* reflected immediately without separate watchers. 57 | * @type {import('esbuild').Plugin} 58 | */ 59 | const workspacePackagesPlugin = { 60 | name: "workspace-packages", 61 | setup(build) { 62 | const path = require("node:path"); 63 | const pkgRoot = path.resolve(__dirname, "../../../packages"); 64 | /** @type {Record} */ 65 | const alias = { 66 | "@pretty-ts-errors/utils": path.join(pkgRoot, "utils/src/index.ts"), 67 | "@pretty-ts-errors/formatter": path.join( 68 | pkgRoot, 69 | "formatter/src/index.ts" 70 | ), 71 | "@pretty-ts-errors/vscode-formatter": path.join( 72 | pkgRoot, 73 | "vscode-formatter/src/index.ts" 74 | ), 75 | }; 76 | build.onResolve( 77 | { filter: /^@pretty-ts-errors\/(utils|formatter|vscode-formatter)$/ }, 78 | (args) => { 79 | const target = alias[args.path]; 80 | return target ? { path: target } : undefined; 81 | } 82 | ); 83 | }, 84 | }; 85 | 86 | main().catch((e) => { 87 | console.error(e); 88 | process.exit(1); 89 | }); 90 | -------------------------------------------------------------------------------- /apps/vscode-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pretty-ts-errors", 3 | "displayName": "Pretty TypeScript Errors", 4 | "publisher": "YoavBls", 5 | "description": "Make TypeScript errors prettier and more human-readable in VSCode", 6 | "version": "0.6.3", 7 | "icon": "assets/icon.png", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/yoavbls/pretty-ts-errors", 11 | "directory": "apps/vscode-extension" 12 | }, 13 | "homepage": "https://github.com/yoavbls/pretty-ts-errors", 14 | "engines": { 15 | "vscode": "^1.77.0" 16 | }, 17 | "vsce": { 18 | "dependencies": false 19 | }, 20 | "categories": [ 21 | "Programming Languages", 22 | "Debuggers", 23 | "Visualization", 24 | "Other" 25 | ], 26 | "galleryBanner": { 27 | "color": "#133b55", 28 | "theme": "dark" 29 | }, 30 | "activationEvents": [ 31 | "onLanguage:typescript", 32 | "onLanguage:javascript", 33 | "onLanguage:typescriptreact", 34 | "onLanguage:javascriptreact", 35 | "onLanguage:astro", 36 | "onLanguage:svelte", 37 | "onLanguage:vue", 38 | "onLanguage:mdx", 39 | "onLanguage:glimmer-js", 40 | "onLanguage:glimmer-ts" 41 | ], 42 | "main": "./dist/extension.js", 43 | "browser": "./dist/extension.js", 44 | "files": [ 45 | "dist/**/*", 46 | "assets/**/*", 47 | "syntaxes/**/*", 48 | "LICENSE" 49 | ], 50 | "contributes": { 51 | "languages": [ 52 | { 53 | "id": "type", 54 | "extensions": [ 55 | ".type" 56 | ] 57 | } 58 | ], 59 | "grammars": [ 60 | { 61 | "language": "type", 62 | "scopeName": "source.type", 63 | "path": "./syntaxes/type.tmGrammar.json" 64 | } 65 | ] 66 | }, 67 | "scripts": { 68 | "vscode:prepublish": "cp ../../README.md README.md && npm run package", 69 | "compile": "node scripts/build", 70 | "watch": "npm run compile -- --watch", 71 | "dev": "npm-run-all --parallel _dev:*", 72 | "_dev:ext": "npm run watch", 73 | "_dev:formatter": "npm run -w @pretty-ts-errors/formatter dev", 74 | "_dev:vscode-formatter": "npm run -w @pretty-ts-errors/vscode-formatter dev", 75 | "build": "vsce package", 76 | "package": "node scripts/build -- --production", 77 | "compile-tests": "tsc -p . --outDir out", 78 | "watch-tests": "tsc -p . -w --outDir out", 79 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 80 | "lint": "tsc -p . --noEmit", 81 | "test": "node ./out/test/runTest.js" 82 | }, 83 | "devDependencies": { 84 | "@types/mocha": "^10.0.10", 85 | "@types/node": "^16.11.68", 86 | "@types/vscode": "^1.77.0", 87 | "@vscode/test-electron": "^2.5.2", 88 | "esbuild": "^0.25.11", 89 | "glob": "^11.0.3", 90 | "mocha": "^11.7.4", 91 | "npm-run-all": "^4.1.5" 92 | }, 93 | "dependencies": { 94 | "@pretty-ts-errors/formatter": "*", 95 | "@pretty-ts-errors/vscode-formatter": "*", 96 | "vscode-languageclient": "^9.0.1" 97 | } 98 | } -------------------------------------------------------------------------------- /apps/vscode-extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import { has } from "@pretty-ts-errors/utils"; 2 | import { formatDiagnostic } from "@pretty-ts-errors/vscode-formatter"; 3 | import { 4 | commands, 5 | env, 6 | ExtensionContext, 7 | languages, 8 | MarkdownString, 9 | Range, 10 | window, 11 | } from "vscode"; 12 | import { createConverter } from "vscode-languageclient/lib/common/codeConverter"; 13 | import { hoverProvider } from "./provider/hoverProvider"; 14 | import { registerSelectedTextHoverProvider } from "./provider/selectedTextHoverProvider"; 15 | import { uriStore } from "./provider/uriStore"; 16 | 17 | const cache = new Map(); 18 | 19 | export function activate(context: ExtensionContext) { 20 | const registeredLanguages = new Set(); 21 | const converter = createConverter(); 22 | 23 | // register the copy command 24 | context.subscriptions.push( 25 | commands.registerCommand( 26 | "prettyTsErrors.copyError", 27 | async (errorMessage: string) => { 28 | await env.clipboard.writeText(errorMessage); 29 | } 30 | ) 31 | ); 32 | 33 | registerSelectedTextHoverProvider(context); 34 | 35 | context.subscriptions.push( 36 | languages.onDidChangeDiagnostics(async (e) => { 37 | e.uris.forEach((uri) => { 38 | const diagnostics = languages.getDiagnostics(uri); 39 | 40 | const items: { 41 | range: Range; 42 | contents: MarkdownString[]; 43 | }[] = []; 44 | 45 | let hasTsDiagnostic = false; 46 | 47 | diagnostics 48 | .filter((diagnostic) => 49 | diagnostic.source 50 | ? has( 51 | ["ts", "ts-plugin", "deno-ts", "js", "glint"], 52 | diagnostic.source 53 | ) 54 | : false 55 | ) 56 | .forEach(async (diagnostic) => { 57 | // formatDiagnostic converts message based on LSP Diagnostic type, not VSCode Diagnostic type, so it can be used in other IDEs. 58 | // Here we convert VSCode Diagnostic to LSP Diagnostic to make formatDiagnostic recognize it. 59 | let formattedMessage = cache.get(diagnostic.message); 60 | 61 | if (!formattedMessage) { 62 | const markdownString = new MarkdownString( 63 | formatDiagnostic(converter.asDiagnostic(diagnostic)) 64 | ); 65 | 66 | markdownString.isTrusted = true; 67 | markdownString.supportHtml = true; 68 | 69 | formattedMessage = markdownString; 70 | cache.set(diagnostic.message, formattedMessage); 71 | 72 | if (cache.size > 100) { 73 | const firstCacheKey = cache.keys().next().value; 74 | cache.delete(firstCacheKey); 75 | } 76 | } 77 | 78 | items.push({ 79 | range: diagnostic.range, 80 | contents: [formattedMessage], 81 | }); 82 | 83 | hasTsDiagnostic = true; 84 | }); 85 | 86 | uriStore[uri.fsPath] = items; 87 | 88 | if (hasTsDiagnostic) { 89 | const editor = window.visibleTextEditors.find( 90 | (editor) => editor.document.uri.toString() === uri.toString() 91 | ); 92 | if (editor && !registeredLanguages.has(editor.document.languageId)) { 93 | registeredLanguages.add(editor.document.languageId); 94 | context.subscriptions.push( 95 | languages.registerHoverProvider( 96 | { 97 | language: editor.document.languageId, 98 | }, 99 | hoverProvider 100 | ) 101 | ); 102 | } 103 | } 104 | }); 105 | }) 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /packages/formatter/src/formatDiagnosticMessage.ts: -------------------------------------------------------------------------------- 1 | import { formatTypeBlock } from "./formatTypeBlock"; 2 | 3 | export const formatDiagnosticMessage = ( 4 | message: string, 5 | codeBlock: (code: string, language?: string, multiLine?: boolean) => string 6 | ) => { 7 | const formatTypeScriptBlock = (_: string, code: string) => 8 | codeBlock(code, "typescript"); 9 | 10 | const formatSimpleTypeBlock = (_: string, code: string) => 11 | codeBlock(code, "type"); 12 | 13 | const formatTypeOrModuleBlock = (_: string, prefix: string, code: string) => 14 | formatTypeBlock( 15 | prefix, 16 | ["module", "file", "file name"].includes(prefix.toLowerCase()) 17 | ? `"${code}"` 18 | : code, 19 | codeBlock 20 | ); 21 | 22 | return ( 23 | message 24 | // format strings wrapped like '"..."' (double quotes inside single quotes) 25 | .replaceAll( 26 | /(?:\s)'"(.*?)(? formatTypeBlock("", `"${p1}"`, codeBlock) 28 | ) 29 | // format declare module snippet 30 | .replaceAll( 31 | /['“](declare module )['”](.*)['“];['”]/g, 32 | (_: string, p1: string, p2: string) => 33 | formatTypeScriptBlock(_, `${p1} "${p2}"`) 34 | ) 35 | // format missing props error 36 | .replaceAll( 37 | /(is missing the following properties from type\s?)'(.*)': ((?:#?\w+, )*(?:(?!and)\w+)?)/g, 38 | (_, pre, type, post) => 39 | `${pre}${formatTypeBlock("", type, codeBlock)}:
    ${post 40 | .split(", ") 41 | .filter(Boolean) 42 | .map((prop: string) => `
  • ${prop}
  • `) 43 | .join("")}
` 44 | ) 45 | // Format type pairs 46 | .replaceAll( 47 | /(types) ['“](.*?)['”] and ['“](.*?)['”][.]?/gi, 48 | (_: string, p1: string, p2: string, p3: string) => 49 | `${formatTypeBlock(p1, p2, codeBlock)} and ${formatTypeBlock( 50 | "", 51 | p3, 52 | codeBlock 53 | )}` 54 | ) 55 | // Format type annotation options 56 | .replaceAll( 57 | /type annotation must be ['“](.*?)['”] or ['“](.*?)['”][.]?/gi, 58 | (_: string, p1: string, p2: string, p3: string | number) => { 59 | if (typeof p3 === "string") { 60 | return `${formatTypeBlock(p1, p2, codeBlock)} or ${formatTypeBlock( 61 | "", 62 | p3, 63 | codeBlock 64 | )}`; 65 | } else { 66 | // If p3 is a number, it is matching a ts(1196) error, see #121 67 | return `${formatTypeBlock("", p1, codeBlock)} or ${formatTypeBlock( 68 | "", 69 | p2, 70 | codeBlock 71 | )}`; 72 | } 73 | } 74 | ) 75 | .replaceAll( 76 | /(Overload \d of \d), ['“](.*?)['”], /gi, 77 | (_, p1: string, p2: string) => 78 | `${p1}${formatTypeBlock("", p2, codeBlock)}` 79 | ) 80 | // format simple strings 81 | .replaceAll(/^['“]"[^"]*"['”]$/g, formatTypeScriptBlock) 82 | // Replace module 'x' by module "x" for ts error #2307 83 | .replaceAll( 84 | /(module )'([^"]*?)'/gi, 85 | (_, p1: string, p2: string) => `${p1}"${p2}"` 86 | ) 87 | // Format string types 88 | .replaceAll( 89 | /(module|file|file name|imported via) ['"“](.*?)['"“](?=[\s(.|,]|$)/gi, 90 | (_, p1: string, p2: string) => formatTypeBlock(p1, `"${p2}"`, codeBlock) 91 | ) 92 | // Format types 93 | .replaceAll( 94 | /(type|type alias|interface|module|file|file name|class|method's|subtype of constraint) ['“](.*?)['“](?=[\s(.|,)]|$)/gi, 95 | (_, p1: string, p2: string) => formatTypeOrModuleBlock(_, p1, p2) 96 | ) 97 | // Format reversed types 98 | .replaceAll( 99 | /(.*)['“]([^>]*)['”] (type|interface|return type|file|module|is (not )?assignable)/gi, 100 | (_: string, p1: string, p2: string, p3: string) => 101 | `${p1}${formatTypeOrModuleBlock(_, "", p2)} ${p3}` 102 | ) 103 | // Format simple types that didn't captured before 104 | .replaceAll( 105 | /['“]((void|null|undefined|any|boolean|string|number|bigint|symbol)(\[\])?)['”]/g, 106 | formatSimpleTypeBlock 107 | ) 108 | // Format some typescript key words 109 | .replaceAll( 110 | /['“](import|export|require|in|continue|break|let|false|true|const|new|throw|await|for await|[0-9]+)( ?.*?)['”]/g, 111 | (_: string, p1: string, p2: string) => 112 | formatTypeScriptBlock(_, `${p1}${p2}`) 113 | ) 114 | // Format return values 115 | .replaceAll( 116 | /(return|operator) ['“](.*?)['”]/gi, 117 | (_, p1: string, p2: string) => `${p1} ${formatTypeScriptBlock("", p2)}` 118 | ) 119 | // Format regular code blocks 120 | .replaceAll( 121 | /(? ` ${codeBlock(p1)} ` 123 | ) 124 | ); 125 | }; 126 | -------------------------------------------------------------------------------- /packages/formatter/test/errorMessageMocks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains mocks of error messages, only some of them 3 | * are used in tests but all of them can be used to test and debug 4 | * the formatting visually, you can try to select them on debug and check the hover. 5 | */ 6 | 7 | import { d } from "@pretty-ts-errors/utils"; 8 | 9 | export const errorWithSpecialCharsInObjectKeys = d` 10 | Type 'string' is not assignable to type '{ 'abc*bc': string; }'. 11 | `; 12 | 13 | export const errorWithDashInObjectKeys = d` 14 | Type '{ person: { 'first-name': string; }; }' is not assignable to type 'string'. 15 | `; 16 | 17 | /** 18 | * Formatting error from this issue: https://github.com/yoavbls/pretty-ts-errors/issues/20 19 | */ 20 | export const errorWithMethodsWordInIt = d` 21 | The 'this' context of type 'ElementHandle' is not assignable to method's 'this' of type 'ElementHandle'. 22 | Type 'Node' is missing the following properties from type 'Element': attributes, classList, className, clientHeight, and 114 more. 23 | `; 24 | 25 | export const errorWithParamsDestructuring = d` 26 | Argument of type '{ $ref: null; ref: (ref: any) => any; columns: ({ label: string; prop: string; } | { label: string; formatter: ({ ip_type }: any) => any; } | { actions: { label: string; disabled: ({ contract_id }: any) => boolean; handler({ contract_id }: any): void; }[]; })[]; ... 4 more ...; load(): Promise<...>; }' is not assignable to parameter of type 'VTableConfig'. 27 | Property 'data' is missing in type '{ $ref: null; ref: (ref: any) => any; columns: ({ label: string; prop: string; } | { label: string; formatter: ({ ip_type }: any) => any; } | { actions: { label: string; disabled: ({ contract_id }: any) => boolean; handler({ contract_id }: any): void; }[]; })[]; ... 4 more ...; load(): Promise<...>; }' but required in type 'VTableConfig'. 28 | `; 29 | 30 | export const errorWithLongType = d` 31 | Property 'isFlying' is missing in type '{ animal: { __typename?: "Animal" | undefined; id: string; name: string; age: number; isAlived: boolean; ... 8 more ...; attributes: { ...; } | ... 3 more ... | { ...; }; }; }' but required in type '{ animal: { __typename?: "Animal" | undefined; id: string; name: string; age: number; isAlived: boolean; isFlying: boolean; ... 8 more ...; attributes: { ...; } | ... 3 more ... | { ...; }; }; }'. 32 | `; 33 | 34 | export const errorWithTruncatedType2 = d` 35 | Type '{ '!top': string[]; 'xsl:declaration': { attrs: { 'default-collation': null; 'exclude-result-prefixes': null; 'extension-element-prefixes': null; 'use-when': null; 'xpath-default-namespace': null; }; }; 'xsl:instruction': { ...; }; ... 49 more ...; 'xsl:literal-result-element': {}; }' is missing the following properties from type 'GraphQLSchema': description, extensions, astNode, extensionASTNodes, and 21 more. 36 | `; 37 | 38 | export const variableNotUsedEror = d` 39 | 'a' is declared but its value is never read. 40 | `; 41 | 42 | export const errorWithSimpleIndentations = d` 43 | Type '(newIds: number[]) => void' is not assignable to type '(selectedId: string[]) => void'. 44 | Types of parameters 'newIds' and 'selectedId' are incompatible. 45 | Type 'string[]' is not assignable to type 'number[]'. 46 | Type 'string' is not assignable to type 'number'. 47 | `; 48 | 49 | export const errorWithComma = d` 50 | Argument of type '{ filters: Filters; } & T' is not assignable to parameter of type 'T & F'. 51 | Type '{ filters: Filters; } & T' is not assignable to type 'F'. 52 | '{ filters: Filters; } & T' is assignable to the constraint of type 'F', but 'F' could be instantiated with a different subtype of constraint '{ filters: Filters; }'. 53 | `; 54 | 55 | export const missingPropertyError = 56 | "\ 57 | Property 'user' is missing in type '{ person: { username: string; email: string; }; }' but required in type '{ user: { name: string; email: `${string}@${string}.${string}`; age: number; }; }'.\ 58 | "; 59 | 60 | export const missingReactPropsError = d` 61 | Type '{ style: { backgroundColor: string; }; }' is not assignable to type 'DropDownPickerProps'. 62 | Type '{ style: { backgroundColor: string; }; }' is not assignable to type 'DropDownPickerMultipleProps & DropDownPickerBaseProps': multiple, setValue, value 64 | `; 65 | 66 | export const leftSideAritmeticError = d` 67 | The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type. 68 | `; 69 | 70 | export const theosError = d` 71 | Exported variable 'uploadRouter' has or is using name 'Uploader' from external module "/Users/theo/Code/Work/filething/packages/uploadthing/dist/types-dbaf1b46" but cannot be named. 72 | `; 73 | 74 | export const ts1378Error = d` 75 | Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', or 'nodenext', and the 'target' option is set to 'es2017' or higher. 76 | `; 77 | 78 | export const ts2304Error = d` 79 | Cannot find name 'varname'. 80 | `; 81 | 82 | export const ts2305Error = d` 83 | Module '"@pretty-ts-errors/formatter"' has no exported member 'values'. 84 | `; 85 | 86 | export const ts2307Error = d` 87 | Cannot find module 'events' or its corresponding type declarations. 88 | `; 89 | 90 | export const ts1360Error = d` 91 | Property 'a' is missing in type '{ b: { name: string; icon: undefined; }; c: { name: string; icon: undefined; }; d: { name: string; icon: undefined; }; e: { name: string; icon: undefined; }; f: { ...; }; g: { ...; }; h:...' but required in type '{a: {name: string; icon: undefined}}'. 92 | `; 93 | 94 | export const errorWithStringChars = d` 95 | Type '"' 'Oh no"' is not assignable to type '"' 'Oh n\"o\"' "'. 96 | `; 97 | 98 | export const ts2322ErrorWithPrivateProperty = d` 99 | Type 'Ref<{ name: string; readonly type: "json"; mm: (px: T) => T; px: (mm: T) => T; ... 18 more ...; toJson: () => string; }>' is not assignable to type 'Ref'. 100 | Type '{ name: string; readonly type: "json"; mm: (px: T) => T; px: (mm: T) => T; ... 18 more ...; toJson: () => string; }' is missing the following properties from type 'MpcdiConfiguration': ratio, #overlaps, download 101 | `; 102 | 103 | export const ts4113Error = d` 104 | This member cannot have an 'override' modifier because it is not declared in the base class 'A'. 105 | `; 106 | 107 | export const ts4117Error = d` 108 | This member cannot have an 'override' modifier because it is not declared in the base class 'A'. Did you mean 'testA'? 109 | `; 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Logo 6 | 7 | 8 | 9 | # Pretty `TypeScript` Errors 10 | 11 | Make TypeScript errors prettier and human-readable in VSCode. 12 | 13 | [![GitHub stars](https://img.shields.io/github/stars/yoavbls/pretty-ts-errors.svg?style=social&label=Star)](https://GitHub.com/yoavbls/pretty-ts-errors/stargazers/) 14 | [![Visual Studio Code](https://custom-icon-badges.demolab.com/badge/Visual%20Studio%20Code-0078d7.svg?logo=vsc&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) [![GitHub license](https://badgen.net/github/license/yoavbls/pretty-ts-errors)](https://github.com/yoavbls/pretty-ts-errors/blob/main/LICENSE) [![Visual Studio Code](https://img.shields.io/visual-studio-marketplace/i/yoavbls.pretty-ts-errors)](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) 15 | Webstorm logo 16 | [![Cursor](https://custom-icon-badges.demolab.com/badge/Cursor-000000?logo=cursor-ai-white)](https://open-vsx.org/extension/yoavbls/pretty-ts-errors) 17 | 18 | TypeScript errors become messier as the complexity of types increases. At some point, TypeScript will throw on you a shitty heap of parentheses and `"..."`. 19 | This extension will help you understand what's going on. For example, in this relatively simple error: 20 | 21 |     22 | 23 | ## Watch this 24 | 25 | 26 | Watch theo's video 27 | 28 | 29 | and others from: 30 | [Web Dev Simplified](https://www.youtube.com/watch?v=ccg-erZYO4k&list=PL0rc4JAdEsVpOriHzlAG7KUnhKIK9c7OR&index=1), 31 | [Josh tried coding](https://www.youtube.com/watch?v=_9y29Cyo9uU&list=PL0rc4JAdEsVpOriHzlAG7KUnhKIK9c7OR&index=3), 32 | [trash dev](https://www.youtube.com/watch?v=WJeD3DKlWT4&list=PL0rc4JAdEsVpOriHzlAG7KUnhKIK9c7OR&index=4&t=208), 33 | and [more](https://www.youtube.com/playlist?list=PL0rc4JAdEsVpOriHzlAG7KUnhKIK9c7OR) 34 | 35 | ## Features 36 | 37 | - Syntax highlighting with your theme colors for types in error messages, supporting both light and dark themes 38 | - A button that leads you to the relevant type declaration next to the type in the error message 39 | - A button that navigates you to the error at [typescript.tv](http://typescript.tv), where you can find a detailed explanation, sometimes with a video 40 | - A button that navigates you to [ts-error-translator](https://ts-error-translator.vercel.app/), where you can read the error in plain English 41 | 42 | ## Supports 43 | 44 | - Node and Deno TypeScript error reporters (in `.ts` files) 45 | - JSDoc type errors (in `.js` and `.jsx` files) 46 | - React, Solid and Qwik errors (in `.tsx` and `.mdx` files) 47 | - Astro, Svelte and Vue files when TypeScript is enabled (in `.astro`, `.svelte` and `.vue` files) 48 | - Ember and Glimmer TypeScript errors and template issues reported by Glint (in `.hbs`, `.gjs`, and `.gts` files) 49 | 50 | ## Installation 51 | 52 | ``` 53 | code --install-extension yoavbls.pretty-ts-errors 54 | ``` 55 | 56 | Or simply by searching for `pretty-ts-errors` in the [VSCode marketplace](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) 57 | 58 | #### How to hide the original errors and make the types copyable 59 | 60 | Follow the instructions [there](./docs/hide-original-errors.md). unfortunately, this hack is required because of VSCode limitations. 61 | 62 | ## Why isn't it trivial 63 | 64 | 1. TypeScript errors contain types that are not valid in TypeScript. 65 | Yes, these types include things like `... more ...`, `{ ... }`, etc in an inconsistent manner. Some are also cutting in the middle because they're too long. 66 | 2. Types can't be syntax highlighted in code blocks because the part of `type X = ...` is missing, so I needed to create a new TextMate grammar, a superset of TypeScript grammar called `type`. 67 | 3. VSCode markdown blocks all styling options, so I had to find hacks to style the error messages. e.g., there isn't an inlined code block on VSCode markdown, so I used a code block inside a codicon icon, which is the only thing that can be inlined. That's why it can't be copied. but it isn't a problem because you can still hover on the error and copy things from the original error pane. 68 | 69 | 70 | ## Hype section 71 | 72 | 73 | 74 | Winning the Productivity Booster category at JSNation 2023 75 | 76 | 77 | 78 | 79 | 80 | 81 | Tanner's tweet 82 | 83 | 84 | 85 | 86 | 87 | 88 | Theo's tweet 89 | 90 | 91 | 92 | 93 | 94 | 95 | Johnson's tweet 96 | 97 | 98 | 99 | ### Stars from stars 100 | 101 | 102 | 103 | 104 | 110 | 116 | 122 | 128 | 134 | 135 | 136 |
105 | 106 |
107 | Kent C. Dodds 108 |
109 |
111 | 112 |
113 | Matt Pocock 114 |
115 |
117 | 118 |
119 | Alex / KATT 120 |
121 |
123 | 124 |
125 | Tanner Linsley 126 |
127 |
129 | 130 |
131 | Theo Browne 132 |
133 |
137 | 138 | ## Sponsorship 139 | 140 | Every penny will be invested in other contributors to the project, especially ones that work 141 | on things that I can't be doing myself like adding support to the extension for other IDEs 🫂 142 | 143 | ## Contribution 144 | 145 | Help by upvoting or commenting on issues we need to be resolved [here](https://github.com/yoavbls/pretty-ts-errors/discussions/43) 146 | Any other contribution is welcome. Feel free to open any issue / PR you think. 147 | --------------------------------------------------------------------------------