├── .gitignore
├── bin
└── tsun
├── screenshot
├── block.png
├── color.png
├── paste.png
├── type.png
└── completion.png
├── src
├── util.ts
├── node-color-readline.d.ts
├── register.ts
├── executor.ts
├── service.ts
└── repl.ts
├── .editorconfig
├── tsconfig.json
├── package.json
├── tsun.ts
├── README.md
└── pnpm-lock.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/bin/tsun:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | require('./tsun.js')
3 |
--------------------------------------------------------------------------------
/screenshot/block.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/HEAD/screenshot/block.png
--------------------------------------------------------------------------------
/screenshot/color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/HEAD/screenshot/color.png
--------------------------------------------------------------------------------
/screenshot/paste.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/HEAD/screenshot/paste.png
--------------------------------------------------------------------------------
/screenshot/type.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/HEAD/screenshot/type.png
--------------------------------------------------------------------------------
/screenshot/completion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HerringtonDarkholme/typescript-repl/HEAD/screenshot/completion.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
44 |
45 | Tab-completion
46 | 
47 |
48 | Multiple Line Editing, typing double blank lines will escape from Multiple line mode
49 | 
50 |
51 | Paste Mode
52 | 
53 |
54 | Definition Lookup
55 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 = [require.resolve('@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/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 |
--------------------------------------------------------------------------------