├── .babelrc ├── .browserslistrc ├── .eslintrc.json ├── .github ├── main.workflow └── workflows │ └── nodejs.yml ├── .gitignore ├── .madrun.mjs ├── .npmignore ├── .nycrc.json ├── ChangeLog ├── LICENSE ├── README.md ├── bin └── gritty.js ├── client ├── get-el.js ├── get-env.js ├── get-host.js └── gritty.js ├── css └── style.css ├── help.json ├── img ├── linux.png └── windows.png ├── index.html ├── package.json ├── server └── gritty.js ├── test ├── before.js ├── client │ ├── get-el.js │ ├── get-env.js │ ├── get-host.js │ └── gritty.js └── server │ └── gritty.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 Chrome versions 2 | last 2 Safari versions 3 | Firefox ESR 4 | not dead 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:putout/recommended" 4 | ], 5 | "plugins": [ 6 | "putout", 7 | "n" 8 | ], 9 | "overrides": [{ 10 | "files": ["bin/**/*.js"], 11 | "rules": { 12 | "n/no-console": 0, 13 | "n/no-process-exit": 0 14 | }, 15 | "extends": [ 16 | "plugin:n/recommended" 17 | ] 18 | }, { 19 | "files": ["client/**/*.js"], 20 | "env": { 21 | "browser": true 22 | } 23 | }] 24 | } 25 | -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "Push" { 2 | resolves = ["lint", "coverage"] 3 | on = "push" 4 | } 5 | 6 | action "lint" { 7 | uses = "gimenete/eslint-action@1.0" 8 | } 9 | 10 | action "coverage" { 11 | uses = "coverallsapp/github-action@v1.0.1" 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: 11 | - 18.x 12 | - 20.x 13 | - 22.x 14 | - 23.x 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: oven-sh/setup-bun@v1 18 | with: 19 | bun-version: latest 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install Redrun 25 | run: bun i redrun -g --no-save 26 | - name: Install 27 | run: bun i --no-save 28 | - name: Lint 29 | run: redrun lint 30 | - name: Coverage 31 | run: redrun coverage report 32 | - name: Coveralls 33 | uses: coverallsapp/github-action@v2 34 | continue-on-error: true 35 | with: 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | npm-debug.log 4 | .nyc_output 5 | yarn-error.log 6 | coverage 7 | 8 | dist 9 | dist-dev 10 | 11 | *.swp 12 | 13 | .idea 14 | -------------------------------------------------------------------------------- /.madrun.mjs: -------------------------------------------------------------------------------- 1 | import {run, cutEnv} from 'madrun'; 2 | 3 | const SUPERTAPE_TIMEOUT = 15_000; 4 | 5 | export default { 6 | 'start': () => 'node bin/gritty', 7 | 'start:dev': () => 'NODE_ENV=development npm start', 8 | 'lint': () => 'putout .', 9 | 'fresh:lint': () => run('lint', '--fresh'), 10 | 'lint:fresh': () => run('lint', '--fresh'), 11 | 'fix:lint': () => run('lint', '--fix'), 12 | 'watch:test': () => run('watcher', 'npm test'), 13 | 'watcher': () => 'nodemon -w test -w client -w server --exec', 14 | 'build-progress': () => 'webpack --progress', 15 | '6to5:client': () => run('build-progress', '--mode production'), 16 | '6to5:client:dev': async () => `NODE_ENV=development ${await run('build-progress', '--mode development')}`, 17 | 'watch:client': () => run('6to5:client', '--watch'), 18 | 'watch:client:dev': () => run('6to5:client:dev', '--watch'), 19 | 'wisdom': () => run('build'), 20 | 'build': () => run('6to5:*'), 21 | 'build:start': () => run(['build:client', 'start']), 22 | 'build:start:dev': () => run(['build:client:dev', 'start:dev']), 23 | 'build:client': () => run('6to5:client'), 24 | 'build:client:dev': () => run('6to5:client:dev'), 25 | 'watch:lint': async () => `nodemon -w client -w server -w webpack.config.js -x ${await run('lint')}`, 26 | 'report': () => 'c8 report --reporter=lcov', 27 | 28 | 'coverage': async () => [`c8 ${await cutEnv('test')}`, { 29 | SUPERTAPE_TIMEOUT, 30 | }], 31 | 32 | 'test': () => [`tape 'test/**/*.js'`, { 33 | SUPERTAPE_TIMEOUT, 34 | }], 35 | }; 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | 3 | webpack.config.js 4 | client 5 | test 6 | yarn-error.log 7 | coverage 8 | 9 | img 10 | 11 | *.swp 12 | 13 | *.config.* 14 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | "bin", 6 | "coverage", 7 | "*.js", 8 | "dist**", 9 | "**/*.spec.js", 10 | "**/fixture", 11 | "test", 12 | ".*.{js,mjs}", 13 | "**/*.config.*" 14 | ], 15 | "branches": 100, 16 | "lines": 100, 17 | "functions": 100, 18 | "statements": 100 19 | } 20 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2024.03.18, v8.1.2 2 | 3 | fix: 4 | - c630120 gritty: get back node-pty 5 | 6 | 2024.03.16, v8.1.1 7 | 8 | fix: 9 | - 92d7298 gritty: drop node-pty 10 | 11 | 2024.03.16, v8.1.0 12 | 13 | feature: 14 | - 72e34e0 gritty: serve-once v3.0.0 15 | 16 | 2024.03.12, v8.0.0 17 | 18 | feature: 19 | - f1b8576 gritty: drop support of node < 18 20 | - 6c1356c gritty: madrun v10.0.1 21 | - 0b9fb47 gritty: css-minimizer-webpack-plugin v6.0.0 22 | - 7475087 gritty: supertape v10.4.0 23 | - 3d8ca38 gritty: eslint-plugin-putout v22.4.1 24 | - 5b36dc7 gritty: putout v35.7.3 25 | - edddf41 gritty: xterm-addon-webgl v0.16.0 26 | - 4d813b6 gritty: xterm-addon-fit v0.8.0 27 | - 8b8aef7 gritty: c8 v9.1.0 28 | 29 | 2023.08.07, v7.2.0 30 | 31 | feature: 32 | - 3990464 package: css-minimizer-webpack-plugin v5.0.1 33 | - 6e4af5c package: eslint-plugin-n v16.0.1 34 | - e73b2d9 package: nodemon v3.0.1 35 | - 9802f3a package: webpack-cli v5.1.4 36 | - fe2b204 package: babel-loader v9.1.3 37 | - 675b292 package: c8 v8.0.1 38 | - 5153c28 package: xterm-addon-webgl v0.15.0 39 | - 7301a7d package: xterm-addon-fit v0.7.0 40 | - 6dfbb61 package: eslint-plugin-putout v19.0.3 41 | - ef0d9e6 package: putout v31.0.3 42 | - 9a3e5fd package: node-pty v1.0.0 43 | 44 | 2022.09.19, v7.1.0 45 | 46 | feature: 47 | - package: xterm-addon-webgl v0.13.0 48 | - package: xterm-addon-fit v0.6.0 49 | - package: supertape v8.1.0 50 | - package: xterm v5.0.0 51 | 52 | 2022.07.23, v7.0.0 53 | 54 | feature: 55 | - package: css-minimizer-webpack-plugin v4.0.0 56 | - (gritty) drop support of node < 16 57 | - (package) madrun v9.0.6 58 | - (package) eslint-plugin-putout v16.0.0 59 | - (package) putout v27.0.1 60 | - (package) supertape v7.6.0 61 | - (package) xterm-addon-webgl v0.12.0 62 | - (package) @iocmd/wait v2.1.0 63 | - (package) yargs-parser v21.0.1 64 | 65 | 2021.11.02, v6.1.0 66 | 67 | feature: 68 | - (package) eslint-plugin-putout v11.0.0 69 | - (package) clean-css-loader v4.1.1 70 | - (package) optimize-css-assets-webpack-plugin v6.0.1 71 | - (package) xterm v4.14.1 72 | - (package) supertape v6.10.0 73 | - (package) putout v21.1.0 74 | 75 | 76 | 2021.08.15, v6.0.6 77 | 78 | feature: 79 | - (package) es6-promisify v7.0.0 80 | 81 | 82 | 83 | 2021.08.15, v6.0.5 84 | 85 | feature: 86 | - (package) eslint v8.0.0-beta.0 87 | - (package) eslint-plugin-putout v9.0.1 88 | - (package) css-loader v6.2.0 89 | - (package) css-minimizer-webpack-plugin v3.0.2 90 | - (package) style-loader v3.2.1 91 | 92 | 93 | 94 | 2021.06.12, v6.0.4 95 | 96 | feature: 97 | - (package) xterm-addon-webgl v0.11.1 98 | - (package) putout v18.0.2 99 | - (package) xterm v4.13.0 100 | - (package) es6-promisify v7.0.0 101 | - (gritty) optimize-css-assets-webpack-plugin -> css-minimizer-webpack-plugin 102 | - (package) putout v16.10.1 103 | 104 | 105 | 2021.03.16, v6.0.3 106 | 107 | feature: 108 | - (package) socket.io-client v4.0.0 109 | - (package) socket.io v4.0.0 110 | - (package) supertape v5.1.0 111 | - (package) putout v15.7.2 112 | - (package) xerm v4.11.0 113 | - (package) xterm-addon-webgl v0.10.0 114 | 115 | 116 | 2021.02.04, v6.0.2 117 | 118 | fix: 119 | - (gritty) client: throw when minimized 120 | 121 | 122 | 2021.02.04, v6.0.1 123 | 124 | feature: 125 | - (package) node-pty v0.10.0 126 | - (package) xterm v4.10.0 127 | - (package) xterm-addon-fit v0.5.0 128 | - (package) clean-css-loader v3.0.0 129 | - (package) eslint-plugin-putout v7.0.0 130 | - (package) putout v14.0.0 131 | - (package) putout v13.7.0 132 | - (package) madrun v8.6.0 133 | - (package) supertape v4.7.0 134 | 135 | 136 | 2020.11.16, v6.0.0 137 | 138 | feature: 139 | - (gritty) client: dist -> destructuring 140 | - (gritty) drop support of socket.request.env 141 | - (package) socket.io-client v3.0.1 142 | - (package) socket.io v3.0.1 143 | - (package) eslint-plugin-putout v6.0.1 144 | - (package) putout v11.0.4 145 | - (webpack) v5 146 | - (package) webpack v5.1.3 147 | - (package) css-loader v5.0.0 148 | - (package) webpack-cli v4.0.0 149 | - (package) style-loader v2.0.0 150 | - (package) putout v10.0.3 151 | 152 | 153 | 2020.09.11, v5.0.0 154 | 155 | feature: 156 | - (gritty) drop support of node < 12 157 | - (package) yargs-parser v20.0.0 158 | 159 | 160 | 2020.09.09, v4.8.16 161 | 162 | feature: 163 | - (package) xterm-addon-webgl v0.9.0 164 | - (package) xterm v4.9.0 165 | 166 | 167 | 2020.08.13, v4.8.15 168 | 169 | feature: 170 | - (package) css-loader v4.2.1 171 | - (package) yargs-parser v19.0.1 172 | 173 | 174 | 2020.07.23, v4.8.14 175 | 176 | feature: 177 | - (package) xterm v4.8.1 178 | - (package) putout v9.0.0 179 | - (package) madrun v7.0.0 180 | - (package) eslint-plugin-putout v5.0.0 181 | 182 | 183 | 2020.06.20, v4.8.13 184 | 185 | feature: 186 | - (package) xterm v4.7.0 187 | - (package) xterm-addon-webgl v0.8.0 188 | 189 | 190 | 2020.05.31, v4.8.12 191 | 192 | feature: 193 | - (package) xterm-addon-webgl v0.7.0 194 | - (package) xterm-addon-fit v0.4.0 195 | - (package) xterm v4.6.0 196 | - (package) eslint v7.0.0 197 | - (package) @cloudcmd/stub v3.0.0 198 | - (package) supertape v2.0.1 199 | - (package) madrun v6.0.0 200 | - (package) putout v8.0.2 201 | - (package) eslint-plugin-putout v4.0.1 202 | 203 | 204 | 2020.04.14, v4.8.11 205 | 206 | feature: 207 | - (gritty) add webgl back 208 | - (package) xterm v4.5.0 209 | 210 | 211 | 2020.03.24, v4.8.10 212 | 213 | fix: 214 | - (gritty) webgl (#10) 215 | 216 | 217 | 2020.03.15, v4.8.9 218 | 219 | feature: 220 | - (package) xterm-addon-webgl v0.5.1 221 | - (package) yargs-parser v18.1.0 222 | - (package) serve-once v2.0.0 223 | 224 | 225 | 2020.02.17, v4.8.8 226 | 227 | feature: 228 | - (package) yargs-parser v17.0.0 229 | 230 | 231 | 2020.02.05, v4.8.7 232 | 233 | feature: 234 | - (package) xterm v4.4.0 235 | - (package) xterm-addon-webgl v0.5.0 236 | 237 | 238 | 2019.12.30, v4.8.6 239 | 240 | feature: 241 | - (package) xterm v4.3.0 (https://github.com/xtermjs/xterm.js/releases/tag/4.3.0) 242 | - (package) eslint-plugin-node v11.0.0 243 | - (package) nyc v15.0.0 244 | - (package) nodemon v2.0.1 245 | - (package) madrun v5.0.0 246 | - (package) putout v7.0.2 247 | - (package) eslint-plugin-putout v3.0.0 248 | 249 | 250 | 2019.10.27, v4.8.5 251 | 252 | feature: 253 | - (package) madrun v4.1.1 254 | - (package) xterm-addon-fit v0.3.0 255 | - (package) xterm v4.2.0 256 | - (package) yargs-parser v16.0.0 257 | 258 | 259 | 2019.10.16, v4.8.4 260 | 261 | feature: 262 | - (package) xterm v4.1.0 263 | - (package) yargs-parser v15.0.0 264 | 265 | 266 | 2019.09.25, v4.8.3 267 | 268 | feature: 269 | - (package) xterm v4.0.2 270 | 271 | 272 | 2019.09.19, v4.8.2 273 | 274 | feature: 275 | - (package) xterm v4.0.1 276 | - (package) putout v6.1.0 277 | - (package) currify v4.0.0 278 | 279 | 280 | 2019.09.09, v4.8.1 281 | 282 | feature: 283 | - (package) wraptile v3.0.0 284 | - (package) yargs-parser v14.0.0 285 | - (package) eslint-plugin-node v10.0.0 286 | 287 | 288 | 2019.09.04, v4.8.0 289 | 290 | feature: 291 | - (babelrc) add browserlist 292 | - (package) madrun v3.0.1 293 | - (package) node-pty v0.9.0-beta24 294 | - (package) eslint-plugin-putout v2.0.0 295 | - (package) putout v5.7.1 296 | - (package) style-loader v1.0.0 297 | 298 | 299 | 2019.07.11, v4.7.4 300 | 301 | feature: 302 | - (package) xterm v3.14.5 303 | 304 | 305 | 2019.07.01, v4.7.3 306 | 307 | feature: 308 | - (package) xterm v3.14.4 309 | - (package) css-loader v3.0.0 310 | - (package) eslint v6.0.1 311 | 312 | 313 | 2019.06.06, v4.7.2 314 | 315 | feature: 316 | - (package) xterm v3.14.2 317 | 318 | 319 | 2019.06.03, v4.7.1 320 | 321 | feature: 322 | - (package) xterm v3.14.1 323 | 324 | 325 | 2019.05.31, v4.7.0 326 | 327 | feature: 328 | - (gritty) add support of cwd 329 | 330 | 331 | 2019.05.27, v4.6.0 332 | 333 | feature: 334 | - (gritty) add ability to emit exit code 335 | - (gritty) add support of node v12 336 | 337 | 338 | 2019.05.27, v4.5.0 339 | 340 | feature: 341 | - (gritty) add support of quotes in command arguments 342 | 343 | 344 | 2019.05.22, v4.4.2 345 | 346 | feature: 347 | - (package) xterm v3.13.2 348 | 349 | 350 | 2019.05.17, v4.4.1 351 | 352 | feature: 353 | - (package) xterm v3.13.1 354 | 355 | 356 | 2019.05.10, v4.4.0 357 | 358 | feature: 359 | - (package) eslint-plugin-node v9.0.1 360 | - (package) clean-css-loader v2.0.0 361 | - (gritty) options: add support of command, autoRestart 362 | - (package) nyc v14.0.0 363 | 364 | 365 | 2019.04.16, v4.3.10 366 | 367 | fix: 368 | - (style) .terminal: height 369 | 370 | feature: 371 | - (package) xterm v3.12.2 372 | 373 | 374 | 2019.03.11, v4.3.9 375 | 376 | feature: 377 | - (package) xterm v3.12.0 378 | - (package) putout v4.0.0 379 | - (package) madrun v2.0.0 380 | 381 | 382 | 2019.02.12, v4.3.8 383 | 384 | feature: 385 | - (package) xterm v3.11.0 386 | - (package) yargs-parser v13.0.0 387 | - (package) add putout 388 | - (package) add @putout/eslint-config 389 | - (package) tape -> supertape 390 | 391 | 392 | 2019.01.10, v4.3.7 393 | 394 | feature: 395 | - (package) xterm v3.10.1 396 | 397 | 398 | 2018.12.25, v4.3.6 399 | 400 | feature: 401 | - (package) node-pty v0.8.0 402 | 403 | 404 | 2018.12.21, v4.3.5 405 | 406 | fix: 407 | - (gritty) --path 408 | 409 | 410 | 2018.12.20, v4.3.4 411 | 412 | feature: 413 | - (package) xterm v3.9.1 414 | - (package) css-loader v2.0.1 415 | - (gritty) sinon, sinon-called-with-diff -> @cloudcmd/stub 416 | 417 | 418 | 2018.11.06, v4.3.3 419 | 420 | feature: 421 | - (gritty) express -> router 422 | - (package) sinon-called-with-diff v3.0.0 423 | - (package) eslint-plugin-node v8.0.0 424 | - (package) sinon v7.0.0 425 | 426 | 427 | 2018.10.09, v4.3.2 428 | 429 | feature: 430 | - (package) currify v3.0.0 431 | 432 | 433 | 2018.10.08, v4.3.1 434 | 435 | feature: 436 | - (package) xterm v3.8.0 437 | - (package) yargs-parser v11.0.0 438 | 439 | 440 | 2018.09.28, v4.3.0 441 | 442 | feature: 443 | - (gritty) add --no-auto-restart 444 | 445 | 446 | 2018.09.28, v4.2.0 447 | 448 | feature: 449 | - (gritty) add --auto-restart 450 | - (package) redrun v7.0.0 451 | 452 | 453 | 2018.09.27, v4.1.0 454 | 455 | feature: 456 | - (gritty) add support of --command 457 | 458 | 459 | 2018.09.27, v4.0.0 460 | 461 | feature: 462 | - (package) debug v4.0.1 463 | - (package) drop support of node < 8 464 | 465 | 466 | 2018.09.08, v3.0.3 467 | 468 | feature: 469 | - (package) babel v7.0.0 470 | - (package) xterm v3.7.0 471 | - (package) babel-loader v8.0.2 472 | - (package) nyc v13.0.1 473 | - (package) style-loader v0.23.0 474 | 475 | 476 | 2018.08.25, v3.0.2 477 | 478 | feature: 479 | - (package) xterm v3.6.0 480 | - (package) style-loader v0.22.0 481 | 482 | 483 | 2018.07.23, v3.0.1 484 | 485 | feature: 486 | - (package) debug v3.1.0 487 | - (package) eslint-plugin-node v7.0.1 488 | 489 | 490 | 2018.07.16, v3.0.0 491 | 492 | feature: 493 | - (package) add eslint-plugin-node 494 | - (package) drop support of node < 6 495 | - (gritty) drop support of authCheck and cloudcmd <= v9.0.0 496 | - (gritty) rm fetch, Promise polyfill: provide your own polyfill if you needed 497 | - (package) optimize-css-assets-webpack-plugin v5.0.0 498 | 499 | 500 | 2018.07.13, v2.2.4 501 | 502 | feature: 503 | - (npmignore) add .* 504 | 505 | 506 | 2018.07.13, v2.2.3 507 | 508 | feature: 509 | - (package) xterm v3.5.1 510 | - (package) css-loader v1.0.0 511 | - (package) eslint v5.0.0 512 | - (package) sinon v6.0.0 513 | - (package) nyc v12.0.2 514 | - (package) webpack-cli v3.0.1 515 | 516 | 517 | 2018.05.30, v2.2.2 518 | 519 | feature: 520 | - (gritty) node-pty-prebuilt -> node-pty 521 | 522 | 523 | 2018.05.29, v2.2.1 524 | 525 | feature: 526 | - (package) promise-polyfill v8.0.0 527 | 528 | 529 | 2018.05.22, v2.2.0 530 | 531 | feature: 532 | - (gritty) add support of fontFamily 533 | 534 | 535 | 2018.05.22, v2.1.2 536 | 537 | feature: 538 | - (package) xterm v3.4.1: improve start up and overall performance of a terminal 539 | 540 | 541 | 2018.05.03, v2.1.1 542 | 543 | feature: 544 | - (package) wraptile v2.0.0 545 | - (package) clean-css-loader v1.0.1 546 | - (package) sinon v5.0.1 547 | - (package) style-loader v0.21.0 548 | 549 | 550 | 2018.04.19, v2.1.0 551 | 552 | feature: 553 | - (gritty) rm redundant .terminal 554 | - (index) rm callback 555 | - (gritty) timeout -> accept 556 | 557 | 558 | 2018.04.18, v2.0.1 559 | 560 | feature: 561 | - (package) xterm v3.3.0 562 | 563 | 564 | 2018.03.30, v2.0.0 565 | 566 | feature: 567 | - (gritty) add auth 568 | - (gritty) drop support of node < 4 569 | 570 | 571 | 2018.03.12, v1.5.2 572 | 573 | feature: 574 | - (package) xterm v3.2.0 575 | - (package) redrun v6.0.0 576 | 577 | 578 | 2018.03.01, v1.5.1 579 | 580 | feature: 581 | - (package) webpack v4.0.1 582 | 583 | 584 | 2018.02.14, v1.5.0 585 | 586 | feature: 587 | - (package) sinon-called-with-diff v2.0.0 588 | - (gritty) add ability to set ENV with help of socket.request.env #2 589 | 590 | 591 | 2018.02.12, v1.4.29 592 | 593 | feature: 594 | - (package) xterm v3.1.0 595 | 596 | 597 | 2018.01.31, v1.4.28 598 | 599 | feature: 600 | - (package) es6-promisify v6.0.0 601 | - (travis) node_js: 8, 9 602 | - (package) style-loader v0.20.1 603 | 604 | 605 | 2018.01.19, v1.4.27 606 | 607 | feature: 608 | - (package) xterm v3.0.2 609 | 610 | 611 | 2018.01.16, v1.4.26 612 | 613 | feature: 614 | - (package) node-pty -> node-pty-prebuilt: speed up install 615 | 616 | 617 | 2018.01.15, v1.4.25 618 | 619 | feature: 620 | - (package) xterm v3.0.1 621 | - (package) mock-require v3.0.1 622 | - (package) promise-polyfill v7.0.0 623 | 624 | 625 | 2017.12.21, v1.4.24 626 | 627 | feature: 628 | - (package) promise-polyfill v6.1.0 629 | - (package) style-loader v0.19.0 630 | - (package) coveralls v3.0.0 631 | - (package) sinon v4.0.0 632 | - (package) babel-preset-es2015 -> babel-preset-env 633 | 634 | 635 | 2017.08.28, v1.4.23 636 | 637 | feature: 638 | - (package) node-pty v0.7.0 639 | 640 | 641 | 2017.08.14, v1.4.22 642 | 643 | feature: 644 | - (package) xterm v2.9.2 645 | - (package) debug v3.0.0 646 | 647 | 648 | 2017.08.07, v1.4.21 649 | 650 | feature: 651 | - (package) xterm v2.9.1 652 | 653 | 654 | 2017.08.04, v1.4.20 655 | 656 | feature: 657 | - (package) sinon v3.0.0 658 | - (package) xterm v2.9.0 659 | 660 | 661 | 2017.07.17, v1.4.19 662 | 663 | fix: 664 | - (package) can work on engines lower then 4 via legacy suffix 665 | 666 | 667 | 2017.07.12, v1.4.18 668 | 669 | fix: 670 | - (package) rm duplicate wraptile 671 | 672 | feature: 673 | - (package) xterm v2.8.1 674 | - (package) clean-css-loader v0.1.2 675 | - (package) webpack v3.0.0 676 | - (package) eslint v4.0.0 677 | - (package) eslint v4.0.0 678 | 679 | 680 | 2017.06.08, v1.4.17 681 | 682 | feature: 683 | - (package) xterm v2.7.0 684 | - (gitignore) add package-lock.json 685 | - (package) nyc v11.0.2 686 | 687 | 688 | 2017.05.22, v1.4.16 689 | 690 | feature: 691 | - (package) clean-css-loader v0.0.6 692 | - (package) style-loader v0.18.0 693 | - (cursor-blink) rm unused 694 | 695 | 696 | 2017.05.20, v1.4.15 697 | 698 | fix: 699 | - (gritty) crash on exit 700 | - (package) add request 701 | - (package) watcher: add client, server 702 | 703 | 704 | 2017.05.12, v1.4.14 705 | 706 | fix: 707 | - (package) wraptile 708 | 709 | 710 | 2017.05.12, v1.4.13 711 | 712 | fix: 713 | - (gritty) add support of node v4 714 | 715 | 716 | 2017.05.10, v1.4.12 717 | 718 | fix: 719 | - (client) socket.io.min -> socket.io 720 | 721 | 722 | 2017.05.10, v1.4.11 723 | 724 | feature: 725 | - (package) socket.io v2.0.1 726 | - (package) socket.io-client v2.0.1 727 | - (package) style-loader v0.17.0 728 | 729 | 730 | 2017.05.08, v1.4.10 731 | 732 | feature: 733 | - (package) xterm v2.6.0 734 | 735 | 736 | 2017.04.26, v1.4.9 737 | 738 | fix: 739 | - (gritty) cursorBlink: insane: disable 740 | 741 | 742 | 2017.04.25, v1.4.8 743 | 744 | fix: 745 | - (webpack) libraryTarget: umd -> var 746 | 747 | feature: 748 | - (package) babel-loader v7.0.0 749 | 750 | 751 | 2017.04.13, v1.4.7 752 | 753 | feature: 754 | - (package) xterm v2.5.0 755 | 756 | 757 | 2017.04.13, v1.4.6 758 | 759 | fix: 760 | - (gritty) heigth 761 | 762 | 763 | 2017.04.12, v1.4.5 764 | 765 | feature: 766 | - (gritty) server/index.js -> server/gritty.js 767 | - (package) clean-css-loader v0.0.5 768 | - (package) add sinon-called-with-diff 769 | 770 | 771 | 2017.04.06, v1.4.4 772 | 773 | feature: 774 | - (gritty) wrap -> wraptile 775 | 776 | 777 | 2017.04.06, v1.4.3 778 | 779 | fix: 780 | - (server) socket connect when no authCheck 781 | 782 | feature: 783 | - (travis) add before_install 784 | - (travis) npm run build:client 785 | - (get-env) add 786 | - (package) add coveralls 787 | - (webpack) output: add pathinfo 788 | - (webpack) devtoolModuleFilenameTemplate: webpack:// -> gritty 789 | 790 | 791 | 2017.04.03, v1.4.2 792 | 793 | feature: 794 | - (package) scripts: build:client(:dev)? 795 | - (cursor-blink) add 796 | 797 | 798 | 2017.04.03, v1.4.1 799 | 800 | fix: 801 | - (gritty) cursorBlink: enable on connect, disable on disconnect 802 | 803 | 804 | 2017.03.31, v1.4.0 805 | 806 | feature: 807 | - (gritty) add ability to reconnect 808 | - (css) improved font 809 | 810 | 811 | 2017.03.30, v1.3.1 812 | 813 | feature: 814 | - (package) xterm v2.4.0 815 | - (package) css-loader v0.28.0 816 | - (package) style-loader v0.16.0 817 | - (package) style-loader v0.15.0 818 | - (package) css-loader v0.27.3 819 | - (package) style-loader v0.14.1 820 | 821 | 822 | 2017.03.04, v1.3.0 823 | 824 | feature: 825 | - (gritty) add --port, --path 826 | 827 | 828 | 2017.03.03, v1.2.1 829 | 830 | fix: 831 | - (gritty) resize: on windows 832 | - (gritty) --v -> --version 833 | 834 | 835 | 2017.03.03, v1.2.0 836 | 837 | feature: 838 | - (gritty) add --module-path 839 | 840 | 841 | 2017.03.03, v1.1.2 842 | 843 | fix: 844 | - (gritty) return {socket, terminal} 845 | 846 | 847 | 2017.03.03, v1.1.1 848 | 849 | fix: 850 | - (gritty) return socket 851 | 852 | 853 | 2017.03.03, v1.1.0 854 | 855 | feature: 856 | - (gritty) add ability to use as middleware 857 | 858 | 859 | 2017.03.02, v1.0.5 860 | 861 | fix: 862 | - (index) Console -> Gritty 863 | 864 | 865 | 2017.03.02, v1.0.4 866 | 867 | fix: 868 | - (style) html: height: 99% 869 | - (gritty) pid -> Pid 870 | 871 | 872 | 2017.03.02, v1.0.3 873 | 874 | fix: 875 | - (gritty) kill: ESRCH: wrong pid 876 | 877 | 878 | 2017.03.02, v1.0.2 879 | 880 | feature: 881 | - (package) xterm: devDependencies 882 | - (package) rm rendy 883 | - (package) rm join-io 884 | 885 | 886 | 2017.03.02, v1.0.1 887 | 888 | fix: 889 | - (package) console -> gritty 890 | 891 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gritty [![License][LicenseIMGURL]][LicenseURL] [![NPM version][NPMIMGURL]][NPMURL] [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Coverage Status][CoverageIMGURL]][CoverageURL] 2 | 3 | [NPMIMGURL]: https://img.shields.io/npm/v/gritty.svg?style=flat&longCache=true 4 | [BuildStatusURL]: https://github.com/cloudcmd/gritty/actions?query=workflow%3A%22Node+CI%22 "Build Status" 5 | [BuildStatusIMGURL]: https://github.com/cloudcmd/gritty/workflows/Node%20CI/badge.svg 6 | [LicenseIMGURL]: https://img.shields.io/badge/license-MIT-317BF9.svg?style=flat&longCache=true 7 | [NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png 8 | [NPMURL]: https://npmjs.org/package/cloudcmd "npm" 9 | [BuildStatusURL]: https://travis-ci.org/cloudcmd/gritty "Build Status" 10 | [LicenseURL]: https://tldrlegal.com/license/mit-license "MIT License" 11 | [CoverageURL]: https://coveralls.io/github/cloudcmd/gritty?branch=master 12 | [CoverageIMGURL]: https://coveralls.io/repos/cloudcmd/gritty/badge.svg?branch=master&service=github 13 | 14 | Web terminal emulator. Based on [node-pty](https://github.com/Tyriar/node-pty) and [xterm.js](https://github.com/sourcelair/xterm.js). 15 | 16 | ![Gritty](https://raw.githubusercontent.com/cloudcmd/gritty/master/img/linux.png "Gritty on Linux") 17 | 18 | ## Install 19 | 20 | `npm i gritty -g` 21 | 22 | ## Usage 23 | 24 | ``` 25 | Usage: gritty [options] 26 | Options: 27 | -h, --help display this help and exit 28 | -v, --version output version information and exit 29 | --path output path of a gritty and exit 30 | --port set port number 31 | --command command to run in terminal (shell by default) 32 | --auto-restart restart command when on exit 33 | --no-auto-restart do not restart command on exit 34 | ``` 35 | 36 | ### Windows 37 | 38 | On `Windows` there is no build tools by default. When can't install `gritty` try to install `windows-build-tools` first. 39 | 40 | ```sh 41 | npm i windows-build-tools -g 42 | npm i gritty -g 43 | ``` 44 | 45 | ![Gritty](https://raw.githubusercontent.com/cloudcmd/gritty/master/img/windows.png "Gritty on Windows") 46 | 47 | ## Use as standalone 48 | 49 | Start `gritty`, and go to url `http://localhost:1337` 50 | 51 | ## API 52 | 53 | ### Client API 54 | 55 | #### gritty(element [, options]) 56 | 57 | ```js 58 | const prefix = '/gritty'; 59 | const env = {}; // default 60 | const fontFamily = 'Courier'; 61 | 62 | gritty('body', { 63 | prefix, 64 | env, 65 | fontFamily, 66 | }); 67 | ``` 68 | 69 | ### Server API 70 | 71 | #### gritty.listen(socket, [, options]) 72 | 73 | `Gritty` could be used as middleware: 74 | 75 | ```js 76 | const prefix = '/gritty'; 77 | 78 | const auth = (accept, reject) => (username, password) => { 79 | accept(); 80 | }; 81 | 82 | gritty.listen(socket, { 83 | prefix, 84 | auth, // optional 85 | }); 86 | ``` 87 | 88 | #### gritty(options) 89 | 90 | Middleware function: 91 | 92 | ```js 93 | const prefix = '/gritty'; 94 | 95 | gritty({ 96 | prefix, 97 | }); 98 | ``` 99 | 100 | ## Usage as middleware 101 | 102 | To use `gritty` in your programs you should make local install: 103 | 104 | `npm i gritty socket.io express --save` 105 | 106 | And use it this way: 107 | 108 | ```js 109 | // server.js 110 | const http = require('node:http'); 111 | const gritty = require('gritty'); 112 | 113 | const express = require('express'); 114 | const io = require('socket.io'); 115 | 116 | const app = express(); 117 | const server = http.createServer(app); 118 | const socket = io.listen(server); 119 | 120 | const port = 1337; 121 | const ip = '0.0.0.0'; 122 | 123 | app.use(gritty()); 124 | app.use(express.static(__dirname)); 125 | 126 | gritty.listen(socket, { 127 | command: 'mc', // optional 128 | autoRestart: true, // default 129 | }); 130 | 131 | server.listen(port, ip); 132 | ``` 133 | 134 | ```html 135 | 136 | 137 |
138 | 139 | 157 | ``` 158 | 159 | ## License 160 | 161 | MIT 162 | -------------------------------------------------------------------------------- /bin/gritty.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const {join} = require('node:path'); 6 | const process = require('node:process'); 7 | const args = require('yargs-parser')(process.argv.slice(2), { 8 | boolean: [ 9 | 'version', 10 | 'help', 11 | 'auto-restart', 12 | 'path', 13 | ], 14 | number: ['port'], 15 | string: ['command'], 16 | alias: { 17 | help: 'h', 18 | version: 'v', 19 | }, 20 | default: { 21 | 'port': process.env.PORT | 1337, 22 | 'auto-restart': true, 23 | }, 24 | }); 25 | 26 | const getMessage = (a) => a.message; 27 | 28 | main(args); 29 | 30 | function main(args) { 31 | if (args.help) 32 | return help(); 33 | 34 | if (args.version) 35 | return version(); 36 | 37 | if (args.path) 38 | return path(); 39 | 40 | start({ 41 | port: args.port, 42 | command: args.command, 43 | autoRestart: args.autoRestart, 44 | }); 45 | } 46 | 47 | function path() { 48 | console.log(join(__dirname, '..')); 49 | } 50 | 51 | function start(options) { 52 | const squad = require('squad'); 53 | 54 | const { 55 | port, 56 | command, 57 | autoRestart, 58 | } = options; 59 | 60 | check(port); 61 | 62 | const DIR = `${__dirname}/../`; 63 | 64 | const gritty = require('../'); 65 | const http = require('node:http'); 66 | 67 | const express = require('express'); 68 | const io = require('socket.io'); 69 | 70 | const app = express(); 71 | const server = http.createServer(app); 72 | 73 | const c9 = process.env.IP; 74 | const ip = c9 || '0.0.0.0'; 75 | 76 | app 77 | .use(gritty()) 78 | .use(express.static(DIR)); 79 | 80 | const socket = io(server); 81 | 82 | gritty.listen(socket, { 83 | command, 84 | autoRestart, 85 | }); 86 | 87 | server 88 | .listen(port, ip) 89 | .on('error', squad(exit, getMessage)); 90 | 91 | console.log(`url: http://localhost:${port}`); 92 | } 93 | 94 | function help() { 95 | const bin = require('../help'); 96 | const usage = 'Usage: gritty [options]'; 97 | 98 | console.log(usage); 99 | console.log('Options:'); 100 | 101 | for (const name of Object.keys(bin)) { 102 | console.log(' %s %s', name, bin[name]); 103 | } 104 | } 105 | 106 | function version() { 107 | const pack = require('../package'); 108 | console.log('v' + pack.version); 109 | } 110 | 111 | function check(port) { 112 | if (isNaN(port)) 113 | exit('port should be a number 0..65535'); 114 | } 115 | 116 | function exit(msg) { 117 | console.error(msg); 118 | process.exit(-1); 119 | } 120 | -------------------------------------------------------------------------------- /client/get-el.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isString = (a) => typeof a === 'string'; 4 | 5 | module.exports = (el) => { 6 | if (isString(el)) 7 | return document.querySelector(el); 8 | 9 | return el; 10 | }; 11 | -------------------------------------------------------------------------------- /client/get-env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isFn = (a) => typeof a === 'function'; 4 | 5 | module.exports = (env) => { 6 | const obj = {}; 7 | 8 | for (const name of Object.keys(env)) { 9 | obj[name] = getValue(env[name]); 10 | } 11 | 12 | return obj; 13 | }; 14 | 15 | function getValue(value) { 16 | if (isFn(value)) 17 | return value(); 18 | 19 | return value; 20 | } 21 | -------------------------------------------------------------------------------- /client/get-host.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => { 4 | const l = location; 5 | 6 | return l.origin || l.protocol + '//' + l.host; 7 | }; 8 | -------------------------------------------------------------------------------- /client/gritty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('@xterm/xterm/css/xterm.css'); 4 | 5 | const {FitAddon} = require('@xterm/addon-fit'); 6 | const {WebglAddon} = require('@xterm/addon-webgl'); 7 | const currify = require('currify'); 8 | const tryCatch = require('try-catch'); 9 | 10 | const wrap = require('wraptile'); 11 | 12 | const {io} = require('socket.io-client'); 13 | const {Terminal} = require('@xterm/xterm'); 14 | 15 | const getEl = require('./get-el'); 16 | const getHost = require('./get-host'); 17 | const getEnv = require('./get-env'); 18 | 19 | const onWindowResize = wrap(_onWindowResize); 20 | const onTermData = currify(_onTermData); 21 | const onTermResize = currify(_onTermResize); 22 | const onData = currify(_onData); 23 | 24 | const onDisconnect = wrap(_onDisconnect); 25 | 26 | const onConnect = wrap(_onConnect); 27 | 28 | module.exports = gritty; 29 | module.exports._onConnect = _onConnect; 30 | module.exports._onDisconnect = _onDisconnect; 31 | module.exports._onData = _onData; 32 | module.exports._onTermResize = _onTermResize; 33 | module.exports._onTermData = _onTermData; 34 | module.exports._onWindowResize = _onWindowResize; 35 | 36 | const defaultFontFamily = 'Menlo, Consolas, "Liberation Mono", Monaco, "Lucida Console", monospace'; 37 | 38 | module.exports._defaultFontFamily = defaultFontFamily; 39 | 40 | function gritty(element, options = {}) { 41 | const el = getEl(element); 42 | 43 | const { 44 | socketPath = '', 45 | fontFamily = defaultFontFamily, 46 | prefix = '/gritty', 47 | command, 48 | autoRestart, 49 | cwd, 50 | } = options; 51 | 52 | const env = getEnv(options.env || {}); 53 | const socket = connect(prefix, socketPath); 54 | 55 | return createTerminal(el, { 56 | env, 57 | cwd, 58 | command, 59 | autoRestart, 60 | socket, 61 | fontFamily, 62 | }); 63 | } 64 | 65 | function createTerminal(terminalContainer, {env, cwd, command, autoRestart, socket, fontFamily}) { 66 | const fitAddon = new FitAddon(); 67 | const webglAddon = new WebglAddon(); 68 | const terminal = new Terminal({ 69 | scrollback: 1000, 70 | tabStopWidth: 4, 71 | fontFamily, 72 | allowProposedApi: true, 73 | }); 74 | 75 | terminal.open(terminalContainer); 76 | terminal.focus(); 77 | 78 | terminal.loadAddon(webglAddon); 79 | terminal.loadAddon(fitAddon); 80 | fitAddon.fit(); 81 | 82 | terminal.onResize(onTermResize(socket)); 83 | terminal.onData(onTermData(socket)); 84 | 85 | window.addEventListener('resize', onWindowResize(fitAddon)); 86 | 87 | const {cols, rows} = terminal; 88 | 89 | socket.on('accept', onConnect(socket, fitAddon, { 90 | env, 91 | cwd, 92 | cols, 93 | rows, 94 | command, 95 | autoRestart, 96 | })); 97 | socket.on('disconnect', onDisconnect(terminal)); 98 | socket.on('data', onData(terminal)); 99 | 100 | return { 101 | socket, 102 | terminal, 103 | }; 104 | } 105 | 106 | function _onConnect(socket, fitAddon, {env, cwd, cols, rows, command, autoRestart}) { 107 | socket.emit('terminal', { 108 | env, 109 | cwd, 110 | cols, 111 | rows, 112 | command, 113 | autoRestart, 114 | }); 115 | socket.emit('resize', { 116 | cols, 117 | rows, 118 | }); 119 | fitAddon.fit(); 120 | } 121 | 122 | function _onDisconnect(terminal) { 123 | terminal.writeln('terminal disconnected...'); 124 | } 125 | 126 | function _onData(terminal, data) { 127 | terminal.write(data); 128 | } 129 | 130 | function _onTermResize(socket, {cols, rows}) { 131 | socket.emit('resize', { 132 | cols, 133 | rows, 134 | }); 135 | } 136 | 137 | function _onTermData(socket, data) { 138 | socket.emit('data', data); 139 | } 140 | 141 | function _onWindowResize(fitAddon) { 142 | // Uncaught Error: This API only accepts integers 143 | // when gritty mimized 144 | const fit = fitAddon.fit.bind(fitAddon); 145 | tryCatch(fit); 146 | } 147 | 148 | function connect(prefix, socketPath) { 149 | const href = getHost(); 150 | const FIVE_SECONDS = 5000; 151 | 152 | const path = `${socketPath}/socket.io`; 153 | const socket = io.connect(href + prefix, { 154 | 'max reconnection attempts': 2 ** 32, 155 | 'reconnection limit': FIVE_SECONDS, 156 | path, 157 | }); 158 | 159 | return socket; 160 | } 161 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body, 6 | .gritty { 7 | height: 100%; 8 | margin: 0; 9 | } 10 | 11 | .terminal { 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /help.json: -------------------------------------------------------------------------------- 1 | { 2 | "-h, --help ": "display this help and exit", 3 | "-v, --version ": "output version information and exit", 4 | "--path ": "output path of a gritty and exit", 5 | "--port ": "set port number", 6 | "--command ": "command to run (shell by default)", 7 | "--auto-restart ": "restart command on exit", 8 | "--no-auto-restart ": "do not restart command on exit" 9 | } 10 | -------------------------------------------------------------------------------- /img/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcmd/gritty/720e064fd7b748ac916b0f8b18366bbbcbb8ebc0/img/linux.png -------------------------------------------------------------------------------- /img/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcmd/gritty/720e064fd7b748ac916b0f8b18366bbbcbb8ebc0/img/windows.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gritty 5 | 6 | 7 | 8 | 9 |
10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gritty", 3 | "version": "8.1.2", 4 | "type": "commonjs", 5 | "author": "coderaiser (https://github.com/coderaiser)", 6 | "description": "Web terminal emulator", 7 | "bin": { 8 | "gritty": "bin/gritty.js" 9 | }, 10 | "main": "server/gritty.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/cloudcmd/gritty.git" 14 | }, 15 | "scripts": { 16 | "start": "madrun start", 17 | "start:dev": "madrun start:dev", 18 | "lint": "madrun lint", 19 | "fix:lint": "madrun fix:lint", 20 | "watch:test": "madrun watch:test", 21 | "watcher": "madrun watcher", 22 | "build-progress": "madrun build-progress", 23 | "6to5:client": "madrun 6to5:client", 24 | "6to5:client:dev": "madrun 6to5:client:dev", 25 | "watch:client": "madrun watch:client", 26 | "watch:client:dev": "madrun watch:client:dev", 27 | "wisdom": "madrun wisdom", 28 | "build": "madrun build", 29 | "build:start": "madrun build:start", 30 | "build:start:dev": "madrun build:start:dev", 31 | "build:client": "madrun build:client", 32 | "build:client:dev": "madrun build:client:dev", 33 | "watch:lint": "madrun watch:lint", 34 | "report": "madrun report", 35 | "coverage": "madrun coverage", 36 | "test": "madrun test" 37 | }, 38 | "keywords": [ 39 | "console", 40 | "terminal", 41 | "emulator", 42 | "express" 43 | ], 44 | "dependencies": { 45 | "currify": "^4.0.0", 46 | "debug": "^4.0.1", 47 | "express": "^4.14.0", 48 | "node-pty": "^1.0.0", 49 | "router": "^1.3.3", 50 | "socket.io": "^4.8.1", 51 | "squad": "^3.0.0", 52 | "string-to-argv": "^1.0.0", 53 | "wraptile": "^3.0.0", 54 | "yargs-parser": "^21.0.1" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.0.0", 58 | "@babel/preset-env": "^7.0.0", 59 | "@iocmd/wait": "^2.1.0", 60 | "@xterm/addon-fit": "^0.9.0", 61 | "@xterm/addon-webgl": "^0.17.0", 62 | "@xterm/xterm": "^5.0.0", 63 | "babel-loader": "^9.1.3", 64 | "c8": "^10.1.3", 65 | "clean-css-loader": "^4.1.1", 66 | "css-loader": "^7.1.2", 67 | "css-minimizer-webpack-plugin": "^7.0.0", 68 | "css-modules-require-hook": "^4.0.6", 69 | "es6-promisify": "^7.0.0", 70 | "eslint": "^9.19.0", 71 | "eslint-plugin-n": "^17.15.1", 72 | "eslint-plugin-putout": "^24.0.0", 73 | "json-loader": "^0.5.4", 74 | "madrun": "^10.0.1", 75 | "mock-require": "^3.0.2", 76 | "nodemon": "^3.0.1", 77 | "optimize-css-assets-webpack-plugin": "^6.0.1", 78 | "putout": "^38.0.6", 79 | "request": "^2.81.0", 80 | "serve-once": "^3.0.0", 81 | "socket.io-client": "^4.0.0", 82 | "style-loader": "^4.0.0", 83 | "supertape": "^10.4.0", 84 | "webpack": "^5.1.3", 85 | "webpack-cli": "^6.0.1" 86 | }, 87 | "engines": { 88 | "node": ">=18" 89 | }, 90 | "license": "MIT", 91 | "publishConfig": { 92 | "access": "public" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /server/gritty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | 5 | const path = require('node:path'); 6 | 7 | const log = require('debug')('gritty'); 8 | const Router = require('router'); 9 | 10 | const currify = require('currify'); 11 | const wraptile = require('wraptile'); 12 | const _pty = require('node-pty'); 13 | 14 | const stringArgv = require('string-to-argv'); 15 | const isFn = (a) => typeof a === 'function'; 16 | const isBool = (a) => typeof a === 'boolean'; 17 | 18 | const DIR_ROOT = `${__dirname}/..`; 19 | 20 | const terminalFn = currify(_terminalFn); 21 | const connectionWraped = wraptile(connection); 22 | 23 | const CMD = process.platform === 'win32' ? 'cmd.exe' : 'bash'; 24 | const isDev = process.env.NODE_ENV === 'development'; 25 | 26 | const getDist = () => { 27 | if (isDev) 28 | return '/dist-dev'; 29 | 30 | return '/dist'; 31 | }; 32 | 33 | const choose = (a, b, options) => { 34 | if (isBool(a)) 35 | return a; 36 | 37 | if (isBool(b)) 38 | return b; 39 | 40 | return options.default; 41 | }; 42 | 43 | module.exports = (options = {}) => { 44 | const router = Router(); 45 | const {prefix = '/gritty'} = options; 46 | 47 | router 48 | .route(`${prefix}/*`) 49 | .get(terminalFn(options)) 50 | .get(staticFn); 51 | 52 | return router; 53 | }; 54 | 55 | function _terminalFn(options, req, res, next) { 56 | const {prefix = '/gritty'} = options; 57 | 58 | req.url = req.url.replace(prefix, ''); 59 | 60 | if (/^\/gritty\.js(\.map)?$/.test(req.url)) 61 | req.url = getDist() + req.url; 62 | 63 | next(); 64 | } 65 | 66 | function staticFn(req, res) { 67 | const file = path.normalize(DIR_ROOT + req.url); 68 | res.sendFile(file); 69 | } 70 | 71 | function createTerminal({command, env, cwd, cols, rows, pty = _pty}) { 72 | cols = cols || 80; 73 | rows = rows || 24; 74 | 75 | const [cmd, ...args] = stringArgv(command); 76 | const term = pty.spawn(cmd, args, { 77 | name: 'xterm-color', 78 | cols, 79 | rows, 80 | cwd, 81 | env: { 82 | ...process.env, 83 | ...env, 84 | }, 85 | }); 86 | 87 | log(`Created terminal with PID: ${term.pid}`); 88 | 89 | return term; 90 | } 91 | 92 | module.exports.listen = (socket, options = {}) => { 93 | check(socket, options); 94 | 95 | const {prefix, auth} = options; 96 | 97 | socket 98 | .of(prefix || '/gritty') 99 | .on('connection', (socket) => { 100 | const connect = connectionWraped(options, socket); 101 | 102 | if (!auth) 103 | return connection(options, socket); 104 | 105 | const reject = () => socket.emit('reject'); 106 | socket.on('auth', auth(connect, reject)); 107 | }); 108 | }; 109 | 110 | function check(socket, options) { 111 | if (!socket) 112 | throw Error('socket could not be empty!'); 113 | 114 | const {auth} = options; 115 | 116 | if (auth && !isFn(auth)) 117 | throw Error('options.auth should be a function!'); 118 | } 119 | 120 | function connection(options, socket) { 121 | socket.emit('accept'); 122 | 123 | let term; 124 | 125 | socket.on('terminal', onTerminal); 126 | 127 | const onResize = (size) => { 128 | size = size || {}; 129 | 130 | const {cols = 80, rows = 25} = size; 131 | 132 | term.resize(cols, rows); 133 | log(`Resized terminal ${term.pid} to ${cols} cols and ${rows} rows.`); 134 | }; 135 | 136 | const onData = (msg) => { 137 | term.write(msg); 138 | }; 139 | 140 | function onTerminal(params) { 141 | params = params || {}; 142 | const { 143 | env, 144 | rows, 145 | cols, 146 | cwd, 147 | } = params; 148 | 149 | const command = params.command || options.command || CMD; 150 | 151 | const autoRestart = choose(params.autoRestart, options.autoRestart, { 152 | default: true, 153 | }); 154 | 155 | term = createTerminal({ 156 | command, 157 | cwd, 158 | env, 159 | rows, 160 | cols, 161 | }); 162 | 163 | const onExit = (code) => { 164 | socket.emit('exit', code); 165 | onDisconnect(); 166 | 167 | if (!autoRestart) 168 | return; 169 | 170 | onTerminal(); 171 | }; 172 | 173 | const onDisconnect = () => { 174 | term.removeListener('exit', onExit); 175 | term.kill(); 176 | log(`Closed terminal ${term.pid}`); 177 | 178 | socket.removeListener('resize', onResize); 179 | socket.removeListener('data', onData); 180 | socket.removeListener('terminal', onTerminal); 181 | socket.removeListener('disconnect', onDisconnect); 182 | }; 183 | 184 | term.on('data', (data) => { 185 | socket.emit('data', data); 186 | }); 187 | 188 | term.on('exit', onExit); 189 | 190 | log('Connected to terminal ' + term.pid); 191 | 192 | socket.on('data', onData); 193 | socket.on('resize', onResize); 194 | socket.on('disconnect', onDisconnect); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /test/before.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('node:http'); 4 | 5 | const {promisify} = require('node:util'); 6 | const express = require('express'); 7 | 8 | const io = require('socket.io'); 9 | const gritty = require('..'); 10 | 11 | const isFn = (a) => typeof a === 'function'; 12 | 13 | module.exports = before; 14 | 15 | function before(options, fn = options) { 16 | if (isFn(options)) 17 | options = {}; 18 | 19 | const app = express(); 20 | const server = http.createServer(app); 21 | 22 | const after = () => { 23 | server.close(); 24 | }; 25 | 26 | app.use(gritty()); 27 | 28 | const socket = io(server); 29 | 30 | gritty.listen(socket, options); 31 | 32 | server.listen(() => { 33 | const {port} = server.address(); 34 | fn(port, after, socket); 35 | }); 36 | } 37 | 38 | module.exports.connect = promisify((options, fn = options) => { 39 | before(options, (port, done, socket) => { 40 | fn(null, { 41 | port, 42 | done, 43 | socket, 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/client/get-el.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const getEl = require('../../client/get-el'); 6 | 7 | test('gritty: get-el: object', (t) => { 8 | const el = {}; 9 | 10 | t.equal(getEl(el), el, 'should return el'); 11 | t.end(); 12 | }); 13 | 14 | test('gritty: get-el: string', (t) => { 15 | const el = 'hello'; 16 | const querySelector = stub(); 17 | 18 | global.document = { 19 | querySelector, 20 | }; 21 | 22 | getEl(el); 23 | 24 | t.calledWith(querySelector, [el], 'should call querySelector'); 25 | 26 | delete global.document; 27 | 28 | t.end(); 29 | }); 30 | -------------------------------------------------------------------------------- /test/client/get-env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | 5 | const getEnv = require('../../client/get-env'); 6 | 7 | test('gritty: get-env: empty', (t) => { 8 | const env = {}; 9 | 10 | t.deepEqual(getEnv(env), {}, 'should return env'); 11 | t.end(); 12 | }); 13 | 14 | test('gritty: get-env: value', (t) => { 15 | const env = { 16 | hello: 123, 17 | }; 18 | 19 | t.deepEqual(getEnv(env), env, 'should return env'); 20 | t.end(); 21 | }); 22 | 23 | test('gritty: get-env: function', (t) => { 24 | const env = { 25 | hello: () => 1337, 26 | }; 27 | 28 | const expected = { 29 | hello: 1337, 30 | }; 31 | 32 | t.deepEqual(getEnv(env), expected, 'should return env'); 33 | t.end(); 34 | }); 35 | -------------------------------------------------------------------------------- /test/client/get-host.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | 5 | const dir = '../../client'; 6 | const getHost = require(`${dir}/get-host`); 7 | 8 | test('gritty: get-host: origin', (t) => { 9 | const origin = 'http://localhost'; 10 | 11 | global.location = { 12 | origin, 13 | }; 14 | 15 | t.equal(getHost(), origin, 'should return origin'); 16 | 17 | delete global.location; 18 | 19 | t.end(); 20 | }); 21 | 22 | test('gritty: get-host: no origin', (t) => { 23 | global.location = { 24 | protocol: 'http:', 25 | host: 'localhost', 26 | }; 27 | 28 | t.equal(getHost(), 'http://localhost', 'should return host'); 29 | 30 | delete global.location; 31 | 32 | t.end(); 33 | }); 34 | -------------------------------------------------------------------------------- /test/client/gritty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const noop = () => {}; 4 | const {test, stub} = require('supertape'); 5 | 6 | require('css-modules-require-hook/preset'); 7 | 8 | global.document = {}; 9 | global.self = {}; 10 | global.window = { 11 | addEventListener: stub(), 12 | }; 13 | 14 | const mock = require('mock-require'); 15 | 16 | const connect = stub().returns({ 17 | on: stub(), 18 | }); 19 | 20 | const open = stub(); 21 | const focus = stub(); 22 | 23 | const Terminal = stub().returns({ 24 | open, 25 | focus, 26 | writeln: stub(), 27 | cols: 80, 28 | rows: 25, 29 | loadAddon: stub(), 30 | onResize: stub(), 31 | onData: stub(), 32 | }); 33 | 34 | Terminal.applyAddon = stub(); 35 | 36 | mock('socket.io-client/dist/socket.io', { 37 | connect, 38 | }); 39 | 40 | mock('@xterm/xterm', { 41 | Terminal, 42 | }); 43 | 44 | mock('@xterm/xterm-addong-webl', { 45 | WebglAddon: noop, 46 | }); 47 | 48 | const gritty = require('../../client/gritty'); 49 | 50 | const { 51 | _onConnect, 52 | _onDisconnect, 53 | _onData, 54 | _onTermResize, 55 | _onTermData, 56 | _onWindowResize, 57 | _defaultFontFamily, 58 | } = gritty; 59 | 60 | test('gritty: Terminal: new', (t) => { 61 | before(); 62 | 63 | gritty(); 64 | t.ok(Terminal.calledWithNew(), 'should have been called with new'); 65 | after(); 66 | 67 | t.end(); 68 | }); 69 | 70 | test('gritty: Terminal: args', (t) => { 71 | before(); 72 | 73 | const args = { 74 | scrollback: 1000, 75 | tabStopWidth: 4, 76 | fontFamily: _defaultFontFamily, 77 | allowProposedApi: true, 78 | }; 79 | 80 | gritty(); 81 | 82 | t.calledWith(Terminal, [args], 'should have been called with args'); 83 | 84 | after(); 85 | t.end(); 86 | }); 87 | 88 | test('gritty: Terminal: args: fontFamily', (t) => { 89 | before(); 90 | 91 | const fontFamily = 'Droid Sans Mono'; 92 | const el = {}; 93 | 94 | const args = { 95 | scrollback: 1000, 96 | tabStopWidth: 4, 97 | fontFamily, 98 | allowProposedApi: true, 99 | }; 100 | 101 | gritty(el, { 102 | fontFamily, 103 | }); 104 | 105 | t.calledWith(Terminal, [args], 'should have been called with args'); 106 | 107 | after(); 108 | t.end(); 109 | }); 110 | 111 | test('gritty: Terminal: open', (t) => { 112 | const el = {}; 113 | 114 | before(); 115 | 116 | gritty(el); 117 | t.calledWith(open, [el], 'should have been called'); 118 | after(); 119 | 120 | t.end(); 121 | }); 122 | 123 | test('gritty: Terminal: focus', (t) => { 124 | const el = {}; 125 | 126 | before(); 127 | 128 | gritty(el); 129 | t.calledWithNoArgs(focus, 'should have been called'); 130 | after(); 131 | 132 | t.end(); 133 | }); 134 | 135 | test('gritty: onConnect: socket: resize', (t) => { 136 | const emit = stub(); 137 | const socket = { 138 | emit, 139 | }; 140 | 141 | const options = { 142 | cols: 80, 143 | rows: 25, 144 | }; 145 | 146 | const fit = stub(); 147 | 148 | _onConnect(socket, {fit}, options); 149 | 150 | t.calledWith(emit, ['resize', options], 'should call emit'); 151 | t.end(); 152 | }); 153 | 154 | test('gritty: onConnect: socket: terminal', (t) => { 155 | const emit = stub(); 156 | const socket = { 157 | emit: (...args) => { 158 | emit(...args); 159 | socket.emit = stub(); 160 | }, 161 | }; 162 | 163 | const options = { 164 | env: { 165 | hello: 'world', 166 | }, 167 | cols: 80, 168 | rows: 25, 169 | cwd: '/', 170 | command: 'bash', 171 | autoRestart: false, 172 | }; 173 | 174 | const fit = stub(); 175 | 176 | _onConnect(socket, {fit}, options); 177 | 178 | t.calledWith(emit, ['terminal', options], 'should call emit'); 179 | t.end(); 180 | }); 181 | 182 | test('gritty: onDisconnect: terminal', (t) => { 183 | const writeln = stub(); 184 | 185 | const msg = 'terminal disconnected...'; 186 | 187 | _onDisconnect({ 188 | writeln, 189 | }); 190 | 191 | t.calledWith(writeln, [msg], 'should call terminal.writeln'); 192 | t.end(); 193 | }); 194 | 195 | test('gritty: onData: terminal', (t) => { 196 | const write = stub(); 197 | 198 | const data = 'hello'; 199 | 200 | _onData({write}, data); 201 | 202 | t.calledWith(write, [data], 'should call terminal.write'); 203 | t.end(); 204 | }); 205 | 206 | test('gritty: onTermResize: socket', (t) => { 207 | const emit = stub(); 208 | 209 | const size = { 210 | cols: 80, 211 | rows: 25, 212 | }; 213 | 214 | _onTermResize({emit}, size); 215 | 216 | t.calledWith(emit, ['resize', size], 'should call socket.emit'); 217 | t.end(); 218 | }); 219 | 220 | test('gritty: onTermData: socket', (t) => { 221 | const emit = stub(); 222 | 223 | const data = 'hello'; 224 | 225 | _onTermData({emit}, data); 226 | 227 | t.calledWith(emit, ['data', data], 'should call socket.emit'); 228 | t.end(); 229 | }); 230 | 231 | test('gritty: onWindowResize: terminal', (t) => { 232 | const fit = stub(); 233 | 234 | _onWindowResize({ 235 | fit, 236 | }); 237 | 238 | t.calledWithNoArgs(fit, 'should call terminal.fit'); 239 | t.end(); 240 | }); 241 | 242 | function before() { 243 | global.location = {}; 244 | } 245 | 246 | function after() { 247 | delete global.location; 248 | } 249 | -------------------------------------------------------------------------------- /test/server/gritty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | 5 | const {once} = require('node:events'); 6 | const tryCatch = require('try-catch'); 7 | 8 | const {test, stub} = require('supertape'); 9 | 10 | const currify = require('currify'); 11 | const io = require('socket.io-client'); 12 | const mockRequire = require('mock-require'); 13 | const wait = require('@iocmd/wait'); 14 | 15 | const serveOnce = require('serve-once'); 16 | const gritty = require('../../'); 17 | 18 | const {connect} = require('../before'); 19 | 20 | const {request} = serveOnce(gritty); 21 | 22 | const {reRequire, stopAll} = mockRequire; 23 | 24 | test('gritty: listen: args: no', (t) => { 25 | const [error] = tryCatch(gritty.listen); 26 | 27 | t.equal(error.message, 'socket could not be empty!', 'should throw when no args'); 28 | t.end(); 29 | }); 30 | 31 | test('gritty: listen: args: auth', (t) => { 32 | const socket = {}; 33 | 34 | socket.on = stub().returns(socket); 35 | socket.of = stub().returns(socket); 36 | 37 | const [error] = tryCatch(gritty.listen, socket, { 38 | auth: 'hello', 39 | }); 40 | 41 | t.equal(error.message, 'options.auth should be a function!', 'should throw when no args'); 42 | t.end(); 43 | }); 44 | 45 | test('gritty: server: dist-dev', async (t) => { 46 | process.env.NODE_ENV = 'development'; 47 | 48 | const gritty = reRequire('../..'); 49 | const {request} = serveOnce(gritty); 50 | 51 | const {status} = await request.get('/gritty/gritty.js'); 52 | delete process.env.NODE_ENV; 53 | 54 | t.equal(status, 200, 'should return OK'); 55 | t.end(); 56 | }); 57 | 58 | test('gritty: server: dist', async (t) => { 59 | const {status} = await request.get(`/gritty/gritty.js`); 60 | 61 | t.equal(status, 200, 'should return OK'); 62 | t.end(); 63 | }); 64 | 65 | test('gritty: server: dist: not found', async (t) => { 66 | const {status} = await request.get('/gritty/not-found.js'); 67 | 68 | t.equal(status, 404, 'should return Not Found'); 69 | t.end(); 70 | }); 71 | 72 | test('gritty: server: socket: resize', async (t) => { 73 | const {port, done} = await connect(); 74 | const socket = io(`http://localhost:${port}/gritty`); 75 | 76 | await once(socket, 'connect'); 77 | socket.emit('terminal'); 78 | socket.emit('resize'); 79 | 80 | await once(socket, 'data'); 81 | socket.close(); 82 | done(); 83 | 84 | t.pass('should emit data'); 85 | t.end(); 86 | }); 87 | 88 | test('gritty: server: socket: resize: terminal options', async (t) => { 89 | const {port, done} = await connect(); 90 | const socket = io(`http://localhost:${port}/gritty`); 91 | 92 | await once(socket, 'connect'); 93 | socket.emit('terminal', { 94 | autoRestart: true, 95 | }); 96 | 97 | socket.emit('resize'); 98 | 99 | await once(socket, 'data'); 100 | socket.close(); 101 | done(); 102 | 103 | t.pass('should emit data'); 104 | t.end(); 105 | }); 106 | 107 | test('gritty: server: socket: exit', async (t) => { 108 | const {port, done} = await connect(); 109 | const socket = io(`http://localhost:${port}/gritty`); 110 | 111 | await once(socket, 'connect'); 112 | socket.emit('terminal'); 113 | 114 | await once(socket, 'data'); 115 | 116 | socket.emit('data', 'e'); 117 | socket.emit('data', 'x'); 118 | socket.emit('data', 'i'); 119 | socket.emit('data', 't'); 120 | socket.emit('data', String.fromCharCode(13)); 121 | 122 | await once(socket, 'exit'); 123 | socket.close(); 124 | done(); 125 | 126 | t.pass('should exit terminal'); 127 | t.end(); 128 | }); 129 | 130 | test('gritty: server: socket: exit: custom cmd', async (t) => { 131 | const {port, done} = await connect({ 132 | command: 'ls', 133 | autoRestart: false, 134 | }); 135 | 136 | const socket = io(`http://localhost:${port}/gritty`); 137 | 138 | await once(socket, 'connect'); 139 | socket.emit('terminal'); 140 | 141 | socket.emit('data', 'e'); 142 | socket.emit('data', 'x'); 143 | socket.emit('data', 'i'); 144 | socket.emit('data', 't'); 145 | socket.emit('data', String.fromCharCode(13)); 146 | 147 | await once(socket, 'exit'); 148 | socket.close(); 149 | done(); 150 | 151 | t.pass('should exit terminal'); 152 | t.end(); 153 | }); 154 | 155 | test('gritty: server: terminal: parse args', async (t) => { 156 | const {port, done} = await connect(); 157 | const socket = io(`http://localhost:${port}/gritty`); 158 | 159 | mockRequire('node-pty', { 160 | spawn: stub(), 161 | }); 162 | 163 | await once(socket, 'connect'); 164 | socket.emit('terminal', { 165 | command: 'bash -c "hello world"', 166 | }); 167 | 168 | const [data] = await once(socket, 'data'); 169 | socket.close(); 170 | done(); 171 | 172 | stopAll(); 173 | 174 | t.match(data, 'bash: hello: command not found'); 175 | t.end(); 176 | }); 177 | 178 | test('gritty: server: socket: emit data', async (t) => { 179 | const {port, done} = await connect(); 180 | const socket = io(`http://localhost:${port}/gritty`); 181 | 182 | await once(socket, 'connect'); 183 | socket.emit('terminal'); 184 | socket.emit('data', 'hello'); 185 | 186 | const [data] = await once(socket, 'data'); 187 | socket.close(); 188 | done(); 189 | 190 | t.equal(data, 'hello', 'should equal data'); 191 | t.end(); 192 | }); 193 | 194 | test('gritty: server: socket: auth', async (t) => { 195 | const auth = currify((accept, reject, username, password) => { 196 | if (username !== 'hello' || password !== 'world') 197 | return reject(); 198 | 199 | accept(); 200 | }); 201 | 202 | const {port, done} = await connect({ 203 | auth, 204 | }); 205 | 206 | const socket = io(`http://localhost:${port}/gritty`); 207 | 208 | await once(socket, 'connect'); 209 | 210 | socket.emit('auth', 'hello', 'world'); 211 | 212 | await once(socket, 'accept'); 213 | socket.close(); 214 | done(); 215 | 216 | t.pass('should emit accepet'); 217 | t.end(); 218 | }); 219 | 220 | test('gritty: server: socket: auth: reject', async (t) => { 221 | const auth = currify((accept, reject, username, password) => { 222 | if (username !== 'hello' || password !== 'world') 223 | return reject(); 224 | 225 | accept(); 226 | }); 227 | 228 | const {port, done} = await connect({ 229 | auth, 230 | }); 231 | 232 | const socket = io(`http://localhost:${port}/gritty`); 233 | 234 | await once(socket, 'connect'); 235 | 236 | socket.emit('auth', 'hello', 'hello'); 237 | 238 | await once(socket, 'reject'); 239 | socket.close(); 240 | done(); 241 | 242 | t.pass('should emit reject'); 243 | t.end(); 244 | }); 245 | 246 | test('gritty: server: platform', (t) => { 247 | const {platform} = process; 248 | 249 | Object.defineProperty(process, 'platform', { 250 | value: 'win32', 251 | }); 252 | 253 | reRequire('../..'); 254 | 255 | t.pass('set CMD'); 256 | 257 | Object.defineProperty(process, 'platform', { 258 | value: platform, 259 | }); 260 | 261 | t.end(); 262 | }); 263 | 264 | test('gritty: server: socket: authCheck', async (t) => { 265 | const auth = (connect, reject) => ({username, password}) => { 266 | if (username !== 'hello' || password !== 'world') 267 | return reject(); 268 | 269 | connect(); 270 | }; 271 | 272 | const {port, done} = await connect({ 273 | auth, 274 | }); 275 | 276 | const socket = io(`http://localhost:${port}/gritty`); 277 | 278 | await once(socket, 'connect'); 279 | 280 | const emit = socket.emit.bind(socket); 281 | emit('auth', { 282 | username: 'hello', 283 | password: 'world', 284 | }); 285 | 286 | await Promise.all([ 287 | once(socket, 'accept'), 288 | wait(emit, 'auth', { 289 | username: 'hello', 290 | password: 'world', 291 | }), 292 | ]); 293 | 294 | socket.close(); 295 | done(); 296 | 297 | t.pass('should emit accepet'); 298 | t.end(); 299 | }); 300 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('node:path'); 4 | 5 | const {env} = require('node:process'); 6 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 7 | 8 | const dir = './client'; 9 | const isDev = env.NODE_ENV === 'development'; 10 | 11 | const dist = path.resolve(__dirname, 'dist'); 12 | const distDev = path.resolve(__dirname, 'dist-dev'); 13 | const devtool = isDev ? 'eval' : 'source-map'; 14 | const notEmpty = (a) => a; 15 | const clean = (array) => array.filter(notEmpty); 16 | 17 | const rules = clean([ 18 | !isDev && { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | }, { 23 | test: /\.css$/, 24 | use: [ 25 | 'style-loader', 26 | 'css-loader', 27 | 'clean-css-loader', 28 | ], 29 | }]); 30 | 31 | module.exports = { 32 | devtool, 33 | entry: { 34 | gritty: `${dir}/gritty.js`, 35 | }, 36 | output: { 37 | library: 'gritty', 38 | filename: '[name].js', 39 | path: isDev ? distDev : dist, 40 | pathinfo: isDev, 41 | devtoolModuleFilenameTemplate, 42 | }, 43 | module: { 44 | rules, 45 | }, 46 | performance: { 47 | maxEntrypointSize: 500_000, 48 | maxAssetSize: 500_000, 49 | }, 50 | optimization: { 51 | minimize: true, 52 | minimizer: [ 53 | new CssMinimizerPlugin(), 54 | ], 55 | }, 56 | }; 57 | 58 | function devtoolModuleFilenameTemplate(info) { 59 | const resource = info.absoluteResourcePath.replace(__dirname + path.sep, ''); 60 | return `file://gritty/${resource}`; 61 | } 62 | --------------------------------------------------------------------------------