├── .babelrc ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ ├── npm-publish.yml │ └── npm-test.yml ├── .gitignore ├── .prettierrc.yaml ├── .vscode ├── extensions.json └── launch.json ├── README.md ├── __tests__ ├── GlobalHttpLogger.spec.ts ├── __snapshots__ │ ├── GlobalHttpLogger.spec.ts.snap │ └── validate.spec.ts.snap ├── eventToCurl.spec.ts ├── fixHeaders.spec.ts ├── globalSetup.js ├── globalTeardown.js ├── mode.spec.ts ├── server.js ├── startServer.js ├── utils.ts ├── validate.spec.ts └── wbenv │ └── WBEnv.spec.ts ├── bin └── wbenv.js ├── inject.js ├── jest.config.js ├── package.json ├── src ├── GlobalHttpLogger.ts ├── SerializedTypes.ts ├── SharedTypes.ts ├── constants.ts ├── curlLogger.ts ├── eventSerializer.ts ├── eventToCurl.ts ├── eventToPretty.ts ├── examples │ ├── curlLogger.ts │ └── inject.ts ├── fixHeaders.ts ├── getProcessData.ts ├── inject.ts ├── main.ts ├── mode.ts ├── schemas │ └── SerializedLoggerEvent.json ├── validate.ts └── wbenv │ ├── WBEnv.ts │ ├── cli.ts │ └── types.ts ├── tester.js ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/typescript" 12 | ], 13 | "plugins": [ 14 | "@babel/proposal-class-properties", 15 | "@babel/proposal-object-rest-spread", 16 | "@babel/plugin-proposal-nullish-coalescing-operator" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.202.5/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster 4 | ARG VARIANT="14-bullseye" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # Add repository for GitHub CLI 8 | RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ 9 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null 10 | 11 | # [Optional] Uncomment this section to install additional OS packages. 12 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 13 | # && apt-get -y install --no-install-recommends mc 14 | 15 | # [Optional] Uncomment if you want to install an additional version of node using nvm 16 | # ARG EXTRA_NODE_VERSION=10 17 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 18 | 19 | # [Optional] Uncomment if you want to install more global node packages 20 | # RUN su node -c "npm install -g " 21 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.202.5/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "runArgs": [ 6 | "--init" 7 | ], 8 | "build": { 9 | "dockerfile": "Dockerfile", 10 | // Update 'VARIANT' to pick a Node version: 16, 14, 12. 11 | // Append -bullseye or -buster to pin to an OS version. 12 | // Use -bullseye variants on local on arm64/Apple Silicon. 13 | "args": { 14 | "VARIANT": "14-bullseye" 15 | } 16 | }, 17 | // Set *default* container specific settings.json values on container create. 18 | "settings": {}, 19 | // Add the IDs of extensions you want installed when the container is created. 20 | "extensions": [ 21 | "dbaeumer.vscode-eslint", 22 | "maptz.regionfolder", 23 | "wmaurer.change-case", 24 | "nemesv.copy-file-name", 25 | "ryanluker.vscode-coverage-gutters", 26 | "jpruliere.env-autocomplete", 27 | "waderyan.gitblame", 28 | "github.vscode-pull-request-github", 29 | "eamodio.gitlens", 30 | "orta.vscode-jest", 31 | "cmstead.js-codeformer", 32 | "eg2.vscode-npm-script", 33 | "silvenga.positions", 34 | "esbenp.prettier-vscode", 35 | "2gua.rainbow-brackets", 36 | "unional.vscode-sort-package-json", 37 | "hbenl.vscode-test-explorer" 38 | ], 39 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 40 | "forwardPorts": [ 41 | 4380 42 | ], 43 | // Use 'postCreateCommand' to run commands after the container is created. 44 | // "postCreateCommand": "yarn", 45 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 46 | "remoteUser": "node", 47 | "containerEnv": { 48 | "NPM_TOKEN": "${localEnv:NPM_TOKEN}" 49 | }, 50 | "features": { 51 | "github-cli": "latest" 52 | } 53 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es6: true 3 | node: true 4 | 5 | parser: '@typescript-eslint/parser' 6 | parserOptions: 7 | project: tsconfig.json 8 | plugins: 9 | - '@typescript-eslint' 10 | extends: 11 | - 'eslint:recommended' 12 | - 'plugin:@typescript-eslint/eslint-recommended' 13 | - 'plugin:@typescript-eslint/recommended' 14 | 15 | rules: 16 | key-spacing: 17 | - warn 18 | - align: colon 19 | indent: 20 | - error 21 | - 4 22 | - { flatTernaryExpressions: true } 23 | linebreak-style: 24 | - error 25 | - unix 26 | quotes: 27 | - error 28 | - single 29 | semi: 30 | - error 31 | - always 32 | prefer-destructuring: 33 | - error 34 | object-shorthand: 35 | - error 36 | '@typescript-eslint/prefer-nullish-coalescing': 37 | - warn 38 | '@typescript-eslint/prefer-optional-chain': 39 | - warn 40 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish on Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 14 15 | registry-url: https://registry.npmjs.org/ 16 | - run: yarn 17 | - run: yarn lint 18 | test: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | node: [10, 12, 14, 16] 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node }} 28 | registry-url: https://registry.npmjs.org/ 29 | - run: yarn 30 | - run: yarn test 31 | 32 | publish-npm: 33 | runs-on: ubuntu-latest 34 | needs: 35 | - lint 36 | - test 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions/setup-node@v1 40 | with: 41 | node-version: 14 42 | registry-url: https://registry.npmjs.org/ 43 | - run: yarn 44 | - run: yarn publish --access=public 45 | env: 46 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 47 | -------------------------------------------------------------------------------- /.github/workflows/npm-test.yml: -------------------------------------------------------------------------------- 1 | name: Test on Push 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: 14 13 | registry-url: https://registry.npmjs.org/ 14 | - run: yarn 15 | - run: yarn lint 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | node: [10, 12, 14, 16] 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node }} 26 | registry-url: https://registry.npmjs.org/ 27 | - run: yarn 28 | - run: yarn test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # vuepress build output 84 | .vuepress/dist 85 | 86 | # Serverless directories 87 | .serverless/ 88 | 89 | # FuseBox cache 90 | .fusebox/ 91 | 92 | # DynamoDB Local files 93 | .dynamodb/ 94 | 95 | # End of https://www.gitignore.io/api/node 96 | 97 | lib 98 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | tabWidth: 4 2 | singleQuote: true 3 | trailingComma: es5 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["orta.vscode-jest", "ryanluker.vscode-coverage-gutters"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "Jest Tests", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 12 | "args": ["--runInBand"], 13 | "cwd": "${workspaceFolder}", 14 | "console": "integratedTerminal", 15 | "internalConsoleOptions": "neverOpen", 16 | "disableOptimisticBPs": true 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wirebird Client 2 | 3 | Client library for [Wirebird](https://npmjs.com/package/wirebird) http inspection tool. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install -D wirebird-client 9 | ``` 10 | 11 | or: 12 | 13 | ```sh 14 | yarn add -D wirebird-client 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### With Wirebird 20 | 21 | First, [install and run Wirebird](https://npmjs.com/package/wirebird) on your machine. 22 | 23 | Default: 24 | 25 | ```sh 26 | NODE_OPTIONS="-r wirebird-client/inject" \ 27 | WIREBIRD=ui \ 28 | node my-script.js 29 | ``` 30 | 31 | Using `wbenv` command: 32 | 33 | ```sh 34 | wbenv ui node my-script.js 35 | ``` 36 | 37 | Specify Wirebird host and port manually: 38 | 39 | ```sh 40 | NODE_OPTIONS="-r wirebird-client/inject" \ 41 | WIREBIRD=ui:http://: \ 42 | node my-script.js 43 | ``` 44 | 45 | Using `wbenv` command: 46 | 47 | ```sh 48 | wbenv -h http://: ui node my-script.js 49 | ``` 50 | 51 | ### Without Wirebird 52 | 53 | Log HTTP requests to the terminal: 54 | 55 | ```sh 56 | NODE_OPTIONS="-r wirebird-client/inject" \ 57 | WIREBIRD=pretty \ 58 | node my-script.js 59 | ``` 60 | 61 | Using `wbenv` command: 62 | 63 | ```sh 64 | wbenv pretty node my-script.js 65 | ``` 66 | 67 | Log HTTP requests to the terminal as curl commands: 68 | 69 | ```sh 70 | NODE_OPTIONS="-r wirebird-client/inject" \ 71 | WIREBIRD=curl \ 72 | node my-script.js 73 | ``` 74 | 75 | Using `wbenv` command: 76 | 77 | ```sh 78 | wbenv curl node my-script.js 79 | ``` 80 | 81 | ### Wbenv command reference 82 | 83 | `wbenv` (**W**ire**b**ird **env**ironment) command is a shell wrapper 84 | that is included with `wirebird-client` package, allowing to run a Node.js script 85 | with `WIREBIRD` and `NODE_OPTIONS` variables set. 86 | 87 | Syntax: 88 | 89 | ```sh 90 | wbenv [-h wirebird_host] {ui|curl|pretty} 91 | ``` 92 | 93 | Examples: 94 | 95 | ```sh 96 | wbenv ui npm start #runs `npm start` logging HTTP requests with Wirebird 97 | wbenv ui yarn add -D @types/react #runs `yarn add -D @types/react` logging HTTP requests with Wirebird (cool, eh?) 98 | wbenv -h http://192.168.88.1:4380 ui node app.js 99 | #runs `node app.js` logging HTTP requests with Wirebird running on http://192.168.88.1:4380 100 | wbenv curl zapier push #runs `zapier push` logging HTTP requests to terminal as Curl commands 101 | ``` -------------------------------------------------------------------------------- /__tests__/GlobalHttpLogger.spec.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import sleep from 'sleep-promise'; 3 | import GlobalHttpLogger from '../src/GlobalHttpLogger'; 4 | import { prepareSnapshot } from './utils'; 5 | 6 | const skipTick = () => sleep(0); 7 | 8 | describe('GlobalHttpLogger', () => { 9 | it('should not capture requests if shouldLog returns false', async () => { 10 | const onRequestEnd = jest.fn(); 11 | const logger = new GlobalHttpLogger({ 12 | onRequestEnd, 13 | shouldLog: (req) => !req.headers['x-do-not-track'], 14 | }); 15 | logger.start(); 16 | const res = await axios.get('http://127.0.0.1:13000/get?a=b', { 17 | headers: { 18 | 'x-do-not-track': '1', 19 | }, 20 | }); 21 | expect(res.data).toEqual('Hello World!'); 22 | expect(res.status).toEqual(200); 23 | await skipTick(); 24 | expect(onRequestEnd.mock.calls.length).toEqual(0); 25 | }); 26 | it('should capture GET requests', async () => { 27 | const onRequestEnd = jest.fn(); 28 | const logger = new GlobalHttpLogger({ onRequestEnd }); 29 | logger.start(); 30 | const res = await axios.get('http://127.0.0.1:13000/get?a=b'); 31 | expect(res.data).toEqual('Hello World!'); 32 | expect(res.status).toEqual(200); 33 | await skipTick(); 34 | expect( 35 | prepareSnapshot(onRequestEnd.mock.calls[0][0]) 36 | ).toMatchSnapshot(); 37 | }); 38 | it('should capture POST requests', async () => { 39 | const onRequestEnd = jest.fn(); 40 | const logger = new GlobalHttpLogger({ onRequestEnd }); 41 | logger.start(); 42 | const res = await axios.post('http://127.0.0.1:13000/post', { 43 | foo: 'bar', 44 | }); 45 | expect(res.data).toEqual({ hello: 'world' }); 46 | expect(res.status).toEqual(200); 47 | await skipTick(); 48 | expect( 49 | prepareSnapshot(onRequestEnd.mock.calls[0][0]) 50 | ).toMatchSnapshot(); 51 | }); 52 | it('should capture GET requests with error response', async () => { 53 | const onRequestEnd = jest.fn(); 54 | const logger = new GlobalHttpLogger({ onRequestEnd }); 55 | logger.start(); 56 | const res = await axios.get('http://127.0.0.1:13000/get/404', { 57 | validateStatus: () => true, 58 | }); 59 | expect(res.data).toEqual({ error: 'Not found' }); 60 | expect(res.status).toEqual(404); 61 | await skipTick(); 62 | expect( 63 | prepareSnapshot(onRequestEnd.mock.calls[0][0]) 64 | ).toMatchSnapshot(); 65 | }); 66 | it('should capture GET requests with network error', async () => { 67 | const onRequestEnd = jest.fn(); 68 | const logger = new GlobalHttpLogger({ onRequestEnd }); 69 | logger.start(); 70 | await expect( 71 | axios.get('http://never.existing.host.asdfgh/', { 72 | validateStatus: () => true, 73 | }) 74 | ).rejects.toThrow('getaddrinfo ENOTFOUND never.existing.host.asdfgh'); 75 | await skipTick(); 76 | const event = prepareSnapshot(onRequestEnd.mock.calls[0][0]); 77 | expect(event.request).toMatchSnapshot(); 78 | expect(event.response).toEqual(null); 79 | expect(event.error?.code).toEqual('ENOTFOUND'); 80 | expect([ 81 | 'getaddrinfo ENOTFOUND never.existing.host.asdfgh', 82 | 'getaddrinfo ENOTFOUND never.existing.host.asdfgh never.existing.host.asdfgh:80', 83 | ]).toContain(event.error?.message); 84 | }); 85 | 86 | it('should decode gzipped content', async () => { 87 | const onRequestEnd = jest.fn(); 88 | const logger = new GlobalHttpLogger({ onRequestEnd }); 89 | logger.start(); 90 | const res = await axios.get('http://127.0.0.1:13000/compressable', { 91 | headers: { 92 | 'accept-encoding': 'gzip', 93 | }, 94 | }); 95 | expect(res.data).toEqual('Hello World!'); 96 | expect(res.status).toEqual(200); 97 | await skipTick(); 98 | expect( 99 | prepareSnapshot(onRequestEnd.mock.calls[0][0]) 100 | ).toMatchSnapshot(); 101 | }); 102 | 103 | it('should allow multiple headers', async () => { 104 | const onRequestEnd = jest.fn(); 105 | const logger = new GlobalHttpLogger({ onRequestEnd }); 106 | logger.start(); 107 | const res = await axios.get('http://127.0.0.1:13000/get?a=b', { 108 | headers: { 109 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 110 | //@ts-ignore 111 | one: ['two', 'three'], 112 | }, 113 | }); 114 | expect(res.data).toEqual('Hello World!'); 115 | expect(res.status).toEqual(200); 116 | await skipTick(); 117 | expect( 118 | prepareSnapshot(onRequestEnd.mock.calls[0][0]) 119 | ).toMatchSnapshot(); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/GlobalHttpLogger.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`GlobalHttpLogger should allow multiple headers 1`] = ` 4 | Object { 5 | "error": null, 6 | "request": Object { 7 | "body": null, 8 | "headers": Object { 9 | "accept": "application/json, text/plain, */*", 10 | "host": "127.0.0.1:13000", 11 | "one": Array [ 12 | "two", 13 | "three", 14 | ], 15 | "user-agent": "axios/0.25.0", 16 | }, 17 | "id": "[presents]", 18 | "method": "GET", 19 | "remoteAddress": "127.0.0.1", 20 | "timeStart": 1, 21 | "url": "http://127.0.0.1:13000/get?a=b", 22 | }, 23 | "response": Object { 24 | "body": "Hello World!", 25 | "headers": Object { 26 | "connection": "close", 27 | "content-length": "12", 28 | "content-type": "text/html; charset=utf-8", 29 | "vary": "Accept-Encoding", 30 | "x-powered-by": "Express", 31 | }, 32 | "rawHeaders": Array [ 33 | "X-Powered-By", 34 | "Express", 35 | "Content-Type", 36 | "text/html; charset=utf-8", 37 | "Content-Length", 38 | "12", 39 | "Vary", 40 | "Accept-Encoding", 41 | "Connection", 42 | "close", 43 | ], 44 | "status": 200, 45 | "timeStart": 1, 46 | }, 47 | } 48 | `; 49 | 50 | exports[`GlobalHttpLogger should capture GET requests 1`] = ` 51 | Object { 52 | "error": null, 53 | "request": Object { 54 | "body": null, 55 | "headers": Object { 56 | "accept": "application/json, text/plain, */*", 57 | "host": "127.0.0.1:13000", 58 | "user-agent": "axios/0.25.0", 59 | }, 60 | "id": "[presents]", 61 | "method": "GET", 62 | "remoteAddress": "127.0.0.1", 63 | "timeStart": 1, 64 | "url": "http://127.0.0.1:13000/get?a=b", 65 | }, 66 | "response": Object { 67 | "body": "Hello World!", 68 | "headers": Object { 69 | "connection": "close", 70 | "content-length": "12", 71 | "content-type": "text/html; charset=utf-8", 72 | "vary": "Accept-Encoding", 73 | "x-powered-by": "Express", 74 | }, 75 | "rawHeaders": Array [ 76 | "X-Powered-By", 77 | "Express", 78 | "Content-Type", 79 | "text/html; charset=utf-8", 80 | "Content-Length", 81 | "12", 82 | "Vary", 83 | "Accept-Encoding", 84 | "Connection", 85 | "close", 86 | ], 87 | "status": 200, 88 | "timeStart": 1, 89 | }, 90 | } 91 | `; 92 | 93 | exports[`GlobalHttpLogger should capture GET requests with error response 1`] = ` 94 | Object { 95 | "error": null, 96 | "request": Object { 97 | "body": null, 98 | "headers": Object { 99 | "accept": "application/json, text/plain, */*", 100 | "host": "127.0.0.1:13000", 101 | "user-agent": "axios/0.25.0", 102 | }, 103 | "id": "[presents]", 104 | "method": "GET", 105 | "remoteAddress": "127.0.0.1", 106 | "timeStart": 1, 107 | "url": "http://127.0.0.1:13000/get/404", 108 | }, 109 | "response": Object { 110 | "body": "{\\"error\\":\\"Not found\\"}", 111 | "headers": Object { 112 | "connection": "close", 113 | "content-length": "21", 114 | "content-type": "application/json; charset=utf-8", 115 | "vary": "Accept-Encoding", 116 | "x-powered-by": "Express", 117 | }, 118 | "rawHeaders": Array [ 119 | "X-Powered-By", 120 | "Express", 121 | "Content-Type", 122 | "application/json; charset=utf-8", 123 | "Content-Length", 124 | "21", 125 | "Vary", 126 | "Accept-Encoding", 127 | "Connection", 128 | "close", 129 | ], 130 | "status": 404, 131 | "timeStart": 1, 132 | }, 133 | } 134 | `; 135 | 136 | exports[`GlobalHttpLogger should capture GET requests with network error 1`] = ` 137 | Object { 138 | "body": null, 139 | "headers": Object { 140 | "accept": "application/json, text/plain, */*", 141 | "host": "never.existing.host.asdfgh", 142 | "user-agent": "axios/0.25.0", 143 | }, 144 | "id": "[presents]", 145 | "method": "GET", 146 | "remoteAddress": null, 147 | "timeStart": 1, 148 | "url": "http://never.existing.host.asdfgh/", 149 | } 150 | `; 151 | 152 | exports[`GlobalHttpLogger should capture POST requests 1`] = ` 153 | Object { 154 | "error": null, 155 | "request": Object { 156 | "body": "{\\"foo\\":\\"bar\\"}", 157 | "headers": Object { 158 | "accept": "application/json, text/plain, */*", 159 | "content-length": 13, 160 | "content-type": "application/json", 161 | "host": "127.0.0.1:13000", 162 | "user-agent": "axios/0.25.0", 163 | }, 164 | "id": "[presents]", 165 | "method": "POST", 166 | "remoteAddress": "127.0.0.1", 167 | "timeStart": 1, 168 | "url": "http://127.0.0.1:13000/post", 169 | }, 170 | "response": Object { 171 | "body": "{\\"hello\\":\\"world\\"}", 172 | "headers": Object { 173 | "connection": "close", 174 | "content-length": "17", 175 | "content-type": "application/json; charset=utf-8", 176 | "vary": "Accept-Encoding", 177 | "x-powered-by": "Express", 178 | }, 179 | "rawHeaders": Array [ 180 | "X-Powered-By", 181 | "Express", 182 | "Content-Type", 183 | "application/json; charset=utf-8", 184 | "Content-Length", 185 | "17", 186 | "Vary", 187 | "Accept-Encoding", 188 | "Connection", 189 | "close", 190 | ], 191 | "status": 200, 192 | "timeStart": 1, 193 | }, 194 | } 195 | `; 196 | 197 | exports[`GlobalHttpLogger should decode gzipped content 1`] = ` 198 | Object { 199 | "error": null, 200 | "request": Object { 201 | "body": null, 202 | "headers": Object { 203 | "accept": "application/json, text/plain, */*", 204 | "accept-encoding": "gzip", 205 | "host": "127.0.0.1:13000", 206 | "user-agent": "axios/0.25.0", 207 | }, 208 | "id": "[presents]", 209 | "method": "GET", 210 | "remoteAddress": "127.0.0.1", 211 | "timeStart": 1, 212 | "url": "http://127.0.0.1:13000/compressable", 213 | }, 214 | "response": Object { 215 | "body": "Hello World!", 216 | "headers": Object { 217 | "connection": "close", 218 | "content-type": "text/html; charset=utf-8", 219 | "transfer-encoding": "chunked", 220 | "vary": "Accept-Encoding", 221 | "x-powered-by": "Express", 222 | }, 223 | "rawHeaders": Array [ 224 | "X-Powered-By", 225 | "Express", 226 | "Content-Type", 227 | "text/html; charset=utf-8", 228 | "Vary", 229 | "Accept-Encoding", 230 | "Content-Encoding", 231 | "gzip", 232 | "Connection", 233 | "close", 234 | "Transfer-Encoding", 235 | "chunked", 236 | ], 237 | "status": 200, 238 | "timeStart": 1, 239 | }, 240 | } 241 | `; 242 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/validate.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`validate should be a correctly serialized event 1`] = ` 4 | Object { 5 | "error": null, 6 | "processData": Object { 7 | "mainModule": "index.js", 8 | "pid": 1, 9 | "title": "node", 10 | }, 11 | "request": Object { 12 | "body": "aGVsbG8gd29ybGQ=", 13 | "headers": Object { 14 | "hello": "world", 15 | }, 16 | "id": "1", 17 | "method": "GET", 18 | "remoteAddress": "127.0.0.1", 19 | "timeStart": 0, 20 | "url": "https://example.com", 21 | }, 22 | "response": Object { 23 | "body": "aGVsbG8gd29ybGQ=", 24 | "headers": Object { 25 | "hello": "world", 26 | }, 27 | "rawHeaders": Array [ 28 | "Hello", 29 | "world", 30 | ], 31 | "status": 200, 32 | "timeStart": 0, 33 | }, 34 | } 35 | `; 36 | 37 | exports[`validate should validate an incorrect event 1`] = ` 38 | Array [ 39 | Object { 40 | "message": "does not match allOf schema <#/definitions/SerializedLoggerEventWithError> with 3 error[s]:", 41 | "property": "instance", 42 | }, 43 | Object { 44 | "message": "is not of a type(s) object", 45 | "property": "instance.error", 46 | }, 47 | Object { 48 | "message": "is not of a type(s) string", 49 | "property": "instance.request.url", 50 | }, 51 | Object { 52 | "message": "is not of a type(s) null", 53 | "property": "instance.response", 54 | }, 55 | Object { 56 | "message": "does not match allOf schema <#/definitions/SerializedLoggerEventWithResponse> with 1 error[s]:", 57 | "property": "instance", 58 | }, 59 | Object { 60 | "message": "is not of a type(s) string", 61 | "property": "instance.request.url", 62 | }, 63 | Object { 64 | "message": "is not any of [subschema 0],[subschema 1]", 65 | "property": "instance", 66 | }, 67 | ] 68 | `; 69 | -------------------------------------------------------------------------------- /__tests__/eventToCurl.spec.ts: -------------------------------------------------------------------------------- 1 | import eventToCurl from '../src/eventToCurl'; 2 | import { LoggerEvent } from '../src/SharedTypes'; 3 | 4 | const mockEvent: LoggerEvent = { 5 | error: { 6 | code : '', 7 | message: '', 8 | stack : '', 9 | }, 10 | request: { 11 | id : '', 12 | timeStart : 0, 13 | remoteAddress: '', 14 | body : Buffer.from('hello', 'utf8'), 15 | method : 'PUT', 16 | url : 'http://127.0.0.1:8080/test/endpoint', 17 | headers : { 18 | foo: 'bar', 19 | }, 20 | }, 21 | response: null, 22 | }; 23 | 24 | describe('eventToCurl', () => { 25 | it('should serialize a request', () => { 26 | expect(eventToCurl(mockEvent)).toEqual( 27 | 'curl \'http://127.0.0.1:8080/test/endpoint\' -X PUT -H \'foo: bar\' --data-binary hello --compressed -i' 28 | ); 29 | }); 30 | 31 | it('should serialize a request as a pretty-printed command', () => { 32 | expect(eventToCurl(mockEvent, { prettyPrint: true })).toEqual( 33 | `curl 'http://127.0.0.1:8080/test/endpoint' \\ 34 | -X PUT \\ 35 | -H 'foo: bar' \\ 36 | --data-binary hello \\ 37 | --compressed \\ 38 | -i` 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/fixHeaders.spec.ts: -------------------------------------------------------------------------------- 1 | import { fixHeaders } from '../src/fixHeaders'; 2 | 3 | describe('fixHeaders', () => { 4 | it('should convert headers like {0:"foo",1:"bar"} into ["foo", "bar"]', () => { 5 | expect( 6 | fixHeaders({ 7 | num : 1, 8 | str : 'hello', 9 | arr : ['pupa', 'lupa'], 10 | obj : { 0: 'biba', 1: 'boba' }, 11 | undef: undefined, 12 | }) 13 | ).toEqual({ 14 | num : 1, 15 | str : 'hello', 16 | arr : ['pupa', 'lupa'], 17 | obj : ['biba', 'boba'], 18 | undef: undefined, 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/globalSetup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const server = require('./server'); 3 | 4 | module.exports = () => { 5 | console.log('Starting server'); 6 | server.start(); 7 | }; 8 | -------------------------------------------------------------------------------- /__tests__/globalTeardown.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const server = require('./server'); 3 | 4 | module.exports = () => { 5 | console.log('Stopping server'); 6 | server.stop(); 7 | }; 8 | -------------------------------------------------------------------------------- /__tests__/mode.spec.ts: -------------------------------------------------------------------------------- 1 | import { getMode, Mode } from '../src/mode'; 2 | 3 | describe('getMode', () => { 4 | it('should parse curl mode', () => { 5 | const m = getMode('curl'); 6 | expect(m).toEqual({ 7 | type: 'curl', 8 | }); 9 | }); 10 | it('should parse pretty mode', () => { 11 | const m = getMode('pretty'); 12 | expect(m).toEqual({ 13 | type: 'pretty', 14 | }); 15 | }); 16 | it('should parse ui mode', () => { 17 | const m = getMode('ui'); 18 | expect(m).toEqual({ 19 | type: 'ui', 20 | url : 'http://localhost:4380', 21 | }); 22 | }); 23 | it('should parse ui mode with a colon', () => { 24 | const m = getMode('ui:'); 25 | expect(m).toEqual({ 26 | type: 'ui', 27 | url : 'http://localhost:4380', 28 | }); 29 | }); 30 | it('should parse ui mode with URL provided', () => { 31 | const m = getMode('ui:https://example.com:2000'); 32 | expect(m).toEqual({ 33 | type: 'ui', 34 | url : 'https://example.com:2000', 35 | }); 36 | }); 37 | it('should parse empty as disabled', () => { 38 | const m = getMode(''); 39 | expect(m).toEqual({ 40 | type: 'disabled', 41 | }); 42 | }); 43 | it('should parse garbage as disabled', () => { 44 | const m = getMode('helloworld'); 45 | expect(m).toEqual({ 46 | type: 'disabled', 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const express = require('express'); 3 | const compression = require('compression'); 4 | const app = express(); 5 | const port = 13000; 6 | 7 | app.use( 8 | compression({ 9 | threshold: '5' 10 | }) 11 | ); 12 | 13 | app.get('/get', (req, res) => res.send('Hello World!')); 14 | app.post('/post', (req, res) => res.json({ hello: 'world' })); 15 | app.get('/get/404', (req, res) => res.status(404).json({ error: 'Not found' })); 16 | app.get('/compressable', (req, res) => res.send('Hello World!')); 17 | 18 | module.exports = { 19 | start() { 20 | return new Promise(resolve => { 21 | this.server = app.listen(port, () => { 22 | console.log(`Listening on port ${port}!`); 23 | resolve(); 24 | }); 25 | }); 26 | }, 27 | stop() { 28 | this.server.close(); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /__tests__/startServer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | require('./server').start(); 3 | -------------------------------------------------------------------------------- /__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | import { LoggerEvent } from '../src/SharedTypes'; 2 | import cloneDeep from 'lodash/cloneDeep'; 3 | import chunk from 'lodash/chunk'; 4 | import flatten from 'lodash/flatten'; 5 | 6 | const removeUnstableRawHeaders = (headers: string[]): string[] => 7 | flatten( 8 | chunk(headers, 2).filter( 9 | ([name]) => !['date', 'etag'].includes(name.toLowerCase()) 10 | ) 11 | ); 12 | 13 | export const removeUnstableData = (input: LoggerEvent): LoggerEvent => { 14 | const output = cloneDeep(input) as any; 15 | if (output.request.timeStart) { 16 | output.request.timeStart = 1; 17 | } 18 | if (output.request.id) { 19 | output.request.id = '[presents]'; 20 | } 21 | if (output.response) { 22 | if (output.response.headers) { 23 | delete output.response.headers.date; 24 | delete output.response.headers.etag; 25 | } 26 | if (output.response.timeStart) { 27 | output.response.timeStart = 1; 28 | } 29 | if (output.response.rawHeaders) { 30 | output.response.rawHeaders = removeUnstableRawHeaders( 31 | output.response.rawHeaders 32 | ); 33 | } 34 | } 35 | if (output.error) { 36 | delete output.error.stack; 37 | } 38 | 39 | return output; 40 | }; 41 | 42 | export const withReadableBuffers = (input: LoggerEvent): LoggerEvent => { 43 | const output = cloneDeep(input) as any; 44 | if (output.request?.body) { 45 | output.request.body = output.request.body.toString('utf8'); 46 | } 47 | if (output.response?.body) { 48 | output.response.body = output.response.body.toString('utf8'); 49 | } 50 | return output; 51 | }; 52 | 53 | export const prepareSnapshot = (input: LoggerEvent): LoggerEvent => 54 | withReadableBuffers(removeUnstableData(input)); 55 | -------------------------------------------------------------------------------- /__tests__/validate.spec.ts: -------------------------------------------------------------------------------- 1 | import { serializeEvent } from '../src/eventSerializer'; 2 | import { LoggerEvent } from '../src/main'; 3 | import { ProcessData } from '../src/SharedTypes'; 4 | import { validate } from '../src/validate'; 5 | 6 | describe('validate', () => { 7 | const event: LoggerEvent = { 8 | request: { 9 | id : '1', 10 | body : new Buffer('hello world', 'utf8'), 11 | headers: { 12 | hello: 'world', 13 | }, 14 | method : 'GET', 15 | timeStart : 0, 16 | remoteAddress: '127.0.0.1', 17 | url : 'https://example.com', 18 | }, 19 | response: { 20 | body : new Buffer('hello world', 'utf8'), 21 | headers: { 22 | hello: 'world', 23 | }, 24 | rawHeaders: ['Hello', 'world'], 25 | status : 200, 26 | timeStart : 0, 27 | }, 28 | error: null, 29 | }; 30 | 31 | const processData: ProcessData = { 32 | mainModule: 'index.js', 33 | pid : 1, 34 | title : 'node', 35 | }; 36 | const serializedEvent = serializeEvent(event, processData); 37 | 38 | it('should be a correctly serialized event', () => { 39 | expect(serializedEvent).toMatchSnapshot(); 40 | }); 41 | 42 | it('should validate a correct event', () => { 43 | const res = validate(serializedEvent); 44 | expect(res.errors).toEqual([]); 45 | expect(res.valid).toEqual(true); 46 | }); 47 | 48 | it('should validate an incorrect event', () => { 49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 50 | const wrongEvent = serializedEvent as any; 51 | const res = validate({ 52 | ...wrongEvent, 53 | request: { ...wrongEvent.request, url: null }, 54 | }); 55 | expect( 56 | res.errors.map((e) => ({ 57 | message : e.message, 58 | property: e.property, 59 | })) 60 | ).toMatchSnapshot(); 61 | expect(res.valid).toEqual(false); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /__tests__/wbenv/WBEnv.spec.ts: -------------------------------------------------------------------------------- 1 | import { ISpawn } from '../../src/wbenv/types'; 2 | import { WBEnv } from '../../src/wbenv/WBEnv'; 3 | 4 | describe('WBEnv', () => { 5 | it('should return 1 if no command provided', () => { 6 | const mockSpawn = jest.fn() as jest.MockedFunction; 7 | const wbenv = new WBEnv( 8 | mockSpawn, 9 | { FOO: 'bar' }, 10 | 'wirebird-client/inject' 11 | ); 12 | const exitCode = wbenv.execute([]); 13 | expect(exitCode).toEqual(1); 14 | expect(mockSpawn).not.toBeCalled(); 15 | }); 16 | 17 | it('should return 1 if no command provided (with -h)', () => { 18 | const mockSpawn = jest.fn() as jest.MockedFunction; 19 | const wbenv = new WBEnv( 20 | mockSpawn, 21 | { FOO: 'bar' }, 22 | 'wirebird-client/inject' 23 | ); 24 | const exitCode = wbenv.execute('-h http://localhost:8080'.split(' ')); 25 | expect(exitCode).toEqual(1); 26 | expect(mockSpawn).not.toBeCalled(); 27 | }); 28 | 29 | it('should execute ui command with non-standart host by default', () => { 30 | const mockSpawn = jest.fn() as jest.MockedFunction; 31 | const wbenv = new WBEnv( 32 | mockSpawn, 33 | { FOO: 'bar' }, 34 | 'wirebird-client/inject' 35 | ); 36 | const exitCode = wbenv.execute( 37 | '-h http://localhost:8080 yarn -D add hello'.split(' ') 38 | ); 39 | expect(exitCode).toEqual(0); 40 | expect(mockSpawn.mock.calls).toMatchInlineSnapshot(` 41 | Array [ 42 | Array [ 43 | "yarn", 44 | Array [ 45 | "-D", 46 | "add", 47 | "hello", 48 | ], 49 | Object { 50 | "env": Object { 51 | "FOO": "bar", 52 | "NODE_OPTIONS": "--require wirebird-client/inject", 53 | "WIREBIRD": "ui:http://localhost:8080", 54 | }, 55 | "stdio": "inherit", 56 | }, 57 | ], 58 | ] 59 | `); 60 | }); 61 | 62 | it('should execute ui command by default', () => { 63 | const mockSpawn = jest.fn() as jest.MockedFunction; 64 | const wbenv = new WBEnv( 65 | mockSpawn, 66 | { FOO: 'bar' }, 67 | 'wirebird-client/inject' 68 | ); 69 | const exitCode = wbenv.execute('yarn -D add hello'.split(' ')); 70 | expect(exitCode).toEqual(0); 71 | expect(mockSpawn.mock.calls).toMatchInlineSnapshot(` 72 | Array [ 73 | Array [ 74 | "yarn", 75 | Array [ 76 | "-D", 77 | "add", 78 | "hello", 79 | ], 80 | Object { 81 | "env": Object { 82 | "FOO": "bar", 83 | "NODE_OPTIONS": "--require wirebird-client/inject", 84 | "WIREBIRD": "ui", 85 | }, 86 | "stdio": "inherit", 87 | }, 88 | ], 89 | ] 90 | `); 91 | }); 92 | 93 | it('should execute ui command containing -options', () => { 94 | const mockSpawn = jest.fn() as jest.MockedFunction; 95 | const wbenv = new WBEnv( 96 | mockSpawn, 97 | { FOO: 'bar' }, 98 | 'wirebird-client/inject' 99 | ); 100 | const exitCode = wbenv.execute('ui yarn -D add hello'.split(' ')); 101 | expect(exitCode).toEqual(0); 102 | expect(mockSpawn.mock.calls).toMatchInlineSnapshot(` 103 | Array [ 104 | Array [ 105 | "yarn", 106 | Array [ 107 | "-D", 108 | "add", 109 | "hello", 110 | ], 111 | Object { 112 | "env": Object { 113 | "FOO": "bar", 114 | "NODE_OPTIONS": "--require wirebird-client/inject", 115 | "WIREBIRD": "ui", 116 | }, 117 | "stdio": "inherit", 118 | }, 119 | ], 120 | ] 121 | `); 122 | }); 123 | 124 | it('should execute ui command', () => { 125 | const mockSpawn = jest.fn() as jest.MockedFunction; 126 | const wbenv = new WBEnv( 127 | mockSpawn, 128 | { FOO: 'bar' }, 129 | 'wirebird-client/inject' 130 | ); 131 | const exitCode = wbenv.execute('ui node my-server.js'.split(' ')); 132 | expect(exitCode).toEqual(0); 133 | expect(mockSpawn.mock.calls).toMatchInlineSnapshot(` 134 | Array [ 135 | Array [ 136 | "node", 137 | Array [ 138 | "my-server.js", 139 | ], 140 | Object { 141 | "env": Object { 142 | "FOO": "bar", 143 | "NODE_OPTIONS": "--require wirebird-client/inject", 144 | "WIREBIRD": "ui", 145 | }, 146 | "stdio": "inherit", 147 | }, 148 | ], 149 | ] 150 | `); 151 | }); 152 | 153 | it('should execute curl command', () => { 154 | const mockSpawn = jest.fn() as jest.MockedFunction; 155 | const wbenv = new WBEnv( 156 | mockSpawn, 157 | { FOO: 'bar' }, 158 | 'wirebird-client/inject' 159 | ); 160 | const exitCode = wbenv.execute('curl node my-server.js'.split(' ')); 161 | expect(exitCode).toEqual(0); 162 | expect(mockSpawn.mock.calls).toMatchInlineSnapshot(` 163 | Array [ 164 | Array [ 165 | "node", 166 | Array [ 167 | "my-server.js", 168 | ], 169 | Object { 170 | "env": Object { 171 | "FOO": "bar", 172 | "NODE_OPTIONS": "--require wirebird-client/inject", 173 | "WIREBIRD": "curl", 174 | }, 175 | "stdio": "inherit", 176 | }, 177 | ], 178 | ] 179 | `); 180 | }); 181 | 182 | it('should execute pretty command', () => { 183 | const mockSpawn = jest.fn() as jest.MockedFunction; 184 | const wbenv = new WBEnv( 185 | mockSpawn, 186 | { FOO: 'bar' }, 187 | 'wirebird-client/inject' 188 | ); 189 | const exitCode = wbenv.execute('pretty node my-server.js'.split(' ')); 190 | expect(exitCode).toEqual(0); 191 | expect(mockSpawn.mock.calls).toMatchInlineSnapshot(` 192 | Array [ 193 | Array [ 194 | "node", 195 | Array [ 196 | "my-server.js", 197 | ], 198 | Object { 199 | "env": Object { 200 | "FOO": "bar", 201 | "NODE_OPTIONS": "--require wirebird-client/inject", 202 | "WIREBIRD": "pretty", 203 | }, 204 | "stdio": "inherit", 205 | }, 206 | ], 207 | ] 208 | `); 209 | }); 210 | 211 | it('should execute ui command merging the existing NODE_OPTIONS', () => { 212 | const mockSpawn = jest.fn() as jest.MockedFunction; 213 | const wbenv = new WBEnv( 214 | mockSpawn, 215 | { 216 | FOO : 'bar', 217 | NODE_OPTIONS: '--foo=bar', 218 | }, 219 | 'wirebird-client/inject' 220 | ); 221 | const exitCode = wbenv.execute('ui node my-server.js'.split(' ')); 222 | expect(exitCode).toEqual(0); 223 | expect(mockSpawn.mock.calls).toMatchInlineSnapshot(` 224 | Array [ 225 | Array [ 226 | "node", 227 | Array [ 228 | "my-server.js", 229 | ], 230 | Object { 231 | "env": Object { 232 | "FOO": "bar", 233 | "NODE_OPTIONS": "--foo=bar --require wirebird-client/inject", 234 | "WIREBIRD": "ui", 235 | }, 236 | "stdio": "inherit", 237 | }, 238 | ], 239 | ] 240 | `); 241 | }); 242 | 243 | it('should execute ui command with -h parameter', () => { 244 | const mockSpawn = jest.fn() as jest.MockedFunction; 245 | const wbenv = new WBEnv( 246 | mockSpawn, 247 | { FOO: 'bar' }, 248 | 'wirebird-client/inject' 249 | ); 250 | const exitCode = wbenv.execute( 251 | '-h http://localhost:8080 ui node my-server.js'.split(' ') 252 | ); 253 | expect(exitCode).toEqual(0); 254 | expect(mockSpawn.mock.calls).toMatchInlineSnapshot(` 255 | Array [ 256 | Array [ 257 | "node", 258 | Array [ 259 | "my-server.js", 260 | ], 261 | Object { 262 | "env": Object { 263 | "FOO": "bar", 264 | "NODE_OPTIONS": "--require wirebird-client/inject", 265 | "WIREBIRD": "ui:http://localhost:8080", 266 | }, 267 | "stdio": "inherit", 268 | }, 269 | ], 270 | ] 271 | `); 272 | }); 273 | 274 | it('should execute ui command passing all the rest parameters to the sub-command', () => { 275 | const mockSpawn = jest.fn() as jest.MockedFunction; 276 | const wbenv = new WBEnv( 277 | mockSpawn, 278 | { FOO: 'bar' }, 279 | 'wirebird-client/inject' 280 | ); 281 | const exitCode = wbenv.execute( 282 | 'ui node my-server.js -h hello'.split(' ') 283 | ); 284 | expect(exitCode).toEqual(0); 285 | expect(mockSpawn.mock.calls).toMatchInlineSnapshot(` 286 | Array [ 287 | Array [ 288 | "node", 289 | Array [ 290 | "my-server.js", 291 | "-h", 292 | "hello", 293 | ], 294 | Object { 295 | "env": Object { 296 | "FOO": "bar", 297 | "NODE_OPTIONS": "--require wirebird-client/inject", 298 | "WIREBIRD": "ui", 299 | }, 300 | "stdio": "inherit", 301 | }, 302 | ], 303 | ] 304 | `); 305 | }); 306 | 307 | it('should execute ui command with -h parameter passing all the rest parameters to the sub-command', () => { 308 | const mockSpawn = jest.fn() as jest.MockedFunction; 309 | const wbenv = new WBEnv( 310 | mockSpawn, 311 | { FOO: 'bar' }, 312 | 'wirebird-client/inject' 313 | ); 314 | const exitCode = wbenv.execute( 315 | '-h http://localhost:8080 ui node my-server.js -- -h hello -m world'.split( 316 | ' ' 317 | ) 318 | ); 319 | expect(exitCode).toEqual(0); 320 | expect(mockSpawn.mock.calls).toMatchInlineSnapshot(` 321 | Array [ 322 | Array [ 323 | "node", 324 | Array [ 325 | "my-server.js", 326 | "-h", 327 | "hello", 328 | "-m", 329 | "world", 330 | ], 331 | Object { 332 | "env": Object { 333 | "FOO": "bar", 334 | "NODE_OPTIONS": "--require wirebird-client/inject", 335 | "WIREBIRD": "ui:http://localhost:8080", 336 | }, 337 | "stdio": "inherit", 338 | }, 339 | ], 340 | ] 341 | `); 342 | }); 343 | }); 344 | -------------------------------------------------------------------------------- /bin/wbenv.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/wbenv/cli'); 4 | -------------------------------------------------------------------------------- /inject.js: -------------------------------------------------------------------------------- 1 | require('./lib/inject'); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/tmp/jest_rs", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | collectCoverage: true, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | globalSetup: './__tests__/globalSetup.js', 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | globalTeardown: './__tests__/globalTeardown.js', 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | // preset: null, 92 | 93 | // Run tests from one or more projects 94 | // projects: null, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: null, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: null, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: 'node', 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | testMatch: ['**/__tests__/**/*.(spec|test).[jt]s?(x)'] 142 | 143 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 144 | // testPathIgnorePatterns: [ 145 | // "/node_modules/" 146 | // ], 147 | 148 | // The regexp pattern or array of patterns that Jest uses to detect test files 149 | // testRegex: [], 150 | 151 | // This option allows the use of a custom results processor 152 | // testResultsProcessor: null, 153 | 154 | // This option allows use of a custom test runner 155 | // testRunner: "jasmine2", 156 | 157 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 158 | // testURL: "http://localhost", 159 | 160 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 161 | // timers: "real", 162 | 163 | // A map from regular expressions to paths to transformers 164 | // transform: null, 165 | 166 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 167 | // transformIgnorePatterns: [ 168 | // "/node_modules/" 169 | // ], 170 | 171 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 172 | // unmockedModulePathPatterns: undefined, 173 | 174 | // Indicates whether each individual test should be reported during the run 175 | // verbose: null, 176 | 177 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 178 | // watchPathIgnorePatterns: [], 179 | 180 | // Whether to use watchman for file crawling 181 | // watchman: true, 182 | }; 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wirebird-client", 3 | "version": "0.2.4", 4 | "description": "DevTools / Network for Node.js", 5 | "keywords": [ 6 | "http-inspector", 7 | "mitm", 8 | "devtools", 9 | "development", 10 | "network", 11 | "http", 12 | "https", 13 | "sniffer", 14 | "logger", 15 | "debugger", 16 | "network debugger" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/wirebird-js/wirebird-client" 21 | }, 22 | "license": "WTFPL", 23 | "author": "corporateanon ", 24 | "main": "lib/main.js", 25 | "types": "lib/src/main.d.ts", 26 | "bin": { 27 | "wbenv": "./bin/wbenv.js" 28 | }, 29 | "files": [ 30 | "lib", 31 | "bin", 32 | "inject.js", 33 | "README.md" 34 | ], 35 | "scripts": { 36 | "build": "npm run build:types && npm run build:js", 37 | "build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline --copy-files", 38 | "build:types": "tsc --emitDeclarationOnly", 39 | "example:curlLogger": "npm run build:js && node ./lib/examples/curlLogger.js", 40 | "example:inject": "npm run build:js && wbenv pretty node -r ./inject ./lib/examples/inject.js", 41 | "example:inject:monitor": "npm run build:js && wbenv ui node -r ./inject ./lib/examples/inject.js", 42 | "generate:schema": "mkdir -p src/schemas && typescript-json-schema --strictNullChecks --required src/SerializedTypes.ts SerializedLoggerEvent > src/schemas/SerializedLoggerEvent.json", 43 | "lint": "eslint .", 44 | "prepublish": "npm run build", 45 | "start-test-server": "node ./__tests__/startServer.js", 46 | "test": "jest", 47 | "tester": "wbenv ui node tester.js", 48 | "type-check": "tsc --noEmit", 49 | "type-check:watch": "npm run type-check -- --watch" 50 | }, 51 | "dependencies": { 52 | "axios": "^0.25.0", 53 | "dedent": "^0.7.0", 54 | "hyperid": "^3.0.0", 55 | "jsonschema": "^1.4.0", 56 | "minimist": "^1.2.5", 57 | "shell-escape": "^0.2.0", 58 | "sleep-promise": "^8.0.1", 59 | "zlib": "^1.0.5" 60 | }, 61 | "devDependencies": { 62 | "@babel/cli": "^7.5.5", 63 | "@babel/core": "^7.5.5", 64 | "@babel/plugin-proposal-class-properties": "^7.5.5", 65 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", 66 | "@babel/plugin-proposal-numeric-separator": "^7.2.0", 67 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5", 68 | "@babel/preset-env": "^7.5.5", 69 | "@babel/preset-typescript": "^7.3.3", 70 | "@types/dedent": "^0.7.0", 71 | "@types/jest": "^27.4.0", 72 | "@types/lodash": "^4.14.165", 73 | "@types/minimist": "^1.2.2", 74 | "@types/nanoid": "^2.0.0", 75 | "@types/request-promise-native": "^1.0.16", 76 | "@types/shell-escape": "^0.2.0", 77 | "@types/superagent": "^4.1.3", 78 | "@typescript-eslint/eslint-plugin": "^4.10.0", 79 | "@typescript-eslint/parser": "^4.10.0", 80 | "compression": "^1.7.4", 81 | "eslint": "^7.16.0", 82 | "express": "^4.17.1", 83 | "jest": "^27.4.7", 84 | "lodash": "^4.17.15", 85 | "prettier": "^2.5.1", 86 | "request-promise-native": "^1.0.7", 87 | "superagent": "^5.1.0", 88 | "typescript": "^3.5.3", 89 | "typescript-json-schema": "^0.40.0" 90 | }, 91 | "engines": { 92 | "node": ">=8" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/GlobalHttpLogger.ts: -------------------------------------------------------------------------------- 1 | import http, { ClientRequest, IncomingMessage } from 'http'; 2 | import https from 'https'; 3 | import hyperid from 'hyperid'; 4 | import { createUnzip, Unzip } from 'zlib'; 5 | import { fixHeaders } from './fixHeaders'; 6 | import { 7 | LoggerEvent, 8 | LoggerEventHandler, 9 | LoggerRequest, 10 | LoggerResponse, 11 | LoggerShouldLog, 12 | Timestamp, 13 | } from './SharedTypes'; 14 | 15 | const uuid = hyperid(); 16 | 17 | const matches = process.version.match(/^v(\d+)\.(\d+)\.(\d+)$/); 18 | const nodeMajorVersion = matches ? +matches[1] : 0; 19 | 20 | interface ClientRequestWithUndocumentedMembers extends ClientRequest { 21 | agent: any; 22 | method: string; 23 | _headers: { [headerName: string]: string }; 24 | } 25 | 26 | class ResponseBodyCollector { 27 | buffers: Buffer[]; 28 | 29 | bodyPromise: Promise; 30 | 31 | private shouldUnzip(response: IncomingMessage): boolean { 32 | const encoding = response.headers['content-encoding']; 33 | const status = response.statusCode; 34 | return ( 35 | !!encoding && 36 | ['gzip', 'deflate', 'compress'].includes(encoding) && 37 | status !== 204 38 | ); 39 | } 40 | 41 | private unzip(response: IncomingMessage): Unzip { 42 | const stream = response.pipe(createUnzip()); 43 | return stream; 44 | } 45 | 46 | constructor(response: IncomingMessage) { 47 | this.buffers = []; 48 | 49 | const stream = this.shouldUnzip(response) 50 | ? this.unzip(response) 51 | : response; 52 | 53 | this.bodyPromise = new Promise((resolve) => { 54 | stream.on('data', (chunk) => { 55 | if (typeof chunk === 'string') { 56 | this.buffers.push(new Buffer(chunk, 'utf8')); 57 | } else { 58 | this.buffers.push(chunk); 59 | } 60 | }); 61 | stream.on('end', () => { 62 | const body = Buffer.concat(this.buffers); 63 | resolve(body); 64 | }); 65 | }); 66 | } 67 | 68 | getBodyAsync(): Promise { 69 | return this.bodyPromise; 70 | } 71 | } 72 | 73 | const waitForRequestRemoteAddress = ( 74 | request: ClientRequest 75 | ): Promise => 76 | new Promise((resolve) => { 77 | request.prependOnceListener('socket', (socket) => { 78 | socket.on('connect', () => resolve(socket.remoteAddress ?? null)); 79 | socket.on('error', () => resolve(null)); 80 | socket.on('close', () => resolve(null)); 81 | }); 82 | }); 83 | 84 | const waitForResponseOrError = ( 85 | request: ClientRequest 86 | ): Promise<{ 87 | response?: IncomingMessage; 88 | responseBodyCollector?: ResponseBodyCollector; 89 | responseTimeStart?: Timestamp; 90 | error?: Error; 91 | }> => 92 | new Promise((resolve) => { 93 | request.prependOnceListener('response', (response) => { 94 | const responseTimeStart = Date.now(); 95 | const responseBodyCollector = new ResponseBodyCollector(response); 96 | resolve({ response, responseBodyCollector, responseTimeStart }); 97 | }); 98 | request.prependOnceListener('error', (error) => { 99 | resolve({ error }); 100 | }); 101 | }); 102 | 103 | const collectRequestBody = (request: ClientRequest): Promise => 104 | new Promise((resolve) => { 105 | const requestBody: Buffer[] = []; 106 | 107 | const reqWrite = request.write; 108 | request.write = function (...args: any) { 109 | /** 110 | * chunk can be either a string or a Buffer. 111 | */ 112 | const [chunk] = args; 113 | 114 | if (Buffer.isBuffer(chunk)) { 115 | requestBody.push(chunk); 116 | } else { 117 | requestBody.push(Buffer.from(chunk, 'utf8')); 118 | } 119 | 120 | return reqWrite.apply(this, args); 121 | }; 122 | 123 | const reqEnd = request.end; 124 | request.end = function (...args: any) { 125 | /** 126 | * the first argument might be a callback or a chunk 127 | */ 128 | const [maybeChunk] = args; 129 | 130 | if (Buffer.isBuffer(maybeChunk)) { 131 | requestBody.push(maybeChunk); 132 | } else if (maybeChunk && typeof maybeChunk !== 'function') { 133 | requestBody.push(Buffer.from(maybeChunk, 'utf8')); 134 | } 135 | 136 | return reqEnd.apply(this, args); 137 | }; 138 | 139 | request.prependOnceListener('finish', () => { 140 | if (!requestBody.length) { 141 | resolve(null); 142 | } else { 143 | resolve(Buffer.concat(requestBody)); 144 | } 145 | }); 146 | 147 | request.prependOnceListener('error', () => resolve(null)); 148 | }); 149 | 150 | const interceptRequest = async ( 151 | request: ClientRequest, 152 | onRequestEnd: (payload: LoggerEvent) => void, 153 | shouldLog?: LoggerShouldLog 154 | ) => { 155 | const { protocol } = (request as ClientRequestWithUndocumentedMembers) 156 | .agent; 157 | const host = request.getHeader('host'); 158 | const { path } = request; 159 | 160 | const loggerRequest: LoggerRequest = { 161 | id : uuid(), 162 | timeStart : Date.now(), 163 | url : `${protocol}//${host}${path}`, 164 | method : (request as ClientRequestWithUndocumentedMembers).method, 165 | headers : fixHeaders(request.getHeaders()), 166 | body : null, 167 | remoteAddress: null, 168 | }; 169 | 170 | if (shouldLog && !shouldLog(loggerRequest)) { 171 | return; 172 | } 173 | 174 | const [ 175 | remoteAddress, 176 | requestBody, 177 | { response, responseBody, responseTimeStart, error }, 178 | ] = await Promise.all([ 179 | waitForRequestRemoteAddress(request), 180 | collectRequestBody(request), 181 | (async () => { 182 | const { 183 | response, 184 | responseBodyCollector, 185 | responseTimeStart, 186 | error, 187 | } = await waitForResponseOrError(request); 188 | if (response && responseBodyCollector) { 189 | return { 190 | response, 191 | responseTimeStart, 192 | responseBody: await responseBodyCollector.getBodyAsync(), 193 | error : null, 194 | }; 195 | } else if (error) { 196 | return { 197 | response : null, 198 | responseBody: null, 199 | error, 200 | }; 201 | } else { 202 | throw new Error('No responseBodyCollector'); 203 | } 204 | })(), 205 | ]); 206 | loggerRequest.body = requestBody ?? null; 207 | loggerRequest.remoteAddress = remoteAddress; 208 | 209 | if (response) { 210 | const loggerResponse: LoggerResponse = { 211 | timeStart : responseTimeStart ?? 0, 212 | status : response.statusCode ?? 0, 213 | body : responseBody ?? null, 214 | headers : response.headers, 215 | rawHeaders: response.rawHeaders, 216 | }; 217 | onRequestEnd({ 218 | request : loggerRequest, 219 | response: loggerResponse, 220 | error : null, 221 | }); 222 | } else if (error) { 223 | const loggerError = { 224 | message: error.message, 225 | code : (error as any).code as string, 226 | stack : error.stack ?? '', 227 | }; 228 | onRequestEnd({ 229 | request : loggerRequest, 230 | response: null, 231 | error : loggerError, 232 | }); 233 | } 234 | }; 235 | 236 | export interface GlobalHttpLoggerOptions { 237 | onRequestEnd: LoggerEventHandler; 238 | shouldLog?: LoggerShouldLog; 239 | } 240 | 241 | export default class GlobalHttpLogger { 242 | private onRequestEnd: LoggerEventHandler; 243 | private shouldLog?: LoggerShouldLog; 244 | constructor({ onRequestEnd, shouldLog }: GlobalHttpLoggerOptions) { 245 | this.onRequestEnd = onRequestEnd; 246 | this.shouldLog = shouldLog; 247 | } 248 | start(): void { 249 | const { onRequestEnd } = this; 250 | const interceptedRequestMethod = ( 251 | object: any, 252 | func: any, 253 | ...rest: any 254 | ) => { 255 | const req = func.call(object, ...rest); 256 | interceptRequest(req, onRequestEnd, this.shouldLog); 257 | return req; 258 | }; 259 | 260 | http.request = interceptedRequestMethod.bind(null, http, http.request); 261 | http.get = interceptedRequestMethod.bind(null, http, http.get); 262 | 263 | /** 264 | * https.request proxies to http.request for 8.x and earlier versions 265 | */ 266 | if (nodeMajorVersion > 8) { 267 | https.get = interceptedRequestMethod.bind(null, https, https.get); 268 | https.request = interceptedRequestMethod.bind( 269 | null, 270 | https, 271 | https.request 272 | ); 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/SerializedTypes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LoggerError, 3 | BaseLoggerRequest, 4 | BaseLoggerResponse, 5 | MonitorMetadata 6 | } from './SharedTypes'; 7 | 8 | export type SerializedLoggerRequest = BaseLoggerRequest; 9 | export type SerializedLoggerResponse = BaseLoggerResponse; 10 | 11 | export interface SerializedLoggerEventWithError { 12 | request: SerializedLoggerRequest; 13 | response: null; 14 | error: LoggerError; 15 | } 16 | 17 | export interface SerializedLoggerEventWithResponse { 18 | request: SerializedLoggerRequest; 19 | response: SerializedLoggerResponse; 20 | error: null; 21 | } 22 | 23 | export type SerializedLoggerEvent = ( 24 | | SerializedLoggerEventWithResponse 25 | | SerializedLoggerEventWithError) & 26 | MonitorMetadata; 27 | -------------------------------------------------------------------------------- /src/SharedTypes.ts: -------------------------------------------------------------------------------- 1 | export type Timestamp = number; 2 | export type Uid = string; 3 | 4 | export interface LoggerHeaders { 5 | [headerName: string]: string | number | string[] | undefined; 6 | } 7 | 8 | export type LoggerResponseRawHeaders = string[]; 9 | 10 | export interface BaseLoggerRequest { 11 | id: Uid; 12 | timeStart: Timestamp; 13 | url: string; 14 | body: T | null; 15 | headers: LoggerHeaders; 16 | method: string; 17 | remoteAddress: string | null; 18 | } 19 | 20 | export interface BaseLoggerResponse { 21 | timeStart: Timestamp; 22 | body: T | null; 23 | headers: LoggerHeaders; 24 | rawHeaders: LoggerResponseRawHeaders; 25 | status: number; 26 | } 27 | 28 | export type LoggerRequest = BaseLoggerRequest; 29 | export type LoggerResponse = BaseLoggerResponse; 30 | 31 | export interface LoggerError { 32 | code: string; 33 | message: string; 34 | stack: string; 35 | } 36 | 37 | export interface LoggerEventWithError { 38 | request: LoggerRequest; 39 | response: null; 40 | error: LoggerError; 41 | } 42 | 43 | export interface LoggerEventWithResponse { 44 | request: LoggerRequest; 45 | response: LoggerResponse; 46 | error: null; 47 | } 48 | 49 | export interface ProcessData { 50 | pid: number; 51 | title: string; 52 | mainModule: string; 53 | } 54 | export interface MonitorMetadata { 55 | processData: ProcessData; 56 | } 57 | 58 | export type LoggerEvent = LoggerEventWithResponse | LoggerEventWithError; 59 | export type MonitorEvent = LoggerEvent & MonitorMetadata; 60 | 61 | export type LoggerEventHandler = (payload: LoggerEvent) => void; 62 | export type LoggerShouldLog = (req: LoggerRequest) => boolean; 63 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const WIREBIRD_DEFAULT_HOST = 'http://localhost:4380'; 2 | -------------------------------------------------------------------------------- /src/curlLogger.ts: -------------------------------------------------------------------------------- 1 | import GlobalHttpLogger from './GlobalHttpLogger'; 2 | import eventToCurl from './eventToCurl'; 3 | import { LoggerEvent } from './SharedTypes'; 4 | 5 | export const startCurlLogger = (): void => { 6 | const logger = new GlobalHttpLogger({ 7 | onRequestEnd: (event: LoggerEvent) => { 8 | const curlCommand = eventToCurl(event); 9 | console.log(curlCommand); 10 | }, 11 | }); 12 | logger.start(); 13 | }; 14 | -------------------------------------------------------------------------------- /src/eventSerializer.ts: -------------------------------------------------------------------------------- 1 | import { SerializedLoggerEvent } from './SerializedTypes'; 2 | import { LoggerEvent, ProcessData } from './SharedTypes'; 3 | 4 | function serializeBody(data: T): T & { body?: string } { 5 | const _data = (data as any) as { body?: Buffer }; 6 | if (!_data.body) { 7 | return data; 8 | } 9 | 10 | return { 11 | ...data, 12 | body: _data.body.toString('base64') 13 | }; 14 | } 15 | 16 | export const serializeEvent = ( 17 | event: LoggerEvent, 18 | processData: ProcessData 19 | ): SerializedLoggerEvent => { 20 | if (event.response) { 21 | return { 22 | request : serializeBody(event.request), 23 | response: serializeBody(event.response), 24 | error : null, 25 | processData 26 | }; 27 | } 28 | 29 | if (event.error) { 30 | return { 31 | request : serializeBody(event.request), 32 | response: null, 33 | error : event.error, 34 | processData 35 | }; 36 | } 37 | 38 | throw new Error('Cannot serialize'); 39 | }; 40 | -------------------------------------------------------------------------------- /src/eventToCurl.ts: -------------------------------------------------------------------------------- 1 | import { LoggerEvent } from './SharedTypes'; 2 | import escape from 'shell-escape'; 3 | 4 | interface EventToCurlOptions { 5 | prettyPrint?: boolean; 6 | } 7 | 8 | export default function eventToCurl( 9 | event: LoggerEvent, 10 | { prettyPrint = false }: EventToCurlOptions = {} 11 | ): string { 12 | const { request } = event; 13 | const lines: string[][] = []; 14 | lines.push(['curl', request.url]); 15 | lines.push(['-X', request.method]); 16 | 17 | for (const [key, value] of Object.entries(request.headers)) { 18 | lines.push(['-H', `${key}: ${value}`]); 19 | } 20 | 21 | if (request.body) { 22 | lines.push(['--data-binary', request.body.toString('utf8')]); 23 | } 24 | 25 | lines.push(['--compressed']); 26 | lines.push(['-i']); 27 | 28 | const separator = prettyPrint ? ' \\\n' : ' '; 29 | 30 | return lines.map((line) => escape(line)).join(separator); 31 | } 32 | -------------------------------------------------------------------------------- /src/eventToPretty.ts: -------------------------------------------------------------------------------- 1 | import { LoggerEvent, LoggerHeaders, LoggerError } from './SharedTypes'; 2 | 3 | const LINE_SEP = '------------------------------------------------'; 4 | 5 | const renderHeading = ({ request, response, error }: LoggerEvent) => { 6 | const statusLine = response ? response.status : error ? error.message : ''; 7 | return `${request.method} ${request.url}\n${ 8 | request.remoteAddress ?? 'IP unknown' 9 | }\n > ${statusLine}`; 10 | }; 11 | 12 | const renderHeaders = (headers: LoggerHeaders) => 13 | Object.keys(headers) 14 | .map((name) => ` ${name}: ${headers[name]}`) 15 | .join('\n'); 16 | 17 | const renderBody = (headers: LoggerHeaders, body: Buffer | null) => 18 | body ? body.toString('utf8') : ' '; 19 | 20 | const renderError = (error: LoggerError) => { 21 | return [ 22 | ` Code: ${error.code}`, 23 | ` Message: ${error.message}`, 24 | ` Stack: ${error.stack}`, 25 | ].join('\n'); 26 | }; 27 | 28 | export default function eventToPretty(event: LoggerEvent): string { 29 | const parts = []; 30 | parts.push(LINE_SEP); 31 | parts.push(renderHeading(event)); 32 | parts.push('Request headers:'); 33 | parts.push(renderHeaders(event.request.headers)); 34 | parts.push('Request body:'); 35 | parts.push(renderBody(event.request.headers, event.request.body)); 36 | 37 | if (event.response) { 38 | parts.push('Response headers:'); 39 | parts.push(renderHeaders(event.response.headers)); 40 | parts.push('Response body:'); 41 | parts.push(renderBody(event.response.headers, event.response.body)); 42 | } 43 | 44 | if (event.error) { 45 | parts.push('Error:'); 46 | parts.push(renderError(event.error)); 47 | } 48 | parts.push(LINE_SEP); 49 | 50 | return parts.join('\n'); 51 | } 52 | -------------------------------------------------------------------------------- /src/examples/curlLogger.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import superagent from 'superagent'; 3 | import request from 'request-promise-native'; 4 | 5 | import { startCurlLogger } from '../curlLogger'; 6 | 7 | const main = async () => { 8 | startCurlLogger(); 9 | await axios.get('https://example.com'); 10 | await axios.get('https://google.com'); 11 | 12 | await axios.post('https://httpbin.org/post', { hello: 'axios' }); 13 | await superagent 14 | .post('https://httpbin.org/post') 15 | .send({ hello: 'superagent' }) 16 | .end(); 17 | await request({ 18 | method: 'post', 19 | uri : 'https://httpbin.org/post', 20 | json : true, 21 | body : { hello: 'request' } 22 | }); 23 | }; 24 | 25 | main().catch(e => { 26 | console.error(e.stack); 27 | process.exit(1); 28 | }); 29 | -------------------------------------------------------------------------------- /src/examples/inject.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import superagent from 'superagent'; 3 | import request from 'request-promise-native'; 4 | 5 | const main = async () => { 6 | await axios.get('https://example.com'); 7 | await axios.get('https://google.com'); 8 | 9 | await axios.post('https://httpbin.org/post', { hello: 'axios' }); 10 | await superagent 11 | .post('https://httpbin.org/post') 12 | .send({ hello: 'superagent' }) 13 | .end(); 14 | await request({ 15 | method: 'post', 16 | uri : 'https://httpbin.org/post', 17 | json : true, 18 | body : { hello: 'request' }, 19 | }); 20 | await axios.get('https://non-existing.oewihfw3oeihfoiwe78hfo.com'); 21 | }; 22 | 23 | main().catch((e) => { 24 | console.error(e.stack); 25 | process.exit(1); 26 | }); 27 | -------------------------------------------------------------------------------- /src/fixHeaders.ts: -------------------------------------------------------------------------------- 1 | import { OutgoingHttpHeader, OutgoingHttpHeaders } from 'http'; 2 | 3 | interface BrokenHeader { 4 | [key: string]: string; 5 | } 6 | 7 | interface BrokenHeaders { 8 | [key: string]: OutgoingHttpHeader | BrokenHeader | undefined; 9 | } 10 | 11 | const fixHeaderValue = ( 12 | brokenValue: OutgoingHttpHeader | BrokenHeader | undefined 13 | ): OutgoingHttpHeader | undefined => { 14 | if (typeof brokenValue === 'object' && !Array.isArray(brokenValue)) { 15 | return Object.keys(brokenValue).map((key) => brokenValue[key]); 16 | } 17 | return brokenValue; 18 | }; 19 | 20 | export const fixHeaders = ( 21 | brokenHeaders: BrokenHeaders 22 | ): OutgoingHttpHeaders => { 23 | const output: OutgoingHttpHeaders = {}; 24 | 25 | for (const key of Object.keys(brokenHeaders)) { 26 | output[key] = fixHeaderValue(brokenHeaders[key]); 27 | } 28 | 29 | return output; 30 | }; 31 | -------------------------------------------------------------------------------- /src/getProcessData.ts: -------------------------------------------------------------------------------- 1 | import { ProcessData } from './SharedTypes'; 2 | 3 | function getProcessData(): ProcessData { 4 | return { 5 | pid : process.pid, 6 | title : process.title || '', 7 | mainModule: require.main ? require.main.filename : '', 8 | }; 9 | } 10 | 11 | export default getProcessData; 12 | -------------------------------------------------------------------------------- /src/inject.ts: -------------------------------------------------------------------------------- 1 | import GlobalHttpLogger from './GlobalHttpLogger'; 2 | import eventToPretty from './eventToPretty'; 3 | import eventToCurl from './eventToCurl'; 4 | import { LoggerEvent, ProcessData } from './SharedTypes'; 5 | import axios from 'axios'; 6 | import { serializeEvent } from './eventSerializer'; 7 | import getProcessData from './getProcessData'; 8 | import { getMode } from './mode'; 9 | 10 | const UI_ENDPOINT_PATH = '/api/updates'; 11 | const DNT_HEADER = 'wirebird-do-not-track'; 12 | 13 | async function sendEventToMonitor( 14 | uiUrl: string, 15 | event: LoggerEvent, 16 | processData: ProcessData 17 | ) { 18 | try { 19 | await axios.post(uiUrl, serializeEvent(event, processData), { 20 | headers: { 21 | [DNT_HEADER]: '1', 22 | }, 23 | }); 24 | } catch (e) { 25 | console.error( 26 | `[wirebird] Failed to send event to ${uiUrl} ${ 27 | (e as Error).message 28 | }` 29 | ); 30 | } 31 | } 32 | 33 | export const main = (): void => { 34 | const env = 35 | process.env.WIREBIRD ?? 36 | //HTTP_INSPECTOR name is left for the backwards compatibility 37 | process.env.HTTP_INSPECTOR ?? 38 | ''; 39 | const mode = getMode(env); 40 | 41 | if (mode.type === 'disabled') { 42 | return; 43 | } 44 | 45 | console.error('[wirebird] 🐤 Attached'); 46 | 47 | const processData = getProcessData(); 48 | 49 | const logger = new GlobalHttpLogger({ 50 | shouldLog: (req) => { 51 | if (mode.type !== 'ui') { 52 | return true; 53 | } 54 | if (req.headers[DNT_HEADER]) { 55 | // Don't log own requests 56 | return false; 57 | } 58 | return true; 59 | }, 60 | onRequestEnd: (event: LoggerEvent) => { 61 | if (mode.type === 'ui') { 62 | sendEventToMonitor( 63 | `${mode.url}${UI_ENDPOINT_PATH}`, 64 | event, 65 | processData 66 | ); 67 | } 68 | if (mode.type === 'curl') { 69 | console.log(eventToCurl(event)); 70 | return; 71 | } 72 | if (mode.type === 'pretty') { 73 | console.log(eventToPretty(event)); 74 | return; 75 | } 76 | }, 77 | }); 78 | logger.start(); 79 | }; 80 | 81 | main(); 82 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import GlobalHttpLogger from './GlobalHttpLogger'; 2 | import { LoggerEvent, MonitorEvent } from './SharedTypes'; 3 | import { SerializedLoggerEvent } from './SerializedTypes'; 4 | import { validate, schema } from './validate'; 5 | import eventToCurl from './eventToCurl'; 6 | 7 | export { 8 | GlobalHttpLogger, 9 | LoggerEvent, 10 | SerializedLoggerEvent, 11 | MonitorEvent, 12 | validate, 13 | schema, 14 | eventToCurl, 15 | }; 16 | -------------------------------------------------------------------------------- /src/mode.ts: -------------------------------------------------------------------------------- 1 | import { WIREBIRD_DEFAULT_HOST } from './constants'; 2 | 3 | interface ModeSimple { 4 | type: 'curl' | 'pretty' | 'disabled'; 5 | } 6 | interface ModeUI { 7 | type: 'ui'; 8 | url: string; 9 | } 10 | export type Mode = ModeUI | ModeSimple; 11 | 12 | export const getMode = (envStr: string): Mode => { 13 | if (envStr === 'curl') { 14 | return { type: 'curl' }; 15 | } 16 | if (envStr === 'pretty') { 17 | return { type: 'pretty' }; 18 | } 19 | 20 | const defaultURL = WIREBIRD_DEFAULT_HOST; 21 | const uiPrefix = 'ui:'; 22 | 23 | if (envStr === 'ui') { 24 | return { 25 | type: 'ui', 26 | url : defaultURL, 27 | }; 28 | } 29 | 30 | if (envStr.startsWith(uiPrefix)) { 31 | return { 32 | type: 'ui', 33 | url : envStr.substr(uiPrefix.length) || defaultURL, 34 | }; 35 | } 36 | return { 37 | type: 'disabled', 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/schemas/SerializedLoggerEvent.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "anyOf": [ 4 | { 5 | "allOf": [ 6 | { 7 | "$ref": "#/definitions/SerializedLoggerEventWithError" 8 | }, 9 | { 10 | "$ref": "#/definitions/MonitorMetadata" 11 | } 12 | ] 13 | }, 14 | { 15 | "allOf": [ 16 | { 17 | "$ref": "#/definitions/SerializedLoggerEventWithResponse" 18 | }, 19 | { 20 | "$ref": "#/definitions/MonitorMetadata" 21 | } 22 | ] 23 | } 24 | ], 25 | "definitions": { 26 | "LoggerError": { 27 | "properties": { 28 | "code": { 29 | "type": "string" 30 | }, 31 | "message": { 32 | "type": "string" 33 | }, 34 | "stack": { 35 | "type": "string" 36 | } 37 | }, 38 | "required": [ 39 | "code", 40 | "message", 41 | "stack" 42 | ], 43 | "type": "object" 44 | }, 45 | "LoggerHeaders": { 46 | "additionalProperties": { 47 | "anyOf": [ 48 | { 49 | "items": { 50 | "type": "string" 51 | }, 52 | "type": "array" 53 | }, 54 | { 55 | "type": [ 56 | "string", 57 | "number" 58 | ] 59 | } 60 | ] 61 | }, 62 | "type": "object" 63 | }, 64 | "MonitorMetadata": { 65 | "properties": { 66 | "processData": { 67 | "$ref": "#/definitions/ProcessData" 68 | } 69 | }, 70 | "required": [ 71 | "processData" 72 | ], 73 | "type": "object" 74 | }, 75 | "ProcessData": { 76 | "properties": { 77 | "mainModule": { 78 | "type": "string" 79 | }, 80 | "pid": { 81 | "type": "number" 82 | }, 83 | "title": { 84 | "type": "string" 85 | } 86 | }, 87 | "required": [ 88 | "mainModule", 89 | "pid", 90 | "title" 91 | ], 92 | "type": "object" 93 | }, 94 | "SerializedLoggerEventWithError": { 95 | "properties": { 96 | "error": { 97 | "$ref": "#/definitions/LoggerError" 98 | }, 99 | "request": { 100 | "$ref": "#/definitions/SerializedLoggerRequest" 101 | }, 102 | "response": { 103 | "type": "null" 104 | } 105 | }, 106 | "required": [ 107 | "error", 108 | "request", 109 | "response" 110 | ], 111 | "type": "object" 112 | }, 113 | "SerializedLoggerEventWithResponse": { 114 | "properties": { 115 | "error": { 116 | "type": "null" 117 | }, 118 | "request": { 119 | "$ref": "#/definitions/SerializedLoggerRequest" 120 | }, 121 | "response": { 122 | "$ref": "#/definitions/SerializedLoggerResponse" 123 | } 124 | }, 125 | "required": [ 126 | "error", 127 | "request", 128 | "response" 129 | ], 130 | "type": "object" 131 | }, 132 | "SerializedLoggerRequest": { 133 | "properties": { 134 | "body": { 135 | "type": [ 136 | "null", 137 | "string" 138 | ] 139 | }, 140 | "headers": { 141 | "$ref": "#/definitions/LoggerHeaders" 142 | }, 143 | "id": { 144 | "type": "string" 145 | }, 146 | "method": { 147 | "type": "string" 148 | }, 149 | "remoteAddress": { 150 | "type": [ 151 | "null", 152 | "string" 153 | ] 154 | }, 155 | "timeStart": { 156 | "type": "number" 157 | }, 158 | "url": { 159 | "type": "string" 160 | } 161 | }, 162 | "required": [ 163 | "body", 164 | "headers", 165 | "id", 166 | "method", 167 | "remoteAddress", 168 | "timeStart", 169 | "url" 170 | ], 171 | "type": "object" 172 | }, 173 | "SerializedLoggerResponse": { 174 | "properties": { 175 | "body": { 176 | "type": [ 177 | "null", 178 | "string" 179 | ] 180 | }, 181 | "headers": { 182 | "$ref": "#/definitions/LoggerHeaders" 183 | }, 184 | "rawHeaders": { 185 | "items": { 186 | "type": "string" 187 | }, 188 | "type": "array" 189 | }, 190 | "status": { 191 | "type": "number" 192 | }, 193 | "timeStart": { 194 | "type": "number" 195 | } 196 | }, 197 | "required": [ 198 | "body", 199 | "headers", 200 | "rawHeaders", 201 | "status", 202 | "timeStart" 203 | ], 204 | "type": "object" 205 | } 206 | } 207 | } 208 | 209 | -------------------------------------------------------------------------------- /src/validate.ts: -------------------------------------------------------------------------------- 1 | import { SerializedLoggerEvent } from './SerializedTypes'; 2 | import { validate as schemaValidate, ValidatorResult } from 'jsonschema'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-var-requires 5 | export const schema = require('./schemas/SerializedLoggerEvent.json'); 6 | 7 | export const validate = (data: SerializedLoggerEvent): ValidatorResult => { 8 | return schemaValidate(data, schema, { nestedErrors: true }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/wbenv/WBEnv.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent'; 2 | import minimist from 'minimist'; 3 | import { ISpawn } from './types'; 4 | 5 | export class WBEnv { 6 | private spawn(command: string, args: string[], env: string) { 7 | const oldNodeOptions = this.processEnv.NODE_OPTIONS ?? ''; 8 | const wirebirdClientInject = this.injectScriptPath; 9 | const nodeOptions = 10 | `${oldNodeOptions} --require ${wirebirdClientInject}`.trimStart(); 11 | 12 | const envs = { NODE_OPTIONS: nodeOptions, WIREBIRD: env }; 13 | const envsForLog = Object.entries(envs) 14 | .map(([k, v]) => `${k}="${v}"`) 15 | .join(' '); 16 | const argsForLog = args.join(' '); 17 | 18 | console.error(`[wbenv] ${envsForLog} ${command} ${argsForLog}`); 19 | 20 | this.childProcessSpawn(command, args, { 21 | stdio: 'inherit', 22 | env : { 23 | ...this.processEnv, 24 | NODE_OPTIONS: nodeOptions, 25 | WIREBIRD : env, 26 | }, 27 | }); 28 | } 29 | 30 | private parse(argv: string[]): number { 31 | const opts = minimist(argv, { stopEarly: true }); 32 | const host = opts.h; 33 | const [first] = opts._; 34 | const envPassed = ['ui', 'curl', 'pretty'].includes(first); 35 | 36 | const positionals = envPassed ? opts._ : ['ui', ...opts._]; 37 | 38 | const [env, command, ...args] = positionals; 39 | if (!command) { 40 | this.printUsage(); 41 | return 1; 42 | } 43 | let envStr = env; 44 | if (env === 'ui' && host) { 45 | envStr = `ui:${host}`; 46 | } 47 | 48 | this.spawn(command, args, envStr); 49 | return 0; 50 | } 51 | 52 | private printUsage() { 53 | console.error(dedent` 54 | usage: wbenv [{ui|curl|pretty}] command [args...] 55 | ui - send requests to Wirebird app 56 | curl - log requests in the terminal as Curl commands 57 | pretty - log requests in the terminal`); 58 | } 59 | 60 | constructor( 61 | private childProcessSpawn: ISpawn, 62 | private processEnv: Record, 63 | private injectScriptPath: string 64 | ) {} 65 | 66 | execute(argv: string[]): number { 67 | return this.parse(argv); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/wbenv/cli.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process'; 2 | import { WBEnv } from './WBEnv'; 3 | 4 | const wbenv = new WBEnv(spawnSync, process.env, require.resolve('../inject')); 5 | process.exit(wbenv.execute(process.argv.slice(2))); 6 | -------------------------------------------------------------------------------- /src/wbenv/types.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process'; 2 | 3 | export type ISpawn = typeof spawnSync; 4 | -------------------------------------------------------------------------------- /tester.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const Axios = require('axios'); 3 | const qs = require('querystring'); 4 | const sleep = require('sleep-promise'); 5 | 6 | const requests = [ 7 | ['get', 'https://example.com'], 8 | ['get', 'https://httpbin.org/xml'], 9 | [ 10 | 'get', 11 | 'https://httpbin.org/get', 12 | { 13 | headers: { 14 | hello: ['foo', 'bar'], 15 | }, 16 | }, 17 | ], 18 | ['post', 'https://example.com', {}], 19 | [ 20 | 'post', 21 | 'https://example.com/form', 22 | qs.stringify({ foo: 'bar', items: [1, 2, 3] }), 23 | { 24 | headers: { 25 | 'Content-Type': 'application/x-www-form-urlencoded', 26 | }, 27 | }, 28 | ], 29 | ['get', 'https://example.com/does-not-exist'], 30 | ['get', 'https://iueugfroiruthgi-does-not-exist.com'], 31 | ['get', 'https://www.fillmurray.com/250/250'], 32 | ['get', 'https://jsonplaceholder.typicode.com/todos'], 33 | ['post', 'https://httpbin.org/post', { hello: 'world' }], 34 | ]; 35 | 36 | let currentRequest = 0; 37 | 38 | async function ping() { 39 | currentRequest++; 40 | currentRequest = currentRequest % requests.length; 41 | const [method, url, params] = requests[currentRequest]; 42 | try { 43 | await Axios[method](url, params); 44 | } catch (e) { 45 | console.log(`Error: ${e.message}`); 46 | } 47 | } 48 | 49 | async function main() { 50 | for (;;) { 51 | await ping(); 52 | await sleep(1000); 53 | } 54 | } 55 | 56 | main(); 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "lib", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | --------------------------------------------------------------------------------