diff --git a/packages/ts-twoslasher/.gitignore b/packages/ts-twoslasher/.gitignore new file mode 100644 index 000000000000..ff88468482b1 --- /dev/null +++ b/packages/ts-twoslasher/.gitignore @@ -0,0 +1,8 @@ +*.log +.DS_Store +node_modules +.rts2_cache_cjs +.rts2_cache_esm +.rts2_cache_umd +.rts2_cache_system +dist diff --git a/packages/ts-twoslasher/LICENSE b/packages/ts-twoslasher/LICENSE new file mode 100644 index 000000000000..9f2bf75b16d0 --- /dev/null +++ b/packages/ts-twoslasher/LICENSE @@ -0,0 +1,17 @@ +The MIT License (MIT) +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/ts-twoslasher/README.md b/packages/ts-twoslasher/README.md new file mode 100644 index 000000000000..11821c7e0097 --- /dev/null +++ b/packages/ts-twoslasher/README.md @@ -0,0 +1,454 @@ +# TypeScript TwoSlasher + +A twisted markup for TypeScript code for writing code samples and letting the TypeScript compiler do more of the work inspired +by the [fourslash test system](https://github.com/orta/typescript-notes/blob/master/systems/testing/fourslash.md). + +Used as a pre-parser before showing code samples inside the TypeScript website and to create a standard way for us +to create examples for bugs on the compiler's issue tracker. + +### Features + +- Enforcing accurate errors from a TypeScript code sample, and leaving the messaging to the compiler +- Declaratively highlight symbols you want to show +- Handling showing the results of transpilation with certain flags +- Splitting a code sample to hide distracting code +- Support an example referencing multiple files +- Creating a playground link for the code + +### TODO + +- I think I will need to add an option to extract symbols for hover on identifiers at this level +- Think about how to ship to npm + + +The twoslash markup API lives inside your code samples code as comments, which can do special commands. There are the following commands: + +```ts +/** Available inline flags which are not compiler flags */ +interface ExampleOptions { + /** Let's the sample suppress all error diagnostics */ + noErrors: false; + /** An array of TS error codes, which you write as space separated - this is so the tool can know about unexpected errors */ + errors: number[]; + /** Shows the JS equivalent of the TypeScript code instead */ + showEmit: false; + /** + * When mixed with showEmit, lets you choose the file to present instead of the source - defaults to index.js which + * means when you just use `showEmit` above it shows the transpiled JS. + */ + showEmittedFile: string; +} +``` + +In addition to this set, you can use `@filename` which allow for exporting between files. + +Finally you can set any tsconfig compiler flag using this syntax, which you can see in some of the examples below. + +### Examples + +#### `compiler_errors.ts` + +```.ts +// @target: ES2015 +// @errors: 7006 + +function fn(s) { + console.log(s.subtr(3)) +} + +fn(42); +``` + +Turns to: + +> ```ts +> +> function fn(s) { +> console.log(s.subtr(3)) +> } +> +> fn(42); +> ``` + +> With: + +> ```json +> { +> "code": "See above", +> "extension": "ts", +> "highlights": [], +> "queries": [], +> "errors": [ +> { +> "category": 1, +> "code": 7006, +> "length": 1, +> "start": 13, +> "renderedMessage": "Parameter 's' implicitly has an 'any' type.", +> "id": "err-7006-13-1" +> } +> ], +> "playgroundURL": "https://www.typescriptlang.org/play/#code/FAMwrgdgxgLglgewgAhBAFAZwJTIN7DLJRKYIA2ApgHTkIDmW1mYARjAE7oDM22wAX2CgMAFgBM2ANzAgA" +> } +> ``` + +#### `compiler_flags.ts` + +```.ts +// @noImplicitAny: false +// @target: ES2015 + +// This will not throw because of the noImplicitAny +function fn(s) { + console.log(s.subtr(3)) +} + +fn(42); +``` + +Turns to: + +> ```ts +> +> // This will not throw because of the noImplicitAny +> function fn(s) { +> console.log(s.subtr(3)) +> } +> +> fn(42); +> ``` + +> With: + +> ```json +> { +> "code": "See above", +> "extension": "ts", +> "highlights": [], +> "queries": [], +> "errors": [], +> "playgroundURL": "https://www.typescriptlang.org/play/#code/FAehAIBUAsEsGdwHdYBtXgHYHsAu5doAnbJcAIwFMBjAQwFd5LxsAzA6ZnASQFsAHVLGqxcAQUwBPYK3qZquWNkzhWmABTwAlOADewcOGrL42VJQB0qbAHNNF+PXK4i6gMxatwAL7AZGgBYAJi0AbmAgA" +> } +> ``` + +#### `cuts_out_unneccessary_code.ts` + +```.ts +interface IdLabel { id: number, /* some fields */ } +interface NameLabel { name: string, /* other fields */ } +type NameOrId = T extends number ? IdLabel : NameLabel; +// This comment should not be included + +// ---cut--- +function createLabel(idOrName: T): NameOrId { + throw "unimplemented" +} + +let a = createLabel("typescript"); +// ^? + +let b = createLabel(2.8); +// ^? + +let c = createLabel(Math.random() ? "hello" : 42); +// ^? +``` + +Turns to: + +> ```ts +> +> function createLabel(idOrName: T): NameOrId { +> throw "unimplemented" +> } +> +> let a = createLabel("typescript"); +> +> let b = createLabel(2.8); +> +> let c = createLabel(Math.random() ? "hello" : 42); +> ``` + +> With: + +> ```json +> { +> "code": "See above", +> "extension": "ts", +> "highlights": [], +> "queries": [ +> { +> "kind": "query", +> "offset": 4, +> "position": 354 +> }, +> { +> "kind": "query", +> "offset": 4, +> "position": 390 +> }, +> { +> "kind": "query", +> "offset": 4, +> "position": 417 +> } +> ], +> "errors": [], +> "playgroundURL": "https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgJIBMAycBGEA2yA3ssOgFzIgCuAtnlADTID0AVMgM4D2tKMwAuk7I2LZAF8AUKEixEKAHJw+2PIRIgVESpzBRQAc2btk3MAAtoyAUJFjJUsAE8ADku0B5KBgA8AFWQIAA9IEGEqOgZkAB8ufSMAPmQAXmRAkLCImnprAH40LFwCZEplVWL8AG4pFnF-C2ARBF4+cC4Lbmp8dCpzZDxSEAR8anQIdCla8QBaOYRqMDmZqRhqYbBgbhBkBCgIOEg1AgCg0IhwkRzouL0DEENEgAoyb3KddIBKMq8fdADkkQpMgQchLFBuAB3ZAAInWwFornwEDakHQMKk0ikyLAyDgqV2+0OEGO+CeMJc7k4e2ArjAMM+NWxEFxOAJewOR0qTwATAA6AAcjKmON27KJXPUTwAsocLHyoHBwrwnp9kAUYVZ8PhuDDSsgACw84VAA" +> } +> ``` + +#### `declarations.ts` + +```.ts +// @declaration: true +// @showEmit +// @showEmittedFile: index.d.ts + +/** + * Gets the length of a string + * @param value a string + */ +export function getStringLength(value: string) { + return value.length +} +``` + +Turns to: + +> ```ts +> /** +> * Gets the length of a string +> * @param value a string +> */ +> export declare function getStringLength(value: string): number; +> ``` + +> With: + +> ```json +> { +> "code": "See above", +> "extension": "ts", +> "highlights": [], +> "queries": [], +> "errors": [], +> "playgroundURL": "https://www.typescriptlang.org/play/#code/PQKhFgCgAIWhxApgFwM7WQC0dANogOwHMtoB7AM2gENpVkAnAS2KlmgAEAHah6gW2gA3argCuOWvWasYIYFEQAPLmQbJoAE0QBjXLxwUxBHciZkC0IigDKjFkQAyhEpgAUI8YgBcde8QBKXwIxfgAjRAYAbiggA" +> } +> ``` + +#### `highlighting.ts` + +```.ts +function greet(person: string, date: Date) { + console.log(`Hello ${person}, today is ${date.toDateString()}!`); +} + +greet("Maddison", new Date()); +// ^^^^^^^^^^ +``` + +Turns to: + +> ```ts +> function greet(person: string, date: Date) { +> console.log(`Hello ${person}, today is ${date.toDateString()}!`); +> } +> +> greet("Maddison", new Date()); +> ``` + +> With: + +> ```json +> { +> "code": "See above", +> "extension": "ts", +> "highlights": [ +> { +> "kind": "highlight", +> "position": 134, +> "length": 10, +> "description": "" +> } +> ], +> "queries": [], +> "errors": [], +> "playgroundURL": "https://www.typescriptlang.org/play/#code/GYVwdgxgLglg9mABAcwE4FN1QBQAd2oDOCAXIoVKjGMgDSIAmAhlOmQCIvoCUiA3gChEiCAmIAbdADpxcZNgAGACXTjZiACR98RBAF96UOMwCeiGIU19mrKUc6sAypWrzuegIQLuAbgF6BATRMHAAiAFkmBgYLBFD6MHQAd0QHdGxuXwEgA" +> } +> ``` + +#### `import_files.ts` + +```.ts +// @filename: file-with-export.ts +export const helloWorld = "Example string"; + +// @filename: index.ts +import {helloWorld} from "./file-with-export" +console.log(helloWorld) +``` + +Turns to: + +> ```ts +> // @filename: file-with-export.ts +> export const helloWorld = "Example string"; +> +> // @filename: index.ts +> import {helloWorld} from "./file-with-export" +> console.log(helloWorld) +> ``` + +> With: + +> ```json +> { +> "code": "See above", +> "extension": "ts", +> "highlights": [], +> "queries": [], +> "errors": [], +> "playgroundURL": "https://www.typescriptlang.org/play/#code/PTAEAEDMEsBsFMB2BDAtvAXKGCC0B3aAFwAtd4APABwHsAnIgOiIGcAoS2h0AYxsRZFQJeLFg0A6vVgATUAF5QAIgCiFNFQShBdaIgDmSgNxs2ICDiRpMoPTMrN20VFyEBvEWMnSZAX2x0NKjKjMCWBMRknPRESmx8AjQIjOL6ABSe4lJ0sgCUbEA" +> } +> ``` + +#### `query.ts` + +```.ts +let foo = "hello there!"; +// ^? +``` + +Turns to: + +> ```ts +> let foo = "hello there!"; +> ``` + +> With: + +> ```json +> { +> "code": "See above", +> "extension": "ts", +> "highlights": [], +> "queries": [ +> { +> "kind": "query", +> "offset": 4, +> "position": 4 +> } +> ], +> "errors": [], +> "playgroundURL": "https://www.typescriptlang.org/play/#code/DYUwLgBAZg9jEF4ICIAWJjHmdAnEAhMgNwBQQA" +> } +> ``` + +#### `showEmit.ts` + +```.ts +// @showEmit +// @target: ES5 +// @downleveliteration +// @importhelpers + +// --importHelpers on: Spread helper will be imported from 'tslib' + +export function fn(arr: number[]) { + const arr2 = [1, ...arr]; +} +``` + +Turns to: + +> ```js +> "use strict"; +> // --importHelpers on: Spread helper will be imported from 'tslib' +> Object.defineProperty(exports, "__esModule", { value: true }); +> var tslib_1 = require("tslib"); +> function fn(arr) { +> var arr2 = tslib_1.__spread([1], arr); +> } +> exports.fn = fn; +> ``` + +> With: + +> ```json +> { +> "code": "See above", +> "extension": "js", +> "highlights": [], +> "queries": [], +> "errors": [], +> "playgroundURL": "https://www.typescriptlang.org/play/#code/EQVwzgpgBGAuBOBLAxrYBuAsAKAPS6gFpDEBbABwHt5YAJCAG3InjCkoDsAuKAZXPgQAhgBMoAC0bN4UAO6IGDKACNoZKjQhiAZvEqkoAclhgGiZYZwB5ZQCsIqAHQiI2xBwgAFPdNgBPAAoIAA8NEwAaKGAAfWiIMABZShEQBghgSIBvKAA3IQYQCB4EQqgAXwBKLGw8mRMzZWiARigAXihBAEcQREEA4HrzYCqcbRAOVEROKG0OAKF4eAqoTJwoddyFqAX4ACY2qEHGpsdYsAFhEQCAbSaAXUidkewynBCwsEdZg9nqoA" +> } +> ``` + +### API + +The API is one main exported function: + +```ts +/** + * Runs the checker against a TypeScript/JavaScript code sample returning potentially + * difference code, and a set of annotations around how it works. + * + * @param code The twoslash markup'd code + * @param extension For example: ts, tsx, typescript, javascript, js + */ +export function twoslasher(code: string, extension: string): TwoSlashReturn; +``` + +Which returns + +```ts +interface TwoSlashReturn { + /** The output code, could be TypeScript, but could also be a JS/JSON/d.ts */ + code: string; + /** The new extension type for the code, potentially changed if they've requested emitted results */ + extension: string; + /** Sample requests to highlight a particular part of the code */ + highlights: { + kind: 'highlight'; + position: number; + length: number; + description: string; + }[]; + /** Requests to use the LSP to get info for a particular symbol in the source */ + queries: { + kind: 'query'; + position: number; + offset: number; + }[]; + /** Diagnostic error messages which came up when creating the program */ + errors: { + renderedMessage: string; + id: string; + category: 0 | 1 | 2 | 3; + code: number; + start: number | undefined; + length: number | undefined; + }[]; + /** The URL for this sample in the playground */ + playgroundURL: string; +} +``` + + +## Local Development + +Below is a list of commands you will probably find useful. You can get debug logs by running with the env var of `DEBUG="*"`. + +### `npm start` or `yarn start` + +Runs the project in development/watch mode. Your project will be rebuilt upon changes. The library will be rebuilt if you make edits. + +### `npm run build` or `yarn build` + +Bundles the package to the `dist` folder. The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module). + +### `npm test` or `yarn test` + +Runs the test watcher (Jest) in an interactive mode. By default, runs tests related to files changed since the last commit. diff --git a/packages/ts-twoslasher/package.json b/packages/ts-twoslasher/package.json new file mode 100644 index 000000000000..7d1748e2fd24 --- /dev/null +++ b/packages/ts-twoslasher/package.json @@ -0,0 +1,51 @@ +{ + "name": "ts-twoslasher", + "version": "0.1.0", + "license": "MIT", + "author": "TypeScript team", + "main": "dist/index.js", + "module": "dist/ts-twoslasher.esm.js", + "typings": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "start": "tsdx watch", + "build": "tsdx build; yarn readme", + "readme": "yarn md-magic README.md --config ./scripts/inline-results.js", + "test": "tsdx test", + "lint": "tsdx lint" + }, + "peerDependencies": {}, + "husky": { + "hooks": { + "pre-commit": "tsdx lint" + } + }, + "prettier": { + "printWidth": 80, + "semi": true, + "singleQuote": true, + "trailingComma": "es5" + }, + "devDependencies": { + "@types/jest": "^24.0.23", + "@types/lz-string": "^1.3.33", + "@types/prettier": "^1.18.3", + "husky": "^3.0.9", + "jest-file-snapshot": "^0.3.8", + "markdown-magic": "^1.0.0", + "tsdx": "^0.11.0", + "tslib": "^1.10.0", + "typescript": "^3.7.2" + }, + "jest": { + "watchPathIgnorePatterns": [ + "test/results" + ] + }, + "dependencies": { + "debug": "^4.1.1", + "lz-string": "^1.4.4" + } +} diff --git a/packages/ts-twoslasher/scripts/inline-results.js b/packages/ts-twoslasher/scripts/inline-results.js new file mode 100644 index 000000000000..6769912898ff --- /dev/null +++ b/packages/ts-twoslasher/scripts/inline-results.js @@ -0,0 +1,87 @@ +const { readdirSync, readFileSync, lstatSync } = require('fs'); +const { join, parse } = require('path'); +const ts = require('typescript'); + +const fixturesFolder = join(__dirname, '../', 'test', 'fixtures'); +const resultsFolder = join(__dirname, '../', 'test', 'results'); + +const wrapCode = (code, ext) => '```' + ext + '\n' + code + '```'; +const wrapCodeAsQuote = (code, ext) => '> ```' + ext + '\n> ' + code.split("\n").join("\n> ") + '```'; + +module.exports = { + transforms: { + /* Match */ + FIXTURES(content, options) { + const mds = [] + + const fileToParse = join(__dirname, '../', 'src', 'index.ts') + let program = ts.createProgram([fileToParse], {}); + program.getTypeChecker({}); + + const sourceFile = program.getSourceFile(fileToParse) + let optionsInterface, mainExport, returnInterface + + ts.forEachChild(sourceFile, node => { + if (node.kind === ts.SyntaxKind.InterfaceDeclaration && node.symbol.escapedName === "ExampleOptions") { + optionsInterface = node + } + + if (node.kind === ts.SyntaxKind.InterfaceDeclaration && node.symbol.escapedName === "TwoSlashReturn") { + returnInterface = node + } + + if (node.kind === ts.SyntaxKind.FunctionDeclaration && node.symbol.escapedName === "twoslasher") { + mainExport = node + mainExport.body = null + } + }); + + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const twoslasher = printer.printNode(ts.EmitHint.Unspecified, mainExport, sourceFile) + "\n"; + const returnObj = printer.printNode(ts.EmitHint.Unspecified, returnInterface, sourceFile) + "\n"; + const optionsObj = printer.printNode(ts.EmitHint.Unspecified, optionsInterface, sourceFile) + "\n"; + + mds.push("The twoslash markup API lives inside your code samples code as comments, which can do special commands. There are the following commands:") + mds.push(wrapCode(optionsObj, "ts")) + + mds.push("In addition to this set, you can use `@filename` which allow for exporting between files.") + + mds.push("Finally you can set any tsconfig compiler flag using this syntax, which you can see in some of the examples below.") + + mds.push('### Examples'); + + readdirSync(fixturesFolder).forEach(fixtureName => { + const fixture = join(fixturesFolder, fixtureName); + if (lstatSync(fixture).isDirectory()) { return; } + + const resultName = parse(fixtureName).name + '.json'; + const result = join(resultsFolder, resultName); + + const input = readFileSync(fixture, 'utf8'); + const output = JSON.parse(readFileSync(result, 'utf8')); + + mds.push(`#### \`${fixtureName}\``); + mds.push(wrapCode(input, parse(fixtureName).ext)); + mds.push('Turns to:'); + + mds.push(wrapCodeAsQuote(output.code, output.extension)); + + mds.push('> With:'); + + const codeless = { ...output, code: "See above" } + mds.push(wrapCodeAsQuote(JSON.stringify(codeless, null, " ") + "\n", "json")); + }); + + mds.push('### API'); + mds.push("The API is one main exported function:") + mds.push(wrapCode(twoslasher, "ts")) + mds.push("Which returns") + mds.push(wrapCode(returnObj, "ts")) + + return mds.join('\n\n'); + }, + }, + callback: function() { + console.log('done'); + }, +}; diff --git a/packages/ts-twoslasher/src/index.ts b/packages/ts-twoslasher/src/index.ts new file mode 100644 index 000000000000..d805a6fd07cf --- /dev/null +++ b/packages/ts-twoslasher/src/index.ts @@ -0,0 +1,420 @@ +import ts from 'typescript'; +import debug from "debug" +import {compressToEncodedURIComponent} from "lz-string" + +// TODO: remove this somehow? +import * as fs from "fs"; + +import { parsePrimitive, escapeHtml, cleanMarkdownEscaped, typesToExtension } from './utils'; + +const log = debug("twoslasher") + +// Hacking in some internal stuff +declare module 'typescript' { + type Option = { + name: string; + type: 'list' | 'boolean' | 'number' | 'string' | ts.Map; + element?: Option; + }; + + const optionDeclarations: Array