├── .editorconfig ├── .github └── workflows │ ├── checks.yml │ ├── labels.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── eslint.config.js ├── examples ├── get_quote.ts ├── index.ts └── math.ts ├── index.ts ├── package.json ├── src ├── repl.ts └── types.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [**.min.js] 15 | indent_style = ignore 16 | insert_final_newline = ignore 17 | 18 | [MakeFile] 19 | indent_style = space 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | - push 4 | - pull_request 5 | - workflow_call 6 | 7 | jobs: 8 | test: 9 | uses: adonisjs/.github/.github/workflows/test.yml@main 10 | 11 | lint: 12 | uses: adonisjs/.github/.github/workflows/lint.yml@main 13 | 14 | typecheck: 15 | uses: adonisjs/.github/.github/workflows/typecheck.yml@main 16 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync labels 2 | on: 3 | workflow_dispatch: 4 | permissions: 5 | issues: write 6 | jobs: 7 | labels: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: EndBug/label-sync@v2 12 | with: 13 | config-file: 'https://raw.githubusercontent.com/thetutlage/static/main/labels.yml' 14 | delete-other-labels: true 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: workflow_dispatch 3 | permissions: 4 | contents: write 5 | id-token: write 6 | jobs: 7 | checks: 8 | uses: ./.github/workflows/checks.yml 9 | release: 10 | needs: checks 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | - name: git config 20 | run: | 21 | git config user.name "${GITHUB_ACTOR}" 22 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 23 | - name: Init npm config 24 | run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 25 | env: 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | - run: npm install 28 | - run: npm run release -- --ci 29 | env: 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 0 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | stale-issue-message: 'This issue has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still need help on this issue' 13 | stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request' 14 | close-issue-message: 'This issue has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still need help on this issue' 15 | close-pr-message: 'This pull request has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still intend to submit this pull request' 16 | days-before-stale: 21 17 | days-before-close: 5 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_STORE 4 | .nyc_output 5 | .idea 6 | .vscode/ 7 | *.sublime-project 8 | *.sublime-workspace 9 | *.log 10 | build 11 | dist 12 | package-lock.json 13 | yarn.lock 14 | shrinkwrap.yaml 15 | .env 16 | !test/fixtures/.env 17 | test/__app 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | README.md 3 | docs 4 | *.md 5 | *.html 6 | config.json 7 | .eslintrc.json 8 | package.json 9 | *.txt 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2022 Harminder Virk, contributors 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @adonisjs/repl 2 | 3 |
4 | 5 | [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] 6 | 7 | ## Introduction 8 | REPL for AdonisJS applications. Syntax highlighting, bracket matching, ESM and TypeScript support out of the box. 9 | 10 | ## Official Documentation 11 | The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/repl) 12 | 13 | ## Contributing 14 | One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. 15 | 16 | We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework. 17 | 18 | ## Code of Conduct 19 | In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). 20 | 21 | ## License 22 | AdonisJS Repl is open-sourced software licensed under the [MIT license](LICENSE.md). 23 | 24 | [gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/repl/checks.yml?style=for-the-badge 25 | [gh-workflow-url]: https://github.com/adonisjs/repl/actions/workflows/checks.yml "Github action" 26 | 27 | [npm-image]: https://img.shields.io/npm/v/@adonisjs/repl/latest.svg?style=for-the-badge&logo=npm 28 | [npm-url]: https://www.npmjs.com/package/@adonisjs/repl/v/latest "npm" 29 | 30 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript 31 | 32 | [license-url]: LICENSE.md 33 | [license-image]: https://img.shields.io/github/license/adonisjs/repl?style=for-the-badge 34 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { configPkg } from '@adonisjs/eslint-config' 2 | export default configPkg({ 3 | ignores: ['coverage'], 4 | }) 5 | -------------------------------------------------------------------------------- /examples/get_quote.ts: -------------------------------------------------------------------------------- 1 | export async function getQuote(): Promise { 2 | return 'I promise to share a quote' 3 | } 4 | -------------------------------------------------------------------------------- /examples/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/repl 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'node:path' 11 | import { homedir } from 'node:os' 12 | import { create } from 'ts-node-maintained' 13 | 14 | import { Repl } from '../src/repl.js' 15 | 16 | /** 17 | * A dummy database object 18 | */ 19 | const db = { 20 | query() { 21 | return this 22 | }, 23 | from(_table: string) { 24 | return this 25 | }, 26 | fetch: async () => { 27 | return [ 28 | { id: 1, name: 'virk' }, 29 | { id: 2, name: 'romain' }, 30 | ] 31 | }, 32 | } 33 | 34 | class User {} 35 | class Profile {} 36 | class Team {} 37 | class Account {} 38 | 39 | const models = { 40 | User, 41 | Team, 42 | Profile, 43 | Account, 44 | } 45 | 46 | const tsNode = create({ project: '../tsconfig.json' }) 47 | const compiler = { 48 | supportsTypescript: true, 49 | compile(code: string, fileName: string) { 50 | const output = tsNode.compile(code, fileName) 51 | return output 52 | .replace('export { };', '') 53 | .replace(/\/\/# sourceMappingURL=(.*)$/, '/** sourceMappingURL=$1 */') 54 | }, 55 | } 56 | 57 | new Repl({ 58 | compiler, 59 | historyFilePath: join(homedir(), '.adonis_repl_history'), 60 | }) 61 | .addMethod( 62 | 'getDb', 63 | function loadDatabase(repl) { 64 | repl.server!.context.db = db 65 | repl.notify( 66 | `Loaded database. You can access it using the ${repl.colors.underline('"db"')} property` 67 | ) 68 | }, 69 | { 70 | description: 'Loads database to the "db" property.', 71 | } 72 | ) 73 | .addMethod( 74 | 'getModels', 75 | (repl) => { 76 | repl.server!.context.models = models 77 | repl.notify('Loaded models. You can access them using the "models" property') 78 | repl.server!.displayPrompt() 79 | }, 80 | { 81 | description: 'Loads database to the "models" property.', 82 | } 83 | ) 84 | .start() 85 | -------------------------------------------------------------------------------- /examples/math.ts: -------------------------------------------------------------------------------- 1 | export default function () { 2 | throw new Error('foo') 3 | } 4 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/repl 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export { Repl } from './src/repl.js' 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adonisjs/repl", 3 | "description": "REPL for AdonisJS", 4 | "version": "4.1.0", 5 | "engines": { 6 | "node": ">=18.16.0" 7 | }, 8 | "type": "module", 9 | "files": [ 10 | "build", 11 | "!build/bin", 12 | "!build/examples" 13 | ], 14 | "exports": { 15 | ".": "./build/index.js", 16 | "./types": "./build/src/types.js" 17 | }, 18 | "scripts": { 19 | "pretest": "npm run lint", 20 | "test": "echo 'No tests'", 21 | "lint": "eslint .", 22 | "format": "prettier --write .", 23 | "typecheck": "tsc --noEmit", 24 | "clean": "del-cli build", 25 | "precompile": "npm run lint && npm run clean", 26 | "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", 27 | "build": "npm run compile", 28 | "version": "npm run build", 29 | "prepublishOnly": "npm run build", 30 | "release": "release-it" 31 | }, 32 | "devDependencies": { 33 | "@adonisjs/eslint-config": "^2.0.0-beta.7", 34 | "@adonisjs/prettier-config": "^1.4.0", 35 | "@adonisjs/tsconfig": "^1.4.0", 36 | "@release-it/conventional-changelog": "^10.0.0", 37 | "@swc/core": "^1.10.7", 38 | "@types/node": "^22.10.5", 39 | "del-cli": "^6.0.0", 40 | "eslint": "^9.18.0", 41 | "prettier": "^3.4.2", 42 | "release-it": "^18.1.1", 43 | "ts-node-maintained": "^10.9.4", 44 | "tsup": "^8.3.5", 45 | "typescript": "^5.7.3" 46 | }, 47 | "dependencies": { 48 | "@poppinss/colors": "^4.1.4", 49 | "string-width": "^7.2.0" 50 | }, 51 | "homepage": "https://github.com/adonisjs/repl#readme", 52 | "repository": { 53 | "type": "git", 54 | "url": "git+https://github.com/adonisjs/repl.git" 55 | }, 56 | "bugs": { 57 | "url": "https://github.com/adonisjs/repl/issues" 58 | }, 59 | "keywords": [ 60 | "adonisjs", 61 | "repl", 62 | "node-repl" 63 | ], 64 | "author": "Harminder Virk ", 65 | "license": "MIT", 66 | "publishConfig": { 67 | "access": "public", 68 | "provenance": true 69 | }, 70 | "tsup": { 71 | "entry": [ 72 | "./index.ts", 73 | "./src/types.ts" 74 | ], 75 | "outDir": "./build", 76 | "clean": true, 77 | "format": "esm", 78 | "dts": false, 79 | "sourcemap": false, 80 | "target": "esnext" 81 | }, 82 | "release-it": { 83 | "git": { 84 | "requireCleanWorkingDir": true, 85 | "requireUpstream": true, 86 | "commitMessage": "chore(release): ${version}", 87 | "tagAnnotation": "v${version}", 88 | "push": true, 89 | "tagName": "v${version}" 90 | }, 91 | "github": { 92 | "release": true 93 | }, 94 | "npm": { 95 | "publish": true, 96 | "skipChecks": true 97 | }, 98 | "plugins": { 99 | "@release-it/conventional-changelog": { 100 | "preset": { 101 | "name": "angular" 102 | } 103 | } 104 | } 105 | }, 106 | "prettier": "@adonisjs/prettier-config" 107 | } 108 | -------------------------------------------------------------------------------- /src/repl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/repl 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import stringWidth from 'string-width' 11 | import useColors from '@poppinss/colors' 12 | import type { Colors } from '@poppinss/colors/types' 13 | import { inspect, promisify as utilPromisify } from 'node:util' 14 | import { type REPLServer, Recoverable, type ReplOptions, start as startRepl } from 'node:repl' 15 | 16 | import type { MethodCallback, MethodOptions, Compiler } from './types.js' 17 | 18 | /** 19 | * List of node global properties to remove from the 20 | * ls inspect 21 | */ 22 | const GLOBAL_NODE_PROPERTIES = [ 23 | 'performance', 24 | 'global', 25 | 'clearInterval', 26 | 'clearTimeout', 27 | 'setInterval', 28 | 'setTimeout', 29 | 'queueMicrotask', 30 | 'clearImmediate', 31 | 'setImmediate', 32 | 'structuredClone', 33 | 'atob', 34 | 'btoa', 35 | 'fetch', 36 | 'crypto', 37 | 'navigator', 38 | ] 39 | 40 | const TS_UTILS_HELPERS = [ 41 | '__extends', 42 | '__assign', 43 | '__rest', 44 | '__decorate', 45 | '__param', 46 | '__esDecorate', 47 | '__runInitializers', 48 | '__propKey', 49 | '__setFunctionName', 50 | '__metadata', 51 | '__awaiter', 52 | '__generator', 53 | '__exportStar', 54 | '__createBinding', 55 | '__values', 56 | '__read', 57 | '__spread', 58 | '__spreadArrays', 59 | '__spreadArray', 60 | '__await', 61 | '__asyncGenerator', 62 | '__asyncDelegator', 63 | '__asyncValues', 64 | '__makeTemplateObject', 65 | '__importStar', 66 | '__importDefault', 67 | '__classPrivateFieldGet', 68 | '__classPrivateFieldSet', 69 | '__classPrivateFieldIn', 70 | ] 71 | 72 | export class Repl { 73 | #replOptions: ReplOptions 74 | 75 | /** 76 | * Length of the longest custom method name. We need to show a 77 | * symmetric view of custom methods and their description 78 | */ 79 | #longestCustomMethodName = 0 80 | 81 | /** 82 | * Reference to the original `eval` method of the repl server. 83 | * Since we are monkey patching it, we need a reference to it 84 | * to call it after our custom logic 85 | */ 86 | #originalEval?: Function 87 | 88 | /** 89 | * Compiler that will transform the user input just 90 | * before evaluation 91 | */ 92 | #compiler?: Compiler 93 | 94 | /** 95 | * Path to the history file 96 | */ 97 | #historyFilePath?: string 98 | 99 | /** 100 | * Set of registered ready callbacks 101 | */ 102 | #onReadyCallbacks: ((repl: Repl) => void)[] = [] 103 | 104 | /** 105 | * A set of registered custom methods 106 | */ 107 | #customMethods: { 108 | [name: string]: { handler: MethodCallback; options: MethodOptions & { width: number } } 109 | } = {} 110 | 111 | /** 112 | * Colors reference 113 | */ 114 | colors: Colors = useColors.ansi() 115 | 116 | /** 117 | * Reference to the repl server. Available after the `start` method 118 | * is invoked 119 | */ 120 | server?: REPLServer 121 | 122 | constructor(options?: { compiler?: Compiler; historyFilePath?: string } & ReplOptions) { 123 | const { compiler, historyFilePath, ...rest } = options || {} 124 | this.#compiler = compiler 125 | this.#historyFilePath = historyFilePath 126 | this.#replOptions = rest 127 | } 128 | 129 | /** 130 | * Registering custom methods with the server context by wrapping 131 | * them inside a function and passes the REPL server instance 132 | * to the method 133 | */ 134 | #registerCustomMethodWithContext(name: string) { 135 | const customMethod = this.#customMethods[name] 136 | if (!customMethod) { 137 | return 138 | } 139 | 140 | /** 141 | * Wrap handler 142 | */ 143 | const handler = (...args: any[]) => customMethod.handler(this, ...args) 144 | 145 | /** 146 | * Re-define the function name to be more description 147 | */ 148 | Object.defineProperty(handler, 'name', { value: customMethod.handler.name }) 149 | 150 | /** 151 | * Register with the context 152 | */ 153 | this.server!.context[name] = handler 154 | } 155 | 156 | /** 157 | * Setup context with default globals 158 | */ 159 | #setupContext() { 160 | /** 161 | * Register "clear" method 162 | */ 163 | this.addMethod( 164 | 'clear', 165 | function clear(repl: Repl, key: string) { 166 | if (!key) { 167 | console.log(repl.colors.red('Define a property name to remove from the context')) 168 | } else { 169 | delete repl.server!.context[key] 170 | } 171 | repl.server!.displayPrompt() 172 | }, 173 | { 174 | description: 'Clear a property from the REPL context', 175 | usage: `clear ${this.colors.gray('(propertyName)')}`, 176 | } 177 | ) 178 | 179 | /** 180 | * Register "p" method 181 | */ 182 | this.addMethod( 183 | 'p', 184 | function promisify(_: Repl, fn: Function) { 185 | return utilPromisify(fn) 186 | }, 187 | { 188 | description: 'Promisify a function. Similar to Node.js "util.promisify"', 189 | usage: `p ${this.colors.gray('(function)')}`, 190 | } 191 | ) 192 | 193 | /** 194 | * Register all custom methods with the context 195 | */ 196 | Object.keys(this.#customMethods).forEach((name) => { 197 | this.#registerCustomMethodWithContext(name) 198 | }) 199 | } 200 | 201 | /** 202 | * Find if the error is recoverable or not 203 | */ 204 | #isRecoverableError(error: any) { 205 | return /^(Unexpected end of input|Unexpected token|' expected)/.test(error.message) 206 | } 207 | 208 | /** 209 | * Custom eval method to execute the user code 210 | * 211 | * Basically we are monkey patching the original eval method, because 212 | * we want to: 213 | * - Compile the user code before executing it 214 | * - And also benefit from the original eval method that supports 215 | * cool features like top level await 216 | */ 217 | #eval( 218 | code: string, 219 | context: any, 220 | filename: string, 221 | callback: (err: Error | null, result?: any) => void 222 | ) { 223 | try { 224 | const compiled = this.#compiler ? this.#compiler!.compile(code, filename) : code 225 | return this.#originalEval!(compiled, context, filename, callback) 226 | } catch (error) { 227 | if (this.#isRecoverableError(error)) { 228 | callback(new Recoverable(error), null) 229 | return 230 | } 231 | 232 | callback(error, null) 233 | } 234 | } 235 | 236 | /** 237 | * Setup history file 238 | */ 239 | #setupHistory() { 240 | if (!this.#historyFilePath) { 241 | return 242 | } 243 | 244 | this.server!.setupHistory(this.#historyFilePath, (error) => { 245 | if (!error) { 246 | return 247 | } 248 | 249 | console.log(this.colors.red('Unable to write to the history file. Exiting')) 250 | console.error(error) 251 | process.exit(1) 252 | }) 253 | } 254 | 255 | /** 256 | * Prints the help for the context properties 257 | */ 258 | #printContextHelp() { 259 | /** 260 | * Print context properties 261 | */ 262 | console.log('') 263 | console.log(this.colors.green('CONTEXT PROPERTIES/METHODS:')) 264 | 265 | const context = Object.keys(this.server!.context).reduce( 266 | (result, key) => { 267 | if ( 268 | !this.#customMethods[key] && 269 | !GLOBAL_NODE_PROPERTIES.includes(key) && 270 | !TS_UTILS_HELPERS.includes(key) 271 | ) { 272 | result[key] = this.server!.context[key] 273 | } 274 | 275 | return result 276 | }, 277 | {} as Record 278 | ) 279 | 280 | console.log(inspect(context, false, 1, true)) 281 | } 282 | 283 | /** 284 | * Prints the help for the custom methods 285 | */ 286 | #printCustomMethodsHelp() { 287 | /** 288 | * Print loader methods 289 | */ 290 | console.log('') 291 | console.log(this.colors.green('GLOBAL METHODS:')) 292 | 293 | Object.keys(this.#customMethods).forEach((method) => { 294 | const { options } = this.#customMethods[method] 295 | 296 | const usage = this.colors.yellow(options.usage || method) 297 | const spaces = ' '.repeat(this.#longestCustomMethodName - options.width + 2) 298 | const description = this.colors.dim(options.description || '') 299 | 300 | console.log(`${usage}${spaces}${description}`) 301 | }) 302 | } 303 | 304 | /** 305 | * Prints the context to the console 306 | */ 307 | #ls() { 308 | this.#printCustomMethodsHelp() 309 | this.#printContextHelp() 310 | this.server!.displayPrompt() 311 | } 312 | 313 | /** 314 | * Notify by writing to the console 315 | */ 316 | notify(message: string) { 317 | console.log(this.colors.yellow().italic(message)) 318 | if (this.server) { 319 | this.server.displayPrompt() 320 | } 321 | } 322 | 323 | /** 324 | * Register a callback to be invoked once the server is ready 325 | */ 326 | ready(callback: (repl: Repl) => void): this { 327 | this.#onReadyCallbacks.push(callback) 328 | return this 329 | } 330 | 331 | /** 332 | * Register a custom loader function to be added to the context 333 | */ 334 | addMethod(name: string, handler: MethodCallback, options?: MethodOptions): this { 335 | const width = stringWidth(options?.usage || name) 336 | if (width > this.#longestCustomMethodName) { 337 | this.#longestCustomMethodName = width 338 | } 339 | 340 | this.#customMethods[name] = { handler, options: Object.assign({ width }, options) } 341 | 342 | /** 343 | * Register method right away when server has been started 344 | */ 345 | if (this.server) { 346 | this.#registerCustomMethodWithContext(name) 347 | } 348 | 349 | return this 350 | } 351 | 352 | /** 353 | * Returns the collection of registered methods 354 | */ 355 | getMethods() { 356 | return this.#customMethods 357 | } 358 | 359 | /** 360 | * Register a compiler. Make sure register the compiler before 361 | * calling the start method 362 | */ 363 | useCompiler(compiler: Compiler): this { 364 | this.#compiler = compiler 365 | return this 366 | } 367 | 368 | /** 369 | * Start the REPL server 370 | */ 371 | start(context?: Record) { 372 | console.log('') 373 | this.notify('Type ".ls" to a view list of available context methods/properties') 374 | 375 | this.server = startRepl({ 376 | prompt: `> ${this.#compiler?.supportsTypescript ? '(ts) ' : '(js) '}`, 377 | input: process.stdin, 378 | output: process.stdout, 379 | terminal: process.stdout.isTTY && !Number.parseInt(process.env.NODE_NO_READLINE!, 10), 380 | useGlobal: true, 381 | ...this.#replOptions, 382 | }) 383 | 384 | /** 385 | * Share context with the server 386 | */ 387 | if (context) { 388 | Object.keys(context).forEach((key) => { 389 | this.server!.context[key] = context[key] 390 | }) 391 | } 392 | 393 | /** 394 | * Define the `ls` command 395 | */ 396 | this.server!.defineCommand('ls', { 397 | help: 'View list of available context methods/properties', 398 | action: this.#ls.bind(this), 399 | }) 400 | 401 | /** 402 | * Setup context and history 403 | */ 404 | this.#setupContext() 405 | this.#setupHistory() 406 | 407 | /** 408 | * Monkey patch the eval method 409 | */ 410 | this.#originalEval = this.server.eval 411 | // @ts-ignore 412 | this.server.eval = this.#eval.bind(this) 413 | 414 | /** 415 | * Display prompt 416 | */ 417 | this.server.displayPrompt() 418 | 419 | /** 420 | * Execute onReady callbacks 421 | */ 422 | this.#onReadyCallbacks.forEach((callback) => callback(this)) 423 | 424 | return this 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/repl 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Repl } from './repl.js' 11 | 12 | /** 13 | * Custom method callback function 14 | */ 15 | export type MethodCallback = (repl: Repl, ...args: any[]) => any 16 | 17 | /** 18 | * Options that can be set when defining a custom method 19 | */ 20 | export type MethodOptions = { 21 | description?: string 22 | usage?: string 23 | } 24 | 25 | /** 26 | * Shape of the Compiler that must be passed to the 27 | * repl constructor 28 | */ 29 | export type Compiler = { 30 | compile: (code: string, fileName: string) => string 31 | supportsTypescript: boolean 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@adonisjs/tsconfig/tsconfig.package.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "./build" 6 | } 7 | } 8 | --------------------------------------------------------------------------------