├── .editorconfig ├── .gitignore ├── README.md ├── bin └── tsun ├── package.json ├── pnpm-lock.yaml ├── screenshot ├── block.png ├── color.png ├── completion.png ├── paste.png └── type.png ├── src ├── executor.ts ├── node-color-readline.d.ts ├── register.ts ├── repl.ts ├── service.ts └── util.ts ├── tsconfig.json └── tsun.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TSUN - TypeScript Upgraded Node 2 | 3 | TSUN, a TypeScript Upgraded Node, supports a REPL and interpreter for TypeScript. 4 | When invoked without ts file argument, TSUN works as a repl where you can type in expression. 5 | When you pass ts file to TSUN in command line argument, TSUN will automatically run it with invisible compilation. 6 | 7 | Feature: 8 | === 9 | * TS 2.0 support 10 | * Keyword highlight 11 | * Tab-completion support 12 | * Multiple Line Mode 13 | * Paste Mode 14 | * Definition Lookup 15 | * directly execute TypeScript application like `node` 16 | * [Vim-Worksheet](https://github.com/HerringtonDarkholme/vim-worksheet) support 17 | * And hidden feature for your exploration 18 | 19 | Install: 20 | === 21 | `npm install -g tsun` 22 | 23 | Usage: 24 | ==== 25 | * Use it as repl: `tsun` 26 | * Use it as interpreter: `tsun path/to/app.ts` 27 | * Other repl command can be accessed by typing `:help` 28 | * Command Line options can be viewd by passing `-h` or `--help` option 29 | 30 | Note: 31 | === 32 | When used as interpreter, tsun will create a temporary directory as output directory and create a node process to execute compiled js. 33 | So it is usually a problem to correctly resolve `node_modules` path or definition file like `*.d.ts`. 34 | Currently, tsun make two symbolic links for `node_modules` and `typings` directories in temporary directory, conventionally. 35 | 36 | TSUN will find the closest `tsconfig.json` relative to your working directory. You can now compile TypeScript to ES6 in node6+! 37 | 38 | Custom definition files and JavaScript library support will be added in next releases. 39 | 40 | ScreenShots: 41 | === 42 | Keyword Highlight 43 | ![Keyword Highlight](https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/master/screenshot/color.png) 44 | 45 | Tab-completion 46 | ![Tab Completion](https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/master/screenshot/completion.png) 47 | 48 | Multiple Line Editing, typing double blank lines will escape from Multiple line mode 49 | ![Multiple Line Editing](https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/master/screenshot/block.png) 50 | 51 | Paste Mode 52 | ![Paste Mode](https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/master/screenshot/paste.png) 53 | 54 | Definition Lookup 55 | ![Definition Lookup](https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/master/screenshot/type.png) 56 | 57 | And there is more for your exploration... 58 | 59 | TODO: 60 | === 61 | If you need these, please let me know by making [issues](https://github.com/HerringtonDarkholme/typescript-repl/issues)! 62 | 63 | * Add customization 64 | - [x] Add tsun config. Now tsun will read the closest tsconfig.json relative to the working directory you execute it. 65 | -------------------------------------------------------------------------------- /bin/tsun: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./tsun.js') 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsun", 3 | "preferGlobal": true, 4 | "version": "0.5.1", 5 | "description": "TSUN: a repl for TypeScript Upgraded Node", 6 | "bin": "./bin/tsun", 7 | "dependencies": { 8 | "colors": "^1.0.3", 9 | "diff": "^3.5.0", 10 | "node-color-readline": "^1.0.1", 11 | "optimist": "^0.6.1", 12 | "temp": "^0.8.1", 13 | "tslib": "^1.0.0", 14 | "@types/node": "^6.0.41" 15 | }, 16 | "files": [ 17 | "bin" 18 | ], 19 | "scripts": { 20 | "prepublish": "tsc" 21 | }, 22 | "keywords": [ 23 | "typescript", 24 | "repl", 25 | "command line", 26 | "interpreter" 27 | ], 28 | "author": "Herrington Darkholme", 29 | "license": "MIT", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/HerringtonDarkholme/typescript-repl.git" 33 | }, 34 | "homepage": "https://github.com/HerringtonDarkholme/typescript-repl", 35 | "bugs": "https://github.com/HerringtonDarkholme/typescript-repl/issues", 36 | "devDependencies": { 37 | "@types/colors": "^0.6.33", 38 | "@types/diff": "0.0.31", 39 | "typescript": "^3.1.0" 40 | }, 41 | "peerDependencies": { 42 | "typescript": ">=3.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.3 2 | 3 | specifiers: 4 | '@types/colors': ^0.6.33 5 | '@types/diff': 0.0.31 6 | '@types/node': ^6.0.41 7 | colors: ^1.0.3 8 | diff: ^3.5.0 9 | node-color-readline: ^1.0.1 10 | optimist: ^0.6.1 11 | temp: ^0.8.1 12 | tslib: ^1.0.0 13 | typescript: ^3.1.0 14 | 15 | dependencies: 16 | '@types/node': 6.14.13 17 | colors: 1.4.0 18 | diff: 3.5.0 19 | node-color-readline: 1.0.1 20 | optimist: 0.6.1 21 | temp: 0.8.4 22 | tslib: 1.14.1 23 | 24 | devDependencies: 25 | '@types/colors': 0.6.33 26 | '@types/diff': 0.0.31 27 | typescript: 3.9.10 28 | 29 | packages: 30 | 31 | /@types/colors/0.6.33: 32 | resolution: {integrity: sha1-F9raWXHDlSWUkNbIPXwYLPbpzlU=} 33 | dev: true 34 | 35 | /@types/diff/0.0.31: 36 | resolution: {integrity: sha1-ORlDcgoE9LopB5TqkMFQTBDTACU=} 37 | dev: true 38 | 39 | /@types/node/6.14.13: 40 | resolution: {integrity: sha512-J1F0XJ/9zxlZel5ZlbeSuHW2OpabrUAqpFuC2sm2I3by8sERQ8+KCjNKUcq8QHuzpGMWiJpo9ZxeHrqrP2KzQw==} 41 | dev: false 42 | 43 | /ansi-regex/2.1.1: 44 | resolution: {integrity: sha1-w7M6te42DYbg5ijwRorn7yfWVN8=} 45 | engines: {node: '>=0.10.0'} 46 | dev: false 47 | 48 | /ansi-styles/2.2.1: 49 | resolution: {integrity: sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=} 50 | engines: {node: '>=0.10.0'} 51 | dev: false 52 | 53 | /ansi/0.3.1: 54 | resolution: {integrity: sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=} 55 | dev: false 56 | 57 | /balanced-match/1.0.2: 58 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 59 | dev: false 60 | 61 | /brace-expansion/1.1.11: 62 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 63 | dependencies: 64 | balanced-match: 1.0.2 65 | concat-map: 0.0.1 66 | dev: false 67 | 68 | /chalk/1.1.3: 69 | resolution: {integrity: sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=} 70 | engines: {node: '>=0.10.0'} 71 | dependencies: 72 | ansi-styles: 2.2.1 73 | escape-string-regexp: 1.0.5 74 | has-ansi: 2.0.0 75 | strip-ansi: 3.0.1 76 | supports-color: 2.0.0 77 | dev: false 78 | 79 | /colors/1.4.0: 80 | resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} 81 | engines: {node: '>=0.1.90'} 82 | dev: false 83 | 84 | /concat-map/0.0.1: 85 | resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} 86 | dev: false 87 | 88 | /diff/3.5.0: 89 | resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} 90 | engines: {node: '>=0.3.1'} 91 | dev: false 92 | 93 | /escape-string-regexp/1.0.5: 94 | resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} 95 | engines: {node: '>=0.8.0'} 96 | dev: false 97 | 98 | /fs.realpath/1.0.0: 99 | resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} 100 | dev: false 101 | 102 | /glob/7.2.0: 103 | resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} 104 | dependencies: 105 | fs.realpath: 1.0.0 106 | inflight: 1.0.6 107 | inherits: 2.0.4 108 | minimatch: 3.0.4 109 | once: 1.4.0 110 | path-is-absolute: 1.0.1 111 | dev: false 112 | 113 | /has-ansi/2.0.0: 114 | resolution: {integrity: sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=} 115 | engines: {node: '>=0.10.0'} 116 | dependencies: 117 | ansi-regex: 2.1.1 118 | dev: false 119 | 120 | /inflight/1.0.6: 121 | resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} 122 | dependencies: 123 | once: 1.4.0 124 | wrappy: 1.0.2 125 | dev: false 126 | 127 | /inherits/2.0.4: 128 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 129 | dev: false 130 | 131 | /minimatch/3.0.4: 132 | resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} 133 | dependencies: 134 | brace-expansion: 1.1.11 135 | dev: false 136 | 137 | /minimist/0.0.10: 138 | resolution: {integrity: sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=} 139 | dev: false 140 | 141 | /node-color-readline/1.0.1: 142 | resolution: {integrity: sha1-5XBj5hAcg4cWCsKqNZ1kJ+HiaIY=} 143 | dependencies: 144 | ansi: 0.3.1 145 | chalk: 1.1.3 146 | dev: false 147 | 148 | /once/1.4.0: 149 | resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} 150 | dependencies: 151 | wrappy: 1.0.2 152 | dev: false 153 | 154 | /optimist/0.6.1: 155 | resolution: {integrity: sha1-2j6nRob6IaGaERwybpDrFaAZZoY=} 156 | dependencies: 157 | minimist: 0.0.10 158 | wordwrap: 0.0.3 159 | dev: false 160 | 161 | /path-is-absolute/1.0.1: 162 | resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} 163 | engines: {node: '>=0.10.0'} 164 | dev: false 165 | 166 | /rimraf/2.6.3: 167 | resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} 168 | hasBin: true 169 | dependencies: 170 | glob: 7.2.0 171 | dev: false 172 | 173 | /strip-ansi/3.0.1: 174 | resolution: {integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=} 175 | engines: {node: '>=0.10.0'} 176 | dependencies: 177 | ansi-regex: 2.1.1 178 | dev: false 179 | 180 | /supports-color/2.0.0: 181 | resolution: {integrity: sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=} 182 | engines: {node: '>=0.8.0'} 183 | dev: false 184 | 185 | /temp/0.8.4: 186 | resolution: {integrity: sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==} 187 | engines: {node: '>=6.0.0'} 188 | dependencies: 189 | rimraf: 2.6.3 190 | dev: false 191 | 192 | /tslib/1.14.1: 193 | resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} 194 | dev: false 195 | 196 | /typescript/3.9.10: 197 | resolution: {integrity: sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==} 198 | engines: {node: '>=4.2.0'} 199 | hasBin: true 200 | dev: true 201 | 202 | /wordwrap/0.0.3: 203 | resolution: {integrity: sha1-o9XabNXAvAAI03I0u68b7WMFkQc=} 204 | engines: {node: '>=0.4.0'} 205 | dev: false 206 | 207 | /wrappy/1.0.2: 208 | resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} 209 | dev: false 210 | -------------------------------------------------------------------------------- /screenshot/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/c17050a640595f9521ccefa288900dbb5d210ade/screenshot/block.png -------------------------------------------------------------------------------- /screenshot/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/c17050a640595f9521ccefa288900dbb5d210ade/screenshot/color.png -------------------------------------------------------------------------------- /screenshot/completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/c17050a640595f9521ccefa288900dbb5d210ade/screenshot/completion.png -------------------------------------------------------------------------------- /screenshot/paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/c17050a640595f9521ccefa288900dbb5d210ade/screenshot/paste.png -------------------------------------------------------------------------------- /screenshot/type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/c17050a640595f9521ccefa288900dbb5d210ade/screenshot/type.png -------------------------------------------------------------------------------- /src/executor.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import * as path from 'path' 3 | import * as child_process from 'child_process' 4 | import * as fs from 'fs' 5 | 6 | export function runCode(argv: any) { 7 | // run code in temp path, and cleanup 8 | var temp = require('temp') 9 | temp.track() 10 | process.on('SIGINT', () => temp.cleanupSync()) 11 | process.on('SIGTERM', () => temp.cleanupSync()) 12 | 13 | let tempPath = temp.mkdirSync('tsrun') 14 | let outDir = tempPath 15 | if (argv.o) { 16 | outDir = path.join(tempPath, argv.o) 17 | } 18 | let compileError = compile(argv._, { 19 | outDir, 20 | noEmitOnError: true, 21 | target: ts.ScriptTarget.ES5, 22 | module: ts.ModuleKind.CommonJS, 23 | experimentalDecorators: true, 24 | }) 25 | if (compileError) process.exit(compileError) 26 | linkDir(process.cwd(), tempPath) 27 | // slice argv. 0: node, 1: tsun binary 2: arg 28 | var newArgv = process.argv.slice(2).map(arg => { 29 | if (!/\.ts$/.test(arg)) return arg 30 | return path.join(outDir, arg.replace(/ts$/, 'js')) 31 | }) 32 | child_process.execFileSync('node', newArgv, { 33 | stdio: 'inherit' 34 | }) 35 | process.exit() 36 | } 37 | 38 | function linkDir(src: string, dest: string) { 39 | let files = ['node_modules', 'typings'] 40 | for (let file of files) { 41 | let srcpath = path.join(src, file) 42 | let destpath = path.join(dest, file) 43 | fs.symlinkSync(srcpath, destpath, 'dir') 44 | } 45 | } 46 | 47 | function compile(fileNames: string[], options: ts.CompilerOptions): number { 48 | var program = ts.createProgram(fileNames, options); 49 | var emitResult = program.emit(); 50 | 51 | var allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); 52 | 53 | allDiagnostics.forEach(diagnostic => { 54 | var message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 55 | if (!diagnostic.file) return console.log(message) 56 | var { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); 57 | console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); 58 | }); 59 | 60 | var exitCode = emitResult.emitSkipped ? 1 : 0; 61 | return exitCode 62 | } 63 | -------------------------------------------------------------------------------- /src/node-color-readline.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'readline' { 2 | interface ReadLineOptions { 3 | colorize: Function 4 | } 5 | } 6 | 7 | declare module 'node-color-readline' { 8 | import * as readline from 'readline' 9 | export =readline 10 | } 11 | -------------------------------------------------------------------------------- /src/register.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import * as fs from 'fs' 3 | 4 | require.extensions['.ts'] = function(module: any, filename: string) { 5 | var text = fs.readFileSync(filename, 'utf8') 6 | module._compile(ts.transpile(text, {}, filename), filename) 7 | } 8 | -------------------------------------------------------------------------------- /src/repl.ts: -------------------------------------------------------------------------------- 1 | import * as readlineTTY from 'node-color-readline' 2 | import * as readlineNoTTY from 'readline' 3 | import * as util from 'util' 4 | import * as vm from 'vm' 5 | import * as tty from 'tty' 6 | import {Console} from 'console' 7 | import * as path from 'path' 8 | import * as child_process from 'child_process' 9 | import * as fs from 'fs' 10 | 11 | import { 12 | completer, acceptedCodes, testSyntacticError, clearHistory, 13 | getType, getDiagnostics, getCurrentCode, getDeclarations, 14 | } from './service' 15 | 16 | import {assign} from './util' 17 | 18 | var Module = require('module') 19 | 20 | import * as colors from 'colors' 21 | 22 | // node-color-readline blows up in non-TTY envs 23 | const readline = (process.stdout as tty.WriteStream).isTTY ? readlineTTY : readlineNoTTY 24 | 25 | var options = require('optimist') 26 | .alias('f', 'force') 27 | .describe('f', 'Force tsun to evaluate code with ts errors.') 28 | .alias('v', 'verbose') 29 | .describe('v', 'Print compiled javascript before evaluating.') 30 | .describe('dere', "I-its's not like I'm an option so DON'T GET THE WRONG IDEA!") 31 | .describe('ignore-undefined', 'Do not output the return value of a command if it evaluates to undefined') 32 | 33 | var argv = options.argv 34 | var verbose = argv.verbose 35 | 36 | export var defaultPrompt = '> ', moreLinesPrompt = '..' 37 | // a buffer for multiline editing 38 | var multilineBuffer = '' 39 | var rl = createReadLine() 40 | 41 | function colorize(line: string) { 42 | let colorized = '' 43 | let regex: [RegExp, string][] = [ 44 | [/\/\/.*$/m, 'grey'], // comment 45 | [/(['"`\/]).*?(?!<\\)\1/, 'cyan'], // string/regex, not rock solid 46 | [/[+-]?(\d+\.?\d*|\d*\.\d+)([eE][+-]?\d+)?/, 'cyan'], // number 47 | [/\b(true|false|null|undefined|NaN|Infinity)\b/, 'blue'], 48 | [/\b(in|if|for|while|var|new|function|do|return|void|else|break)\b/, 'green'], 49 | [/\b(instanceof|with|case|default|try|this|switch|continue|typeof)\b/, 'green'], 50 | [/\b(let|yield|const|class|extends|interface|type)\b/, 'green'], 51 | [/\b(try|catch|finally|Error|delete|throw|import|from|as)\b/, 'red'], 52 | [/\b(eval|isFinite|isNaN|parseFloat|parseInt|decodeURI|decodeURIComponent)\b/, 'yellow'], 53 | [/\b(encodeURI|encodeURIComponent|escape|unescape|Object|Function|Boolean|Error)\b/, 'yellow'], 54 | [/\b(Number|Math|Date|String|RegExp|Array|JSON|=>|string|number|boolean)\b/, 'yellow'], 55 | [/\b(console|module|process|require|arguments|fs|global)\b/, 'yellow'], 56 | [/\b(private|public|protected|abstract|namespace|declare|@)\b/, 'magenta'], // TS keyword 57 | [/\b(keyof|readonly)\b/, 'green'], 58 | ] 59 | while (line !== '') { 60 | let start = +Infinity 61 | let color = '' 62 | let length = 0 63 | for (let reg of regex) { 64 | let match = reg[0].exec(line) 65 | if (match && match.index < start) { 66 | start = match.index 67 | color = reg[1] 68 | length = match[0].length 69 | } 70 | } 71 | colorized += line.substring(0, start) 72 | if (color) { 73 | colorized += (line.substr(start, length))[color] 74 | } 75 | line = line.substr(start + length) 76 | } 77 | return colorized 78 | } 79 | 80 | function createReadLine() { 81 | return readline.createInterface({ 82 | input: process.stdin, 83 | output: process.stdout, 84 | colorize: colorize, 85 | completer(line: string) { 86 | let code = multilineBuffer + '\n' + line 87 | return completer(code) as any 88 | } 89 | }) 90 | } 91 | 92 | // Much of this function is from repl.REPLServer.createContext 93 | function createContext() { 94 | var builtinLibs = require('repl')._builtinLibs 95 | var context: any; 96 | context = vm.createContext(); 97 | assign(context, global) 98 | 99 | context.console = new Console(process.stdout); 100 | context.global = context; 101 | context.global.global = context; 102 | context.module = new Module(''); 103 | try { 104 | // hack for require.resolve("./relative") to work properly. 105 | context.module.filename = path.resolve('repl'); 106 | } catch (e) { 107 | // path.resolve('repl') fails when the current working directory has been 108 | // deleted. Fall back to the directory name of the (absolute) executable 109 | // path. It's not really correct but what are the alternatives? 110 | const dirname = path.dirname(process.execPath); 111 | context.module.filename = path.resolve(dirname, 'repl'); 112 | } 113 | context.module.paths = Module._nodeModulePaths(context.module.filename) 114 | context.paths = Module._resolveLookupPaths(process.cwd(), context.module)[1] 115 | var req = context.module.require.bind(context.module) 116 | context.require = req 117 | 118 | // Lazy load modules on use 119 | builtinLibs.forEach(function (name: string) { 120 | Object.defineProperty(context, name, { 121 | get: function () { 122 | var lib = require(name); 123 | context[name] = lib; 124 | return lib; 125 | }, 126 | // Allow creation of globals of the same name 127 | set: function (val: any) { 128 | delete context[name]; 129 | context[name] = val; 130 | }, 131 | configurable: true 132 | }); 133 | }); 134 | 135 | return context; 136 | } 137 | 138 | 139 | 140 | 141 | 142 | function printHelp() { 143 | console.log(` 144 | tsun repl commands 145 | :type symbol print the type of an identifier 146 | :doc symbol print the documentation for an identifier 147 | :clear clear all the code 148 | :print print code input so far 149 | :help print this manual 150 | :paste enter paste mode 151 | :load filename source typescript file in current context`.blue) 152 | if (argv.dere) { 153 | console.log(':baka Who would like some pervert like you, baka~'.blue) 154 | } 155 | } 156 | 157 | 158 | 159 | var context = createContext(); 160 | function startEvaluate(code: string) { 161 | multilineBuffer = '' 162 | let allDiagnostics = getDiagnostics(code) 163 | if (allDiagnostics.length) { 164 | console.warn(colors.bold(allDiagnostics.join('\n').red)) 165 | if (defaultPrompt != '> ') { 166 | console.log('') 167 | console.log(defaultPrompt, 'URUSAI URUSAI URUSAI'.magenta) 168 | console.log('') 169 | } 170 | return repl(defaultPrompt); 171 | } 172 | let current = getCurrentCode() 173 | if (verbose) { 174 | console.log(current.green); 175 | } 176 | try { 177 | var result = vm.runInContext(current, context); 178 | 179 | if (result === undefined && !argv['ignore-undefined']) { 180 | console.log(util.inspect(result, false, 2, true)) 181 | } 182 | } catch (e) { 183 | console.log(e.stack); 184 | } 185 | 186 | } 187 | 188 | function waitForMoreLines(code: string, indentLevel: number) { 189 | if (/\n{2}$/.test(code)) { 190 | console.log('You typed two blank lines! start new command'.yellow) 191 | multilineBuffer = '' 192 | return repl(defaultPrompt) 193 | } 194 | var nextPrompt = ''; 195 | for (var i = 0; i < indentLevel; i++) { 196 | nextPrompt += moreLinesPrompt; 197 | } 198 | multilineBuffer = code 199 | repl(nextPrompt); 200 | } 201 | 202 | function replLoop(_: string, code: string) { 203 | code = multilineBuffer + '\n' + code 204 | let diagnostics = testSyntacticError(code) 205 | if (diagnostics.length === 0) { 206 | startEvaluate(code) 207 | repl(defaultPrompt) 208 | } else { 209 | let openCurly = (code.match(/\{/g) || []).length; 210 | let closeCurly = (code.match(/\}/g) || []).length; 211 | let openParen = (code.match(/\(/g) || []).length; 212 | let closeParen = (code.match(/\)/g) || []).length; 213 | // at lease one indent in multiline 214 | let indentLevel = (openCurly - closeCurly + openParen - closeParen) || 1 215 | waitForMoreLines(code, indentLevel || 1) 216 | } 217 | } 218 | 219 | function addLine(line: string) { 220 | multilineBuffer += '\n' + line 221 | } 222 | 223 | function enterPasteMode() { 224 | console.log('// entering paste mode, press ctrl-d to evaluate'.cyan) 225 | console.log('') 226 | let oldPrompt = defaultPrompt 227 | rl.setPrompt('') 228 | rl.on('line', addLine) 229 | rl.once('close', () => { 230 | console.log('evaluating...'.cyan) 231 | rl.removeListener('line', addLine) 232 | startEvaluate(multilineBuffer) 233 | rl = createReadLine() 234 | repl(defaultPrompt = oldPrompt) 235 | }) 236 | } 237 | 238 | function loadFile(filename: string) { 239 | try { 240 | let filePath = path.resolve(filename) 241 | let fileContents = fs.readFileSync(filePath, 'utf8') 242 | if (verbose) { 243 | console.log(`loading file: ${filePath}`.cyan) 244 | console.log(colorize(fileContents)) 245 | console.log('evaluating...'.cyan) 246 | } 247 | startEvaluate(fileContents) 248 | } catch(e) { 249 | console.log(e) 250 | } 251 | } 252 | 253 | function getSource(name: string) { 254 | let declarations = getDeclarations() 255 | for (let file in declarations) { 256 | let names = declarations[file] 257 | if (names[name]) { 258 | let decl = names[name] 259 | let pager = process.env.PAGER 260 | let parent = decl[0].parent 261 | let text = parent ? parent.getFullText() : '' 262 | if (!pager || text.split('\n').length < 24) { 263 | console.log(text) 264 | repl(defaultPrompt) 265 | return 266 | } 267 | process.stdin.pause() 268 | var tty = require('tty') 269 | tty.setRawMode(false) 270 | var temp = require('temp') 271 | let tempFile = temp.openSync('DUMMY_FILE' + Math.random()) 272 | fs.writeFileSync(tempFile.path, text) 273 | let display = child_process.spawn('less', [tempFile.path], { 274 | 'stdio': [0, 1, 2] 275 | }) 276 | display.on('exit', function() { 277 | temp.cleanupSync() 278 | tty.setRawMode(true) 279 | process.stdin.resume() 280 | repl(defaultPrompt) 281 | }) 282 | return 283 | } 284 | } 285 | console.log(`identifier ${name} not found`.yellow) 286 | } 287 | 288 | // main loop 289 | export function repl(prompt: string) { 290 | 'use strict'; 291 | rl.question(prompt, function (code: string) { 292 | if (/^:(type|doc)/.test(code)) { 293 | let identifier = code.split(' ')[1] 294 | if (!identifier) { 295 | console.log(':type command need names!'.red) 296 | return repl(prompt) 297 | } 298 | const ret = getType(identifier, code.indexOf('doc') === 1) 299 | if (ret) { 300 | console.log(colorize(ret)) 301 | } else { 302 | console.log(`no info for "${identifier}" is found`.yellow) 303 | } 304 | return repl(prompt) 305 | } 306 | if (/^:source/.test(code)) { 307 | let identifier = code.split(' ')[1] 308 | if (!identifier) { 309 | console.log(':source command need names!'.red) 310 | return repl(prompt) 311 | } 312 | getSource(identifier) 313 | return 314 | } 315 | if (/^:help/.test(code)) { 316 | printHelp() 317 | return repl(prompt) 318 | } 319 | if (/^:clear/.test(code)) { 320 | clearHistory() 321 | multilineBuffer = '' 322 | context = createContext() 323 | return repl(defaultPrompt) 324 | } 325 | if (/^:print/.test(code)) { 326 | console.log(colorize(acceptedCodes)) 327 | return repl(prompt) 328 | } 329 | if (/^:paste/.test(code) && !multilineBuffer) { 330 | return enterPasteMode() 331 | } 332 | if (/^:load/.test(code) && !multilineBuffer) { 333 | let filename = code.split(' ')[1]; 334 | if (!filename) { 335 | console.log(':load: file name expected'.red) 336 | return repl(prompt) 337 | } 338 | loadFile(filename) 339 | return repl(prompt) 340 | } 341 | if (argv.dere && /^:baka/.test(code)) { 342 | defaultPrompt = 'ξ(゚⊿゚)ξ> ' 343 | moreLinesPrompt = 'ζ(///*ζ) '; 344 | return repl(defaultPrompt) 345 | } 346 | replLoop(prompt, code) 347 | }); 348 | } 349 | 350 | export function startRepl() { 351 | repl(defaultPrompt) 352 | } 353 | -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import * as path from 'path' 3 | import { readdirSync, existsSync, readFileSync, statSync } from 'fs' 4 | import * as diff from 'diff' 5 | import {assign} from './util' 6 | 7 | // codes has been accepted by service, as opposed to codes in buffer and user input 8 | // if some action fails to compile, acceptedCodes will be rolled-back 9 | export var acceptedCodes = getInitialCommands() 10 | // a counter indicating repl edition history, every action will increment it 11 | var versionCounter = 0 12 | 13 | function findConfigFile(searchPath: string) { 14 | while (true) { 15 | const fileName = path.join(searchPath, "tsconfig.json"); 16 | if (existsSync(fileName)) { 17 | return fileName; 18 | } 19 | const parentPath = path.dirname(searchPath); 20 | if (parentPath === searchPath) { 21 | break; 22 | } 23 | searchPath = parentPath; 24 | } 25 | return undefined; 26 | } 27 | 28 | const CWD = process.cwd() 29 | 30 | const DEFAULT_OPTIONS: ts.CompilerOptions = { 31 | target: ts.ScriptTarget.ES5, 32 | newLine: ts.NewLineKind.LineFeed, 33 | experimentalDecorators: true, 34 | emitDecoratorMetadata: true, 35 | noUnusedLocals: false, 36 | configFilePath: path.join(CWD, 'tsconfig.json'), 37 | } 38 | 39 | // these option must be set in repl environment 40 | const OVERRIDE_OPTIONS: ts.CompilerOptions = { 41 | module: ts.ModuleKind.CommonJS, 42 | noEmitHelpers: true, 43 | noUnusedLocals: false, 44 | sourceMap: false, 45 | noEmit: false 46 | } 47 | 48 | function compileOption(): () => ts.CompilerOptions { 49 | let configFile = findConfigFile(process.cwd()) 50 | if (!configFile) { 51 | return () => DEFAULT_OPTIONS 52 | } 53 | 54 | let configText = readFileSync(configFile, 'utf8') 55 | let result = ts.parseConfigFileTextToJson(configFile, configText) 56 | if (result.error) { 57 | return () => DEFAULT_OPTIONS 58 | } 59 | let optionOrError = ts.convertCompilerOptionsFromJson( 60 | result.config.compilerOptions, 61 | path.dirname(configFile) 62 | ) 63 | if (optionOrError.errors.length) { 64 | return () => DEFAULT_OPTIONS 65 | } 66 | let options = optionOrError.options 67 | 68 | // override some impossible option 69 | assign(options, OVERRIDE_OPTIONS) 70 | return () => options 71 | } 72 | 73 | const resolvedOpt = compileOption()() 74 | const DUMMY_FILE = resolvedOpt.rootDir ? resolvedOpt.rootDir + 'TSUN.repl.generated.ts': 'TSUN.repl.generated.ts' 75 | 76 | var serviceHost: ts.LanguageServiceHost = { 77 | getCompilationSettings: compileOption(), 78 | getScriptFileNames: () => [DUMMY_FILE], 79 | getScriptVersion: (fileName) => { 80 | return fileName === DUMMY_FILE ? versionCounter.toString() : '1' 81 | }, 82 | getScriptSnapshot: (fileName) => { 83 | try { 84 | var text = fileName === DUMMY_FILE 85 | ? acceptedCodes 86 | : readFileSync(fileName).toString() 87 | return ts.ScriptSnapshot.fromString(text) 88 | } catch(e) { 89 | return undefined 90 | } 91 | }, 92 | getCurrentDirectory: () => CWD, 93 | getDirectories: ts.sys.getDirectories, 94 | directoryExists: ts.sys.directoryExists, 95 | fileExists: ts.sys.fileExists, 96 | readFile: ts.sys.readFile, 97 | readDirectory: ts.sys.readDirectory, 98 | getDefaultLibFileName: options => ts.getDefaultLibFilePath(options) 99 | } 100 | 101 | var service = ts.createLanguageService(serviceHost) 102 | 103 | export var getDeclarations = (function() { 104 | var declarations: {[fileName: string]: {[name: string]: ts.DeclarationName[]}} = {} 105 | let declFiles = getDeclarationFiles() 106 | for (let file of declFiles) { 107 | let text = readFileSync(file, 'utf8') 108 | declarations[file] = collectDeclaration(ts.createSourceFile(file, text, ts.ScriptTarget.Latest)) 109 | } 110 | return function(cached: boolean = false) { 111 | if (!cached) { 112 | declarations[DUMMY_FILE] = collectDeclaration(ts.createSourceFile(DUMMY_FILE, acceptedCodes, ts.ScriptTarget.Latest)) 113 | } 114 | return declarations 115 | } 116 | })() 117 | 118 | function getDeclarationFiles() { 119 | var libPaths = [path.resolve(__dirname, '../../node_modules/@types/node/index.d.ts')] 120 | try { 121 | let typings = path.join(process.cwd(), './typings') 122 | let dirs = readdirSync(typings) 123 | for (let dir of dirs) { 124 | if (!/\.d\.ts$/.test(dir)) continue 125 | let p = path.join(typings, dir) 126 | if (statSync(p).isFile()) { 127 | libPaths.push(p) 128 | } 129 | } 130 | } catch(e) { 131 | } 132 | return libPaths 133 | } 134 | 135 | function getInitialCommands() { 136 | return getDeclarationFiles() 137 | .map(dir => `/// \n`).join() 138 | } 139 | 140 | // private api hacks 141 | function collectDeclaration(sourceFile: any): any { 142 | let decls = sourceFile.getNamedDeclarations() 143 | var ret: any = {} 144 | for (let decl in decls) { 145 | ret[decl] = Array.isArray(decls[decl]) && decls[decl].map((t: any) => t.name) 146 | } 147 | return ret 148 | } 149 | 150 | 151 | export function completer(line: string) { 152 | // append new line to get completions, then revert new line 153 | versionCounter++ 154 | let originalCodes = acceptedCodes 155 | acceptedCodes += line 156 | if (':' === line[0]) { 157 | let candidates = ['type', 'detail', 'source', 'paste', 'clear', 'print', 'help'] 158 | candidates = candidates.map(c => ':' + c).filter(c => c.indexOf(line) >= 0) 159 | return [candidates, line.trim()] 160 | } 161 | let completions = service.getCompletionsAtPosition(DUMMY_FILE, acceptedCodes.length, undefined) 162 | if (!completions) { 163 | acceptedCodes = originalCodes 164 | return [[], line] 165 | } 166 | let prefix = /[A-Za-z_$]+$/.exec(line) 167 | let candidates: string[] = [] 168 | if (prefix) { 169 | let prefixStr = prefix[0] 170 | candidates = completions.entries.filter((entry) => { 171 | let name = entry.name 172 | return name.substr(0, prefixStr.length) == prefixStr 173 | }).map(entry => entry.name) 174 | } else { 175 | candidates = completions.entries.map(entry => entry.name) 176 | } 177 | acceptedCodes = originalCodes 178 | return [candidates, prefix ? prefix[0] : line] 179 | } 180 | 181 | export function getType(name: string, detailed: boolean): string { 182 | versionCounter++ 183 | let originalCodes = acceptedCodes 184 | acceptedCodes += '\n;' + name 185 | let typeInfo = service.getQuickInfoAtPosition(DUMMY_FILE, acceptedCodes.length - 1) 186 | let ret = '' 187 | if (typeInfo) { 188 | ret = detailed 189 | ? ts.displayPartsToString(typeInfo.documentation) 190 | : ts.displayPartsToString(typeInfo.displayParts) 191 | } 192 | acceptedCodes = originalCodes 193 | return ret 194 | } 195 | 196 | export function getDiagnostics(code: string): string[] { 197 | let fallback = acceptedCodes 198 | acceptedCodes += code 199 | versionCounter++ 200 | let allDiagnostics = service.getCompilerOptionsDiagnostics() 201 | .concat(service.getSemanticDiagnostics(DUMMY_FILE)) 202 | let ret = allDiagnostics.map(diagnostic => { 203 | let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') 204 | return message 205 | }) 206 | if (ret.length) acceptedCodes = fallback 207 | return ret 208 | } 209 | 210 | let lastOutput = '' 211 | export function getCurrentCode() { 212 | let emit = service.getEmitOutput(DUMMY_FILE) 213 | let output = emit.outputFiles[0].text 214 | let changes = diff.diffLines(lastOutput, output) 215 | let ret = changes.filter(c => c.added).map(c => c.value).join('\n') 216 | lastOutput = output 217 | return ret 218 | } 219 | 220 | export function testSyntacticError(code: string) { 221 | let fallback = acceptedCodes 222 | versionCounter++ 223 | acceptedCodes += code 224 | let diagnostics = service.getSyntacticDiagnostics(DUMMY_FILE) 225 | acceptedCodes = fallback 226 | return diagnostics 227 | } 228 | 229 | export function clearHistory() { 230 | acceptedCodes = getInitialCommands() 231 | lastOutput = '' 232 | } 233 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export function assign(dest: any, src: any) { 2 | for (let key in src) { 3 | dest[key] = src[key] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "noImplicitAny": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitThis": true, 8 | "noImplicitReturns": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "strictNullChecks": true, 12 | "skipLibCheck": true, 13 | "outDir": "bin" 14 | }, 15 | "exclude": ["node_modules", "bin"] 16 | } 17 | -------------------------------------------------------------------------------- /tsun.ts: -------------------------------------------------------------------------------- 1 | import * as tslib from 'tslib' 2 | import {assign} from './src/util' 3 | assign(global, tslib) 4 | 5 | import './src/register' 6 | 7 | import {runCode} from './src/executor' 8 | import {startRepl} from './src/repl' 9 | 10 | var options = require('optimist') 11 | .usage(`A TypeScript REPL. Usage: 12 | ${'tsun'.blue} [options] [script.ts]`) 13 | .alias('h', 'help') 14 | .describe('h', 'Print this help message') 15 | .alias('o', 'out') 16 | .describe('o', 'output directory relative to temporary') 17 | .describe('dere', "I-its's not like I'm an option so DON'T GET THE WRONG IDEA!") 18 | 19 | var argv = options.argv 20 | 21 | if (argv._.length === 1) { 22 | runCode(argv) 23 | } 24 | if (argv.h) { 25 | options.showHelp() 26 | process.exit(1) 27 | } 28 | 29 | if (!argv.dere) { 30 | console.log('TSUN'.blue, ': TypeScript Upgraded Node') 31 | console.log('type in TypeScript expression to evaluate') 32 | console.log('type', ':help'.blue.bold, 'for commands in repl') 33 | } else { 34 | console.log('TSUN'.magenta, " I'm- I'm not making this repl because I like you or anything!") 35 | console.log("don'... don't type ", ':help'.magenta.bold, ', okay? Idiot!') 36 | } 37 | 38 | console.log('') 39 | startRepl(); 40 | --------------------------------------------------------------------------------