Skip to content

Instantly share code, notes, and snippets.

@tcodes0
Last active July 8, 2020 19:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tcodes0/b19e2faa970b7c98352c69758343ffd1 to your computer and use it in GitHub Desktop.
Save tcodes0/b19e2faa970b7c98352c69758343ffd1 to your computer and use it in GitHub Desktop.
Awesome Typescript REPL
import decache from 'decache'
import { watch, FSWatcher } from 'fs'
import REPL from 'repl'
type ReloadConfig = { newline?: boolean }
type JSObject<Value = any> = Record<string, Value>
const AUTO_RELOAD = true
/**
* path to the modules you'll be developing and reloading.
* [0] path
* [1] lib
* [2] contextKey
* [3] greeting
*/
const CONFIG = [
['../module_one', '@namespace/module_one', 'one', 'global.one \t module_one package'],
['../module_two', '@namespace/module_two', 'two', 'global.two \t module_two package'],
['../module_three', '@namespace/module_three', 'three', 'global.three \t module_three package'],
]
let lastReload = { ok: true, error: null }
const prompt = `> `
process.env.NODE_ENV = 'REPL'
const tsRepl = REPL.start({
prompt,
replMode: REPL.REPL_MODE_STRICT,
})
/**
* Wrapper on console.log for manipulating REPL output before printing
*/
const print = function (this: REPL.REPLServer, message: string | string[]) {
/**
* Repl is available as 'this'.
* use console.log to print messages, end with this.write('\n') to avoid the cursor being stuck.
*/
let padding = ' '
let prefix = '\nINFO: '
if (typeof message === 'string') {
console.log(`${padding}${prefix}${message}`)
} else {
let output = `${padding}${prefix}`
for (const line of message) {
output += line + '\n' + padding
}
console.log(output)
}
this.write('\n')
return
}
/**
* Curried function that reloads a module the REPL imports.
* Used by auto-reloading
*/
const reload = function (repl: REPL.REPLServer, modulePath: string, contextKey: string, config: ReloadConfig = {}) {
decache(modulePath)
try {
/**
* reload module, this may throw.
* It normally throws because you didn't finish typing and the code reloaded
* leaving it with broken syntax
*/
repl.context[contextKey] = require(modulePath)
if (config.newline) {
repl.write('\n')
}
lastReload.ok = true
lastReload.error = null
} catch (error) {
/**
* set the lastReload object to fail state
* capture error to inspection if desired
* "why isn't my code reloading"?
* maybe it is, just has an error!
*/
lastReload.ok = false
lastReload.error = error
}
}
const greetings = ['Welcome to the namespace REPL', '.help \t\t see commands']
/**
* reload all modules in REPL
*/
function reloadAll(repl: REPL.REPLServer, input?: string, config?: ReloadConfig) {
if (input) {
print.call(repl, 'Reload global.namespace code')
return
}
CONFIG.forEach(([_path, lib, contextKey, greeting]) => {
if (!greetings.includes(greeting)) {
greetings.push(greeting)
}
reload(tsRepl, lib, contextKey)
})
if (config?.newline) {
tsRepl.write('\n')
}
}
if (AUTO_RELOAD) {
const watchers = CONFIG.reduce<JSObject<FSWatcher>>((acc, [path, lib, contextKey]) => {
if (!acc[lib]) {
acc[lib] = watch(path, { recursive: true }, (_event, _filename) => {
// console.log({ watcher: { _event, _filename, lib, contextKey } })
reload(tsRepl, lib, contextKey)
})
}
return acc
}, {})
/**
* push watchers into context to allow inspection/manipulation
*/
tsRepl.context.watchers = watchers
greetings.push('global.watchers \t auto-reload watchers, if you need them')
tsRepl.context.__history = tsRepl.context.__h = []
tsRepl.context.__ = 'nothing here yet!'
/**
* save or s helper to unwrap promises
*/
tsRepl.context.save = tsRepl.context.s = function save<T = any>(prom: Promise<T> | unknown) {
const _save = result => {
tsRepl.context.__history.push(result)
tsRepl.context.__ = result
}
if (!prom) {
return _save(prom)
}
if (!(prom as JSObject).then) {
_save(prom)
} else {
;(prom as Promise<T>).then(_save).catch(_save)
}
}
greetings.push('global.s \t\t saves results, also unwraps promises. Aliases: save')
greetings.push('global.__ \t last saved result')
greetings.push('global.__h \t history of all saved results. Aliases: __history')
tsRepl.defineCommand('lastReload', {
help: 'Check reload Status',
action: function () {
console.log(lastReload)
this.write('\n')
},
})
greetings.push('.lastReload \t last reload status')
}
tsRepl.on('exit', () => {
console.log('Bye :)')
tsRepl.write('\n')
process.nextTick(() => {
process.exit(0)
})
})
/**
* careful here to use a unique file path
*/
tsRepl.setupHistory(
`${process.env.HOME}/.namespace_repl_node_history`,
err => err && print.call(tsRepl, ['Repl history error:', err.message])
)
tsRepl.defineCommand('r', {
help: 'Reload namespace',
action: input => reloadAll(tsRepl, input, { newline: true }),
})
greetings.push('.r \t\t reload manually')
tsRepl.defineCommand('e', {
help: 'Exit Repl',
action: tsRepl.close,
})
greetings.push('.e or .exit \t leave :(')
tsRepl.defineCommand('c', {
help: 'See available code on context',
action: _input => {
const message = CONFIG.map(c => c[3])
print.call(tsRepl, message)
},
})
greetings.push('.c \t\t See available code on context')
reloadAll(tsRepl)
print.call(tsRepl, greetings)
@tcodes0
Copy link
Author

tcodes0 commented May 18, 2020

package.json

{
  "name": "@namespace/repl",
  "version": "0.0.1",
  "license": "MIT",
  "main": "index.js",
  "module": "index.ts",
  "scripts": {
    "start": "yarn ts-node --transpile-only -r tsconfig-paths/register index.ts",
    "startComment": [
      "see https://github.com/TypeStrong/ts-node/issues/1007 for esm support, but it breaks code-reload",
      "node --loader ts-node/esm --experimental-specifier-resolution=node --experimental-top-level-await --no-warnings index.ts --transpile-only -r tsconfig-paths/register index.ts"
    ]
  },
  "dependencies": {
    "@namespace/module_one": "0.0.1",
    "@namespace/module_two": "0.0.1",
    "@namespace/module_three": "0.0.1",
    "decache": "4.6.0",
    "ts-node": "8.10.2",
    "tsconfig-paths": "3.9.0"
  }
}

@tcodes0
Copy link
Author

tcodes0 commented May 18, 2020

tsconfig.json (module)

{
  "compilerOptions": {
    "rootDir": ".",
  },
  "extends": "../../tsconfig.json",
  "exclude": ["node_modules"],
  "include": [".", "index.ts"]
}

@tcodes0
Copy link
Author

tcodes0 commented May 18, 2020

tsconfig.json (monorepo root)

{
  "compilerOptions": {
    /* Basic Options */
    "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
    "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
    "moduleResolution": "node",
    "lib": [
      /* Specify library files to be included in the compilation. */
      "esnext",
      "dom",
      "dom.iterable"
    ],
    "allowJs": true /* Allow javascript files to be compiled. */,
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
    "declaration": true /* Generates corresponding '.d.ts' file. */,
    "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
    "sourceMap": true /* Generates corresponding '.map' file. */,
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    "outDir": "./build" /* Redirect output structure to the directory. */,
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true /* Do not emit outputs. */,
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true /* Enable all strict type-checking options. */,
    "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    "noUnusedLocals": false /* Report errors on unused locals. */,
    "noUnusedParameters": false /* Report errors on unused parameters. */,
    "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
    "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,

    /* Module Resolution Options   */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": "packages" /* Base directory to resolve non-absolute module names. */,
    "paths": {
      "module_one/*": ["module_one/src/*"]
    } /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */,
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    "typeRoots": ["@types", "node_modules/@types"] /* List of folders to include type definitions from. */,
    // "types": ["@types/*"],                           /* Type declaration files to be included in compilation. */
    "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
    "resolveJsonModule": true,
    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
    "skipLibCheck": true,
    // see https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#type-only-modules-watching
    "importsNotUsedAsValues": "preserve",
    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
  },
  "exclude": ["node_modules", "build"],
  "include": ["packages"]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment