├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_suggestion.md │ └── request_analytics_data_access.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── renovate.json └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .lintstagedrc.json ├── .node-version ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .yarnrc ├── ANALYTICS.md ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RESEARCH.md ├── docs ├── brand │ ├── archie-small.png │ ├── archie-small.svg │ ├── archie.png │ ├── archie.sketch │ ├── archie.svg │ ├── avatar.png │ ├── icon.png │ ├── logo-gh-social.png │ ├── logo-no-margin.png │ ├── logo-no-margin.svg │ ├── logo.png │ ├── logo.sketch │ └── logo.svg ├── component-overview.drawio.svg ├── demo.gif ├── demo.mp4 ├── live-operator-logs.gif ├── live-operator-logs.mp4 ├── manage-operator-log-points.gif ├── manage-operator-log-points.mp4 ├── system-interactions-sequence-diagram.mermaid ├── system-interactions-sequence-diagram.svg ├── toggle-log-points.gif └── toggle-log-points.mp4 ├── lerna.json ├── nx.json ├── package.json ├── packages ├── extension-integrationtest │ ├── jest-runner-vscode.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── __snapshots__ │ │ │ └── extension.test.ts.snap │ │ ├── extension.test.ts │ │ └── util │ │ │ ├── openAndShowTextDocument.ts │ │ │ └── waitForExtension.ts │ └── tsconfig.json ├── extension │ ├── .gitignore │ ├── .vscodeignore │ ├── jest.config.js │ ├── jest │ │ └── unit │ │ │ └── setup.js │ ├── package.json │ ├── package.nls.json │ ├── resources │ │ ├── debug-breakpoint-log-unverified.svg │ │ ├── debug-breakpoint-log.svg │ │ ├── rxjs-operator-log-point-all-enabled.svg │ │ ├── rxjs-operator-log-point-disabled.svg │ │ └── rxjs-operator-log-point-some-enabled.svg │ ├── rollup.config.js │ ├── src │ │ ├── __mocks__ │ │ │ └── vscode.ts │ │ ├── analytics │ │ │ ├── __mocks__ │ │ │ │ └── posthog-node.ts │ │ │ ├── askToOptInToAnalyticsReporter.ts │ │ │ ├── events.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── isBuiltInOperatorName.test.ts │ │ │ ├── isBuiltInOperatorName.ts │ │ │ └── posthogConfiguration.ts │ │ ├── colors.ts │ │ ├── commands │ │ │ ├── commands.ts │ │ │ ├── executeCommand.ts │ │ │ ├── getMarkdownCommandWithArgs.ts │ │ │ ├── operatorLogPointManagement.ts │ │ │ ├── registerCommand.ts │ │ │ └── toggleOperatorLogPointGutterIcon.ts │ │ ├── configuration │ │ │ ├── configurationAccessor.ts │ │ │ └── index.ts │ │ ├── debugConfigurationProvider.ts │ │ ├── decoration │ │ │ ├── decorationManager.ts │ │ │ ├── decorationSetter.ts │ │ │ ├── index.ts │ │ │ ├── liveLogDecorationProvider.ts │ │ │ ├── operatorLogPointDecorationProvider │ │ │ │ ├── createHoverMessageForLogPoint.ts │ │ │ │ └── index.ts │ │ │ └── operatorLogPointGutterIconDecorationProvider │ │ │ │ ├── getEnabledState.ts │ │ │ │ ├── getIconForEnabledState.ts │ │ │ │ └── index.ts │ │ ├── extension.ts │ │ ├── global.d.ts │ │ ├── integrationTest │ │ │ ├── decorationSetterSpy.ts │ │ │ ├── executeCommand.ts │ │ │ ├── index.ts │ │ │ ├── prepareForIntegrationTest.ts │ │ │ ├── registerTestCommand.ts │ │ │ └── testCommands.ts │ │ ├── ioc │ │ │ ├── disposableContainer.ts │ │ │ ├── rootContainer.ts │ │ │ ├── sessionContainer.ts │ │ │ └── types.ts │ │ ├── logger │ │ │ ├── console.ts │ │ │ └── index.ts │ │ ├── operatorLogPoint │ │ │ ├── index.fixture.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── manager │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── merger.test.ts │ │ │ │ └── merger.ts │ │ │ └── recommender │ │ │ │ ├── index.ts │ │ │ │ ├── parser.test.ts │ │ │ │ └── parser.ts │ │ ├── resources.ts │ │ ├── sessionManager │ │ │ ├── cdpClientAddressProvider.ts │ │ │ ├── index.ts │ │ │ ├── session.test.ts │ │ │ └── session.ts │ │ ├── telemetryBridge │ │ │ ├── cdpClient.ts │ │ │ ├── cdpClientProvider.ts │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── util │ │ │ ├── environmentInfo.ts │ │ │ ├── map │ │ │ │ ├── difference.test.ts │ │ │ │ ├── difference.ts │ │ │ │ ├── intersection.test.ts │ │ │ │ └── intersection.ts │ │ │ └── types.ts │ │ └── workspaceMonitor │ │ │ ├── detector.ts │ │ │ ├── index.ts │ │ │ └── isSupportedDocument.ts │ └── tsconfig.json ├── runtime-nodejs │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── global.d.ts │ │ └── index.ts │ └── tsconfig.json ├── runtime-webpack │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── docs │ │ ├── demo.gif │ │ └── demo.mp4 │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── RxJSDebuggingPlugin.ts │ │ ├── index.ts │ │ ├── instrumentation.ts │ │ ├── loader.ts │ │ └── webpackTelemetryBridge.ts │ └── tsconfig.json ├── runtime │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── consts.ts │ │ ├── global.d.ts │ │ ├── instrumentation │ │ │ └── operatorLogPoint │ │ │ │ ├── createOperatorLogPointTelemetrySubscriber.ts │ │ │ │ ├── index.ts │ │ │ │ ├── operate.test.ts │ │ │ │ ├── operate.ts │ │ │ │ ├── operatorIdentifierFromStackFrame.test.ts │ │ │ │ ├── operatorIdentifierFromStackFrame.ts │ │ │ │ ├── patchObservable.test.ts │ │ │ │ └── patchObservable.ts │ │ ├── telemetryBridge.test.ts │ │ ├── telemetryBridge.ts │ │ └── utils │ │ │ ├── isRxJSImport.test.ts │ │ │ ├── isRxJSImport.ts │ │ │ ├── runtimeType.ts │ │ │ └── waitForCDPBindings.ts │ └── tsconfig.json ├── telemetry │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── match.ts │ │ ├── observableEvent │ │ │ ├── index.ts │ │ │ └── match.ts │ │ ├── operatorIdentifier │ │ │ ├── index.ts │ │ │ ├── serialize.test.ts │ │ │ ├── serialize.ts │ │ │ ├── toString.test.ts │ │ │ └── toString.ts │ │ ├── serialize.test.ts │ │ └── serialize.ts │ └── tsconfig.json ├── testbench-nodejs-typescript │ ├── .gitignore │ ├── .vscode │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ ├── package.json │ ├── src │ │ ├── node.ts │ │ └── observable.ts │ └── tsconfig.json ├── testbench-nodejs │ ├── .gitignore │ ├── .vscode │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ ├── package.json │ └── src │ │ ├── commonjs │ │ ├── .eslintrc.js │ │ ├── index.js │ │ └── observable.js │ │ └── esmodules │ │ ├── index.mjs │ │ └── observable.mjs └── testbench-webpack │ ├── .gitignore │ ├── .vscode │ ├── launch.json │ ├── settings.json │ └── tasks.json │ ├── package.json │ ├── src │ ├── browser.ts │ └── observable.ts │ ├── tsconfig.json │ └── webpack.config.mjs ├── renovate.json ├── tsconfig.base.json ├── workspace.code-workspace ├── workspace.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | trim_trailing_whitespaces = true 10 | indent_style = space 11 | quote_type = single 12 | indent_size = 2 13 | tab_width = 2 14 | end_of_line = lf 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | ecmaVersion: 12, 10 | sourceType: 'module', 11 | }, 12 | plugins: ['prettier', '@typescript-eslint'], 13 | rules: { 14 | '@typescript-eslint/no-empty-function': [1], 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ts text eol=lf 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | ## Version Overview 10 | 11 | - Operating System: 12 | - Visual Studio Code: 13 | - "RxJS Debugging for Visual Studio Code": 14 | 15 | 16 | 17 | ## Description 18 | 19 | 20 | 21 | ## How to Reproduce 22 | 23 | 24 | 25 | 1. Go to '...' 26 | 2. Click on '....' 27 | 3. Scroll down to '....' 28 | 4. See error 29 | 30 | ## Expected Behavior 31 | 32 | 33 | 34 | ## Screenshots/Video 35 | 36 | 37 | 38 | ## Additional Context 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Suggestion 3 | about: Suggest an idea 4 | title: 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | ## Suggestion 10 | 11 | 12 | 13 | ## Advantages 14 | 15 | 16 | 17 | 18 | ## Risks 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/request_analytics_data_access.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Analytics Data Access 3 | about: Request access to collected usage analytics data 4 | title: 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Context 2 | 3 | - Fixes # 4 | 5 | ## Description 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "labels": ["dependencies"], 3 | "extends": ["config:base"], 4 | "prConcurrentLimit": 5 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '26 0 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | packages/*/out/ 3 | .vscode-test/ 4 | .dependencies/ 5 | *.log 6 | *.vsix 7 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,js,json,yml}": ["prettier --write"] 3 | } 4 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 14.18.1 2 | 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["nrwl.angular-console"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 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 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Testbench: NodeJS", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}/packages/extension", 14 | "${workspaceFolder}/packages/testbench-nodejs" 15 | ], 16 | "outFiles": ["${workspaceFolder}/packages/extension/out/**/*.js"] 17 | }, 18 | { 19 | "name": "Testbench: NodeJS with TypeScript", 20 | "type": "extensionHost", 21 | "request": "launch", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}/packages/extension", 24 | "${workspaceFolder}/packages/testbench-nodejs-typescript" 25 | ], 26 | "outFiles": ["${workspaceFolder}/packages/extension/out/**/*.js"] 27 | }, 28 | { 29 | "name": "Testbench: Webpack", 30 | "type": "extensionHost", 31 | "request": "launch", 32 | "args": [ 33 | "--extensionDevelopmentPath=${workspaceFolder}/packages/extension", 34 | "${workspaceFolder}/packages/testbench-webpack" 35 | ], 36 | "outFiles": ["${workspaceFolder}/packages/extension/out/**/*.js"] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "**/out": true // set this to false to include "out" folder in search results 5 | }, 6 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 7 | "typescript.tsc.autoDetect": "off", 8 | "typescript.tsdk": "node_modules/typescript/lib", 9 | 10 | "jest.disabledWorkspaceFolders": ["packages/extension-integrationtest"] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "extension: Build and Watch", 8 | "type": "nx", 9 | "project": "extension", 10 | "command": "build --watch", 11 | "isBackground": true, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | }, 16 | "presentation": { 17 | "reveal": "silent", 18 | "panel": "new" 19 | }, 20 | "problemMatcher": { 21 | "pattern": { 22 | "regexp": "\\(!\\)" 23 | }, 24 | "background": { 25 | "beginsPattern": { 26 | "regexp": "^bundles .+ → .+\\.\\.\\.$" 27 | }, 28 | "endsPattern": { 29 | "regexp": "^created out/extension.js in .*s$" 30 | } 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix "" 2 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | ## Glossary 4 | 5 | - *CDP*: Chrome DevTools Protocol. https://chromedevtools.github.io/devtools-protocol/ 6 | - *DAP*: Debug Adapter Protocol. https://microsoft.github.io/debug-adapter-protocol/overview 7 | - *VM*: Virtual Machine 8 | 9 | ## Components 10 | 11 | ![Component Overview Diagram](docs/component-overview.drawio.svg) 12 | 13 | RxJS-specific debugging reuses debugging sessions started by *Visual Studio Codes* built-in [JavaScript debugging extension (*js-debug*)](https://github.com/microsoft/vscode-js-debug). The *RxJS Debugging Extension* communicates through *js-debug* using CDP with the *Debugging Runtime*. The *Debugging Runtime* interacts with the *RxJS Program*, running in the *JavaScript VM* (e.g., Node.JS or browsers like Google Chrome). 14 | 15 | ### RxJS Debugging Extension 16 | 17 | The [*RxJS Debugging Extension*](./packages/extension) integrates with *Visual Studio Code* using its extension API and provides relevant user interfaces and functionalities. It allows developers to use RxJS debugging features like operator log points. 18 | 19 | Furthermore, it ensures that, once a *js-debug* debugging session is started, essential hooks are registered in the *JavaScript VM* using [CDP Bindings](#cdp-bindings). 20 | 21 | The communication protocol to exchange data with the *Debugging Runtime* is implemented in the extension's [TelemetryBridge](./packages/extension/src/telemetryBridge/index.ts). 22 | 23 | ### Debugging Runtime 24 | 25 | A *Debugging Runtime* interfaces with the live *RxJS Program* and forwards relevant *Telemetry Data* (e.g. a value emitted by an Observable) to the *RxJS Debugging Extension*. A *Debugging Runtime* runs in the same process as the *RxJS Program*. 26 | 27 | Specific *JavaScript VM*s require specific *Debugging Runtimes*. E.g., [runtime-nodejs](./packages/runtime-nodejs) enables debugging of *RxJS Programs* executed in Node.JS. Web application bundled with Webpack require the [runtime-webpack](./packages/runtime-webpack) plugin likewise. 28 | 29 | Independently from "how" a *Debugging Runtime* finds its way to the *JavaScript VM*, all of them fulfil following tasks: 30 | 31 | - Use hooks registered using [CDP Bindings](#cdp-bindings) to establish communication with the *RxJS Debugging Extension* 32 | - Patch RxJS to provide required *Telemetry Data* 33 | - Communicate with the *RxJS Debugging* Extension using the runtimes [TelemetryBridge](./packages/runtime/telemetryBridge.ts) 34 | 35 | ## CDP Bindings 36 | 37 | A binding is a function available in a *JavaScript VM* global scope. It is created using the [Runtime.addBinding](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-addBinding) function of a CDP client (i.e. the *RxJS Debugging Extension*). Once the *Binding* function is called, a callback in the CDP client is executed. 38 | 39 | *RxJS Debugging for Visual Studio Code* uses this form of remote procedure calls (RPC) to communicate with the *Debugging Runtime* in a *JavaScript VM*. 40 | 41 | Once the *RxJS Debugging Extension* detects a new *js-debug* debugging session, following bindings are registered: 42 | 43 | | Name | Payload | Notes | 44 | | --------------------------- | -------- | ------------------------------------------------------------ | 45 | | `rxJsDebuggerRuntimeReady` | None | A *Debugging Runtime* is expected to call this binding once it is ready to debug an *RxJS Program*. | 46 | | `sendRxJsDebuggerTelemetry` | `string` | Sends a JSON-encoded [TelemetryEvent](./packages/telemetry/src/index.ts) to the *RxJS Debugging Extension*. | 47 | 48 | Both the *RxJS Debugging Extension* as well as the *Debugging Runtime* use a well defined communication protocol implemented by their respective telemetry bridges. 49 | 50 | ## Example System Interaction 51 | 52 | Based on [testbench-nodejs](./packages/testbench-nodejs), the following sequence diagram shows typical interactions between the presented system components. 53 | 54 | *The JavaScript VM component is omitted for clarity.* 55 | 56 | ![Example System Interaction Sequence Diagram](./docs/system-interactions-sequence-diagram.svg) 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Install the latest version from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=manuelalabor.rxjs-debugging-for-vs-code). 4 | 5 | ## 1.1.1 6 | 7 | - Bugfix: RxJS is not detected on Windows Systems [#139](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/139) 8 | 9 | ## 1.1.0 10 | 11 | - Improvement: Support for Plain JavaScript [#126](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/126) 12 | 13 | ## 1.0.1 14 | 15 | - Bugfix: Live Logs from Previous Debug Session shown again in a new Debug Session [#123](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/123) 16 | 17 | ## 1.0.0 18 | 19 | - Feature: Support RxJS 7 [#52](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/52) 20 | - Bugfix: Operator Log Point Decorations change Line Height [#118](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/118) 21 | 22 | ## 0.9.0 23 | 24 | - Feature: Support Debugging of Browser-based Applications [#43](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/43) 25 | - Feature: Collect Analytics Data on Opt-In [#63](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/63) 26 | - Improvement: Add Integration Test for Operator Log Points [#49](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/49) 27 | - Bugfix: Enabled Log Point stays where it was enabled once [#102](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/102) 28 | 29 | ## 0.1.2 30 | 31 | - Fix: Log Point Events not displayed [#54](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/54) 32 | - Improvement: Support for `pwa-node` launch configurations 33 | 34 | ## 0.1.1 35 | 36 | - Add Icon for Visual Studio Code Extension Marketplace. 🦉 37 | 38 | ## 0.1.0 39 | 40 | - Feature: Operator Log Points 41 | - Feature: NodeJS Support 42 | 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Rules 4 | 5 | To keep it simple, we only have three basic rules: 6 | 7 | 1. Don't panic 8 | 2. Don't be evil 9 | 3. Don't feed the trolls 10 | 11 | ## Examples 12 | 13 | The following non-exhaustive list provides specific guidelines and examples: 14 | 15 | - Be respectful, be responsible, be kind 16 | - Avoid asking for deadlines 17 | - Don't feel entitled to free support, advice, or features if you are not a [contributor](CONTRIBUTING.md) 18 | - If you have a [general question](https://github.com/swissmanu/rxjs-debugging-for-vscode/discussions), don't use GitHub Issues 19 | - If you are having a bad day and want to offend someone, please go somewhere else 20 | 21 | ## Reporting 22 | 23 | We encourage all community members to resolve problems on their own whenever possible. Instances of abusive, 24 | harassing, or otherwise unacceptable behavior may be [reported](mailto:rxjsdebugging@alabor.me) to us. 25 | 26 | ## Enforcement 27 | 28 | Any violation may be punished with a snarky comment and finally a "plonk", which means that we ignore you according to rule #3. 29 | 30 | 31 | 32 | ---- 33 | 34 | 35 | 36 | >This Code of Conduct was adapted from [PhotoPrism](https://github.com/photoprism/photoprism/blob/develop/CODE_OF_CONDUCT.md). Thank you for a great product 🙏 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes any type of contribution! ❤️ [Opening an issue](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/new/choose) to document a problem you encountered or suggesting a new feature is always a good start. 4 | 5 | Before you submit a pull request, please discuss potential changes with the maintainer either in an [issue](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues/new/choose), using [GitHub Discussions](https://github.com/swissmanu/rxjs-debugging-for-vscode/discussions) or via email. 6 | 7 | ## Development 8 | 9 | ### Get Started 10 | 11 | To get started with development, follow these four steps: 12 | 13 | 1. Clone the repo and run `yarn` to install all dependencies. 14 | 2. Open `workspace.code-workspace` with Visual Studio Code. 15 | 3. Run the "extension: Build and Watch" task, which will continuously (re-)build the extension. 16 | 4. Run the "Testbench: NodeJS" launch configuration to open a new Visual Studio Code window, which: 17 | - loads the RxJS debugging extension in development mode, so you can use the debugger in the original Visual Studio Code window. 18 | - uses `packages/testbench-nodejs` as workspace, so you can test the RxJS debugging extension with a real example. 19 | 20 | 21 | ### Repository Structure 22 | 23 | This repository is organized as monorepo. We use [nx](https://nx.dev/) and [lerna](https://lerna.js.org/) to streamline tasks. 24 | 25 | Following packages can be found in the [`packages`](./packages) directory: 26 | 27 | - [`extension`](./packages/extension): The main package containing the debugging extension for Visual Studio Code. 28 | - [`telemetry`](./packages/telemetry): TypeScript types and helper functions used for communication between runtime and debugging extension. 29 | - [`runtime`](./packages/runtime): Contains rudimentary utilities to augment RxJS in an arbitrary runtime environment. 30 | - [`runtime-nodejs`](./packages/runtime-nodejs): NodeJS specific augmentation functionalities. 31 | - [`runtime-webpack`](./packages/runtime-webpack): Webpack plugin, published as `@rxjs-debugging/runtime-webpack`, providing runtime augmentation for web applications built with Webpack. 32 | - [`extension-integrationtest`](./packages/extension-integrationtest): An integration test suite verifying various aspects of the extension. 33 | - [`testbench-*`](./packages): Test environments simulating various scenarios to test the debugger. 34 | 35 | ### Run Test Suites 36 | 37 | Unit and integration tests are automatically executed once changes are pushed to Github. You can run them locally using the following commands: 38 | 39 | - Unit tests: 40 | 41 | ```shell 42 | yarn nx run-many --target=test --all --parallel 43 | ``` 44 | 45 | - Integration tests: 46 | 47 | ```shell 48 | yarn nx run extension-integrationtest:integrationtest --configuration=test 49 | ``` 50 | 51 | ### Architecture Concepts 52 | 53 | The [ARCHITECTURE.md](./ARCHITECTURE.md) file gives an overview on the most important architectural concepts. 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Manuel Alabor 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 | # ![Archie the Debugger Owl](./docs/brand/archie-small.png) RxJS Debugging for Visual Studio Code 2 | 3 | [![Click to visit marketplace](https://vsmarketplacebadge.apphb.com/version-short/manuelalabor.rxjs-debugging-for-vs-code.svg)](https://marketplace.visualstudio.com/items?itemName=manuelalabor.rxjs-debugging-for-vs-code) [![Twitter](https://img.shields.io/badge/Follow-%40rxjsdebugging-blue?logo=twitter)](https://twitter.com/rxjsdebugging) 4 | 5 | > Never, ever use `tap(console.log)` again. 6 | 7 | Add non-intrusive debugging capabilities for [RxJS](https://rxjs.dev/) applications to [Visual Studio Code](https://code.visualstudio.com/). 8 | 9 | ![Operator Log Points with RxJS Debugging for Visual Studio Code](./docs/demo.gif) 10 | 11 | ## Features 12 | 13 | - RxJS debugging, fully integrated with Visual Studio Code 14 | - Works with RxJS 6.6.7 and newer 15 | - Support for Node.js and Webpack-based RxJS applications 16 | 17 | ## Requirements 18 | 19 | - [Visual Studio Code 1.61](https://code.visualstudio.com/) or newer 20 | - [RxJS 6.6.7](https://rxjs.dev/) or newer 21 | - To debug NodeJS-based applications: 22 | - [Node.js 12](https://nodejs.org/) or newer 23 | - To debug Webpack-based web applications: 24 | - [Webpack 5.60.0](https://webpack.js.org/) or newer 25 | - The latest [@rxjs-debugging/runtime-webpack](https://www.npmjs.com/package/@rxjs-debugging/runtime-webpack) Webpack plugin (see [here](https://www.npmjs.com/package/@rxjs-debugging/runtime-webpack) for setup instructions) 26 | 27 | ## Usage 28 | 29 | ### Operator Log Points 30 | 31 | Operator log points make manually added `console.log` statements a thing of the past: RxJS Debugger detects [operators](https://rxjs.dev/guide/operators) automatically and recommends a log point. Hover the mouse cursor on the operator to add or remove a log point to the respective operator: 32 | 33 | Manage Operator Log Points 34 | 35 | Once you launch your application with the JavaScript debugger built-in to Visual Studio Code, enabled log points display [events of interest](https://rxjs.dev/guide/observable#anatomy-of-an-observable) inline in the editor: 36 | 37 | - Subscribe 38 | - Emitted values (next, error, complete) 39 | - Unsubscribe 40 | 41 | Live Operator Log Points 42 | 43 | By default, RxJS Debugger clears logged events from the editor after you stop the JavaScript debugger. You can customize this behavior in the settings. 44 | 45 | Finally, you can toggle gutter indicators for recommended log points via the command palette: 46 | 47 | Toggle Display of Log Point Recommendations 48 | 49 | 50 | 51 | ---- 52 | 53 | 54 | 55 | ## Roadmap & Future Development 56 | 57 | Refer to the [milestones overview](https://github.com/swissmanu/rxjs-debugging-for-vscode/milestones) for planned, future iterations. The [issue list](https://github.com/swissmanu/rxjs-debugging-for-vscode/issues) provides an overview on all open development topics. 58 | 59 | ## Contributing 60 | 61 | "RxJS Debugging for Visual Studio Code" welcomes any type of contribution! ❤️ 62 | Have a look at [CONTRIBUTING.md](./CONTRIBUTING.md) for further details. 63 | 64 | ## Playground 65 | 66 | Jump right in and explore, how "RxJS Debugging for Visual Studio Code" can improve your RxJS debugging workflow: 67 | 68 | https://github.com/swissmanu/playground-rxjs-debugging-for-vscode 69 | 70 | ## Analytics Data 71 | 72 | The "RxJS Debugging for Visual Studio Code" extension collects usage analytics data from users who opt-in. See [ANALYTICS.md](./ANALYTICS.md) for more information on what data is collected and why. 73 | 74 | ## Research 75 | 76 | This extension is based on research by Manuel Alabor. See [RESEARCH.md](./RESEARCH.md) for more information. 77 | -------------------------------------------------------------------------------- /RESEARCH.md: -------------------------------------------------------------------------------- 1 | # Research 2 | 3 | "RxJS Debugging for Visual Studio Code" is the result the master studies research by Manuel Alabor on the topic how developers debug RxJS-based code. 4 | 5 | - Manuel Alabor and Markus Stolze. 2020. Debugging of RxJS-based applications. In Proceedings of the 7th ACM SIGPLAN International Workshop on Reactive and Event-Based Languages and Systems (REBLS 2020). Association for Computing Machinery, New York, NY, USA, 15–24. DOI: https://doi.org/10.1145/3427763.3428313 6 | - Manuel Alabor. 2021. User Journey: Debugging of RxJS-Based Applications. https://alabor.me/research/user-journey-debugging-of-rxjs-based-applications/ 7 | 8 | 9 | ## Open Research 10 | 11 | "RxJS Debugging for Visual Studio Code" collects usage analytics data on an opt-in basis. We provide open access to this data for research projects. Please refer to [ANALYTICS.md](./ANALYTICS.md) for further information. -------------------------------------------------------------------------------- /docs/brand/archie-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/brand/archie-small.png -------------------------------------------------------------------------------- /docs/brand/archie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/brand/archie.png -------------------------------------------------------------------------------- /docs/brand/archie.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/brand/archie.sketch -------------------------------------------------------------------------------- /docs/brand/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/brand/avatar.png -------------------------------------------------------------------------------- /docs/brand/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/brand/icon.png -------------------------------------------------------------------------------- /docs/brand/logo-gh-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/brand/logo-gh-social.png -------------------------------------------------------------------------------- /docs/brand/logo-no-margin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/brand/logo-no-margin.png -------------------------------------------------------------------------------- /docs/brand/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/brand/logo.png -------------------------------------------------------------------------------- /docs/brand/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/brand/logo.sketch -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/demo.gif -------------------------------------------------------------------------------- /docs/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/demo.mp4 -------------------------------------------------------------------------------- /docs/live-operator-logs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/live-operator-logs.gif -------------------------------------------------------------------------------- /docs/live-operator-logs.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/live-operator-logs.mp4 -------------------------------------------------------------------------------- /docs/manage-operator-log-points.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/manage-operator-log-points.gif -------------------------------------------------------------------------------- /docs/manage-operator-log-points.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/manage-operator-log-points.mp4 -------------------------------------------------------------------------------- /docs/system-interactions-sequence-diagram.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant User 3 | participant vscode 4 | participant jsdebug as js-debug 5 | participant Extension as RxJS Debugging Extension 6 | participant Program as RxJS Program 7 | participant Runtime as Debugging Runtime 8 | 9 | 10 | User->>+vscode: Open observable.ts 11 | vscode-->>User: Show observable.ts 12 | vscode-->>+Extension: Opened observable.ts 13 | Extension->>Extension: Recommend Operator Log Points 14 | Extension->>-vscode: Update Operator Log Point Decorations 15 | vscode-->>User: Show Operator Log Point Decorations 16 | User->>+Extension: Enable Operator Log Point 17 | Extension->>Extension: Enable Operator Log Point 18 | Extension->>-vscode: Update Operator Log Point Decorations 19 | vscode-->>User: Show Operator Log Point Decorations 20 | 21 | User->>vscode: Start Debug 22 | vscode->>+jsdebug: Start Debug Session 23 | jsdebug->>+Extension: Will Start Debug Session 24 | Extension-->>jsdebug: Customize Debug Session with Debugging Runtime 25 | jsdebug->>+Program: Launch and inject Debugging Runtime 26 | Runtime->>Program: Patch RxJS 27 | Runtime->>Extension: Call Binding "rxJsDebuggerRuntimeReady" 28 | Extension->>Runtime: updateOperatorLogPoints() 29 | loop 30 | Program->>Runtime: Telemetry Data 31 | Runtime->>Extension: Telemetry Data 32 | Extension->>vscode: Update Live Log Decoration for observable.ts 33 | vscode-->>User: Show Live Log Decoration 34 | end 35 | 36 | Program-->>-jsdebug: Process exited 37 | jsdebug-->>vscode: Debug Session terminated 38 | jsdebug->>-Extension: Debug Session terminated 39 | Extension->>-vscode: Clear Live Log Decoration 40 | vscode-->>-User: Hide Live Log Decoration 41 | -------------------------------------------------------------------------------- /docs/toggle-log-points.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/toggle-log-points.gif -------------------------------------------------------------------------------- /docs/toggle-log-points.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/docs/toggle-log-points.mp4 -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "1.1.1", 4 | "npmClient": "yarn" 5 | } 6 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@nrwl/workspace/presets/npm.json", 3 | "npmScope": "rxjs-debugging", 4 | "tasksRunnerOptions": { 5 | "default": { 6 | "runner": "@nrwl/workspace/tasks-runners/default", 7 | "options": { 8 | "cacheableOperations": ["build", "test", "lint", "package", "prepare"] 9 | } 10 | } 11 | }, 12 | "targetDependencies": { 13 | "build": [ 14 | { 15 | "target": "build", 16 | "projects": "dependencies" 17 | } 18 | ], 19 | "prepare": [ 20 | { 21 | "target": "prepare", 22 | "projects": "dependencies" 23 | } 24 | ], 25 | "package": [ 26 | { 27 | "target": "package", 28 | "projects": "dependencies" 29 | } 30 | ], 31 | "test": [ 32 | { 33 | "target": "test", 34 | "projects": "dependencies" 35 | } 36 | ] 37 | }, 38 | "affected": { 39 | "defaultBase": "main" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxjs-debugging", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "prepare": "husky install", 9 | "lint": "eslint --ext ts 'packages/*/src/**'", 10 | "clean": "lerna exec -- rm -rf out && nx reset" 11 | }, 12 | "devDependencies": { 13 | "@nrwl/cli": "13.2.3", 14 | "@nrwl/tao": "13.2.3", 15 | "@nrwl/workspace": "13.2.3", 16 | "@typescript-eslint/eslint-plugin": "5.5.0", 17 | "@typescript-eslint/parser": "5.5.0", 18 | "eslint": "8.4.0", 19 | "eslint-plugin-prettier": "4.0.0", 20 | "husky": "7.0.4", 21 | "lerna": "4.0.0", 22 | "lint-staged": "12.1.2", 23 | "prettier": "2.5.0", 24 | "typescript": "4.5.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/extension-integrationtest/jest-runner-vscode.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path'); 3 | 4 | function relativePath(...segments) { 5 | return path.resolve(path.join(__dirname, ...segments)); 6 | } 7 | 8 | /** @type {import('jest-runner-vscode').RunnerOptions} */ 9 | module.exports = { 10 | version: '1.61.0', 11 | launchArgs: ['--new-window', '--disable-extensions'], 12 | workspaceDir: relativePath('..', 'testbench-nodejs'), 13 | extensionDevelopmentPath: relativePath('../extension'), 14 | openInFolder: true, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/extension-integrationtest/jest.config.js: -------------------------------------------------------------------------------- 1 | function isCI() { 2 | return !!process.env['CI']; 3 | } 4 | 5 | const baseTimeout = 20_000; // 20s 6 | 7 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 8 | module.exports = { 9 | runner: 'vscode', 10 | modulePathIgnorePatterns: ['.vscode-test/'], 11 | testEnvironment: 'node', 12 | transform: { 13 | '^.+\\.tsx?$': 'ts-jest', 14 | }, 15 | testMatch: ['**/src/**/*.test.ts'], 16 | testTimeout: isCI() ? baseTimeout * 10 : baseTimeout, 17 | verbose: true, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/extension-integrationtest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rxjs-debugging/extension-integrationtest", 3 | "version": "1.1.1", 4 | "license": "MIT", 5 | "private": true, 6 | "engines": { 7 | "vscode": "^1.61.0" 8 | }, 9 | "scripts": { 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "rxjs-debugging-for-vs-code": "^1.1.1" 14 | }, 15 | "devDependencies": { 16 | "@types/jest": "27.0.3", 17 | "@types/node": "16.11.11", 18 | "@types/vscode": "1.61.0", 19 | "jest": "27.4.3", 20 | "jest-runner-vscode": "2.0.0", 21 | "ts-jest": "27.1.0", 22 | "typescript": "4.5.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/extension-integrationtest/src/__snapshots__/extension.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RxJS Debugging for vscode shows operator life cycle events as text editor decoration 1`] = ` 4 | Array [ 5 | Object { 6 | "decorationType": "TextEditorDecorationType8", 7 | "options": Array [], 8 | "ranges": Array [], 9 | }, 10 | Object { 11 | "decorationType": "TextEditorDecorationType8", 12 | "options": Array [ 13 | "(5:9007199254740991):(5:9007199254740991)-undefined-{\\"after\\":{\\"contentText\\":\\"Next: 0\\"}}", 14 | ], 15 | "ranges": Array [], 16 | }, 17 | Object { 18 | "decorationType": "TextEditorDecorationType8", 19 | "options": Array [ 20 | "(5:9007199254740991):(5:9007199254740991)-undefined-{\\"after\\":{\\"contentText\\":\\"Next: 1\\"}}", 21 | ], 22 | "ranges": Array [], 23 | }, 24 | Object { 25 | "decorationType": "TextEditorDecorationType8", 26 | "options": Array [ 27 | "(5:9007199254740991):(5:9007199254740991)-undefined-{\\"after\\":{\\"contentText\\":\\"Next: 2\\"}}", 28 | ], 29 | "ranges": Array [], 30 | }, 31 | Object { 32 | "decorationType": "TextEditorDecorationType8", 33 | "options": Array [ 34 | "(5:9007199254740991):(5:9007199254740991)-undefined-{\\"after\\":{\\"contentText\\":\\"Next: 3\\"}}", 35 | ], 36 | "ranges": Array [], 37 | }, 38 | Object { 39 | "decorationType": "TextEditorDecorationType8", 40 | "options": Array [ 41 | "(5:9007199254740991):(5:9007199254740991)-undefined-{\\"after\\":{\\"contentText\\":\\"Completed\\"}}", 42 | ], 43 | "ranges": Array [], 44 | }, 45 | Object { 46 | "decorationType": "TextEditorDecorationType8", 47 | "options": Array [ 48 | "(5:9007199254740991):(5:9007199254740991)-undefined-{\\"after\\":{\\"contentText\\":\\"Unsubscribe\\"}}", 49 | ], 50 | "ranges": Array [], 51 | }, 52 | Object { 53 | "decorationType": "TextEditorDecorationType8", 54 | "options": Array [ 55 | "(5:9007199254740991):(5:9007199254740991)-undefined-{\\"after\\":{\\"contentText\\":\\"Unsubscribe\\"}}", 56 | ], 57 | "ranges": Array [], 58 | }, 59 | Object { 60 | "decorationType": "TextEditorDecorationType8", 61 | "options": Array [ 62 | "(5:9007199254740991):(5:9007199254740991)-undefined-{\\"after\\":{\\"contentText\\":\\"Unsubscribe\\"}}", 63 | ], 64 | "ranges": Array [], 65 | }, 66 | Object { 67 | "decorationType": "TextEditorDecorationType8", 68 | "options": Array [], 69 | "ranges": Array [], 70 | }, 71 | ] 72 | `; 73 | -------------------------------------------------------------------------------- /packages/extension-integrationtest/src/extension.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Commands, 3 | executeCommand, 4 | OperatorLogPoint, 5 | TestCommands, 6 | } from 'rxjs-debugging-for-vs-code/out/integrationTest'; 7 | import * as vscode from 'vscode'; 8 | import openAndShowTextDocument from './util/openAndShowTextDocument'; 9 | import waitForExtension from './util/waitForExtension'; 10 | 11 | describe('RxJS Debugging for vscode', () => { 12 | test('shows operator life cycle events as text editor decoration', async () => { 13 | const document = await openAndShowTextDocument('**/commonjs/observable.js'); 14 | await waitForExtension(); 15 | 16 | // Enable Operator Log Point for the first operator, take. 17 | await executeCommand( 18 | vscode.commands, 19 | Commands.EnableOperatorLogPoint, 20 | new OperatorLogPoint( 21 | document.uri, 22 | new vscode.Position(5, 4), 23 | { 24 | fileName: document.uri.fsPath, 25 | line: 5, 26 | character: 25, 27 | operatorIndex: 0, 28 | }, 29 | 'take' 30 | ) 31 | ); 32 | 33 | const debuggingDone = new Promise((resolve) => { 34 | vscode.debug.onDidTerminateDebugSession(() => resolve()); 35 | }); 36 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 37 | await vscode.debug.startDebugging(vscode.workspace.workspaceFolders![0], 'Launch CommonJS'); 38 | await debuggingDone; 39 | 40 | const decorations = await executeCommand( 41 | vscode.commands, 42 | TestCommands.GetDecorationSetterRecording, 43 | 'src/commonjs/observable.js', 44 | 'liveLog' 45 | ); 46 | 47 | expect(decorations).toMatchSnapshot(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/extension-integrationtest/src/util/openAndShowTextDocument.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export default async function openAndShowTextDocument(filePattern: string): Promise { 4 | const [file] = await vscode.workspace.findFiles(filePattern); 5 | const document = await vscode.workspace.openTextDocument(file); 6 | await vscode.window.showTextDocument(document); 7 | 8 | return document; 9 | } 10 | -------------------------------------------------------------------------------- /packages/extension-integrationtest/src/util/waitForExtension.ts: -------------------------------------------------------------------------------- 1 | import { Commands } from 'rxjs-debugging-for-vs-code/out/integrationTest'; 2 | import * as vscode from 'vscode'; 3 | 4 | async function waitForCommands(): Promise { 5 | const command = Commands.EnableOperatorLogPoint; 6 | const commands = await vscode.commands.getCommands(true); 7 | if (commands.indexOf(command) !== -1) { 8 | return; 9 | } 10 | throw new Error(`"${command}" Command not found`); 11 | } 12 | 13 | async function wait(ms: number): Promise { 14 | return new Promise((resolve) => setTimeout(resolve, ms)); 15 | } 16 | 17 | export default async function waitForExtension(): Promise { 18 | let ok = false; 19 | for (let retry = 0; retry < 5; retry++) { 20 | try { 21 | await waitForCommands(); 22 | ok = true; 23 | } catch (_) { 24 | await wait(1000); 25 | } 26 | } 27 | 28 | if (!ok) { 29 | throw new Error('Could not determine if extension was available or not.'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/extension-integrationtest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "sourceMap": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "outDir": "out", 13 | "rootDir": "src" 14 | }, 15 | "exclude": ["node_modules", "packages/out"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/extension/.gitignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | *.md 3 | LICENSE 4 | -------------------------------------------------------------------------------- /packages/extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | .github/** 2 | .husky/** 3 | .vscode/** 4 | .vscode-test/** 5 | docs/** 6 | !docs/brand/icon.png 7 | test-benches/** 8 | jest/** 9 | node_modules/** 10 | out/integrationTest/** 11 | out/**/*.test.js 12 | src/** 13 | .editorconfig 14 | **/.eslintrc.js 15 | **/.eslintrc.json 16 | .gitattributes 17 | .gitignore 18 | jest.config.js 19 | renovate.json 20 | rollup.config.js 21 | tsconfig.json 22 | **/tsconfig.json 23 | 24 | *.log 25 | **/*.map 26 | **/*.ts 27 | -------------------------------------------------------------------------------- /packages/extension/jest.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 4 | module.exports = { 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | globalSetup: join(__dirname, 'jest', 'unit', 'setup.js'), 8 | 9 | transformIgnorePatterns: ['node_modules/(?!(@rxjs-debugging)/)'], 10 | testPathIgnorePatterns: ['node_modules', 'src/integrationTests'], 11 | modulePathIgnorePatterns: ['.vscode-test/'], 12 | }; 13 | -------------------------------------------------------------------------------- /packages/extension/jest/unit/setup.js: -------------------------------------------------------------------------------- 1 | require('reflect-metadata'); 2 | 3 | module.exports = () => {}; 4 | -------------------------------------------------------------------------------- /packages/extension/package.nls.json: -------------------------------------------------------------------------------- 1 | { 2 | "rxjs-debugging.config.title": "RxJS Debugging", 3 | "rxjs-debugging.config.hideLiveLogWhenStoppingDebugger": "When enabled, live log values are hidden automatically after stopping the debugger.", 4 | "rxjs-debugging.config.recommendOperatorLogPointsWithAnIcon": "When enabled, recommended operator log points will be indicated using an icon at the beginning of the line. Hint: You can toggle this setting using the related command in the command palette as well.", 5 | "rxjs-debugging.config.logLevel": "Internal log level for the extension. (reload required)", 6 | "rxjs-debugging.config.enableUsageAnalytics": "When enabled, anonymized behavior data is collected to further improve RxJS Debugging and fuel future research projects looking into RxJS Debugging.\n\n\nYour individual identity will neither be revealed nor can be reconstructed at any time.\n\nSee [ANALYTICS.md](https://github.com/swissmanu/rxjs-debugging-for-vscode/blob/main/ANALYTICS.md) for a full disclosure on what data for what reason gets collected, where it is stored and how you can get access to it.\n\n\nThank you for considering your contribution 🙏", 7 | 8 | "rxjs-debugging.command.toggleOperatorLogPointGutterIcon": "Toggle gutter icon for recommended operator log points" 9 | } 10 | -------------------------------------------------------------------------------- /packages/extension/resources/debug-breakpoint-log-unverified.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 52 | 59 | 60 | -------------------------------------------------------------------------------- /packages/extension/resources/debug-breakpoint-log.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 52 | 56 | 57 | -------------------------------------------------------------------------------- /packages/extension/resources/rxjs-operator-log-point-all-enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 53 | 57 | 58 | -------------------------------------------------------------------------------- /packages/extension/resources/rxjs-operator-log-point-disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 53 | 57 | 58 | -------------------------------------------------------------------------------- /packages/extension/resources/rxjs-operator-log-point-some-enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 53 | 57 | 58 | -------------------------------------------------------------------------------- /packages/extension/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import inject from '@rollup/plugin-inject'; 3 | import json from '@rollup/plugin-json'; 4 | import nodeResolve from '@rollup/plugin-node-resolve'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | import * as path from 'path'; 7 | import copy from 'rollup-plugin-copy'; 8 | import { terser } from 'rollup-plugin-terser'; 9 | import packageJson from './package.json'; 10 | 11 | const terserOptions = { format: { comments: () => false } }; 12 | const POSTHOG_PROJECT_API_KEY = process.env['POSTHOG_PROJECT_API_KEY'] ?? null; 13 | const POSTHOG_HOST = process.env['POSTHOG_HOST'] ?? null; 14 | 15 | /** 16 | * @type {import('rollup').RollupOptions} 17 | */ 18 | export default ({ configMode }) => { 19 | const doProductionBuild = configMode === 'production'; 20 | const doTestBuild = configMode === 'test'; 21 | 22 | if (doProductionBuild && (POSTHOG_PROJECT_API_KEY === null || POSTHOG_HOST === null)) { 23 | console.error( 24 | 'POSTHOG_PROJECT_API_KEY and/or POSTHOG_HOST environment variables are missing. Cannot create production build.' 25 | ); 26 | process.exit(1); 27 | return; 28 | } 29 | 30 | const intro = ` 31 | const EXTENSION_VERSION = "${packageJson.version}"; 32 | const POSTHOG_PROJECT_API_KEY = "${POSTHOG_PROJECT_API_KEY}"; 33 | const POSTHOG_HOST = "${POSTHOG_HOST}" 34 | `; 35 | 36 | return { 37 | input: { 38 | extension: 'src/extension.ts', 39 | ...(doTestBuild ? { 'integrationTest/index': 'src/integrationTest/index.ts' } : {}), // Integration Test API 40 | }, 41 | output: { 42 | dir: 'out', 43 | format: 'commonjs', 44 | sourcemap: !doProductionBuild, 45 | intro, 46 | }, 47 | external: [ 48 | 'vscode', 49 | ...(doProductionBuild ? [] : ['typescript']), // Faster dev builds without including TypeScript 50 | ], 51 | plugins: [ 52 | json(), 53 | commonjs({ 54 | ignore: ['bufferutil', 'utf-8-validate'], // Ignore optional peer dependencies of ws 55 | }), 56 | nodeResolve({ preferBuiltins: true }), 57 | doTestBuild && 58 | inject({ 59 | prepareForIntegrationTest: path.resolve(path.join('src', 'integrationTest', 'prepareForIntegrationTest.ts')), 60 | }), 61 | typescript({ 62 | declaration: doTestBuild, 63 | }), 64 | copy({ 65 | overwrite: true, 66 | targets: [ 67 | { src: '../runtime-nodejs/out/**/*', dest: './out/runtime-nodejs' }, 68 | { src: '../../docs/*', dest: './docs' }, 69 | { src: '../../*.md', dest: './' }, 70 | { src: '../../LICENSE', dest: './' }, 71 | ], 72 | }), 73 | doProductionBuild && terser(terserOptions), 74 | ], 75 | }; 76 | }; 77 | -------------------------------------------------------------------------------- /packages/extension/src/__mocks__/vscode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | /* eslint-disable @typescript-eslint/no-empty-function */ 4 | 5 | /** 6 | * This file contains a set of fragile mocks of vscode API objects. They allow us to run Jest-based unit tests for our 7 | * own code. 8 | * These are only used for unit tests. Integration tests are executed using the official extension test runner. 9 | */ 10 | 11 | import type { Event as EventType, EventEmitter as EventEmitterType, Uri as UriType } from 'vscode'; 12 | 13 | export class Position { 14 | constructor(readonly line: number, readonly character: number) {} 15 | } 16 | 17 | export class EventEmitter implements EventEmitterType { 18 | private listeners: ((data: T) => void)[] = []; 19 | 20 | event: EventType = (listener) => { 21 | this.listeners.push(listener); 22 | return new Disposable(() => {}); 23 | }; 24 | 25 | /** 26 | * Notify all subscribers of the {@link EventEmitter.event event}. Failure 27 | * of one or more listener will not fail this function call. 28 | * 29 | * @param data The event object. 30 | */ 31 | fire(data: T): void { 32 | for (const listener of this.listeners) { 33 | listener(data); 34 | } 35 | } 36 | 37 | /** 38 | * Dispose this object and free resources. 39 | */ 40 | dispose(): void {} 41 | } 42 | 43 | export class Disposable { 44 | /** 45 | * Combine many disposable-likes into one. Use this method 46 | * when having objects with a dispose function which are not 47 | * instances of Disposable. 48 | * 49 | * @param disposableLikes Objects that have at least a `dispose`-function member. 50 | * @return Returns a new disposable which, upon dispose, will 51 | * dispose all provided disposables. 52 | */ 53 | static from(...disposableLikes: { dispose: () => any }[]): Disposable { 54 | return new Disposable(() => {}); 55 | } 56 | 57 | /** 58 | * Creates a new Disposable calling the provided function 59 | * on dispose. 60 | * @param callOnDispose Function that disposes something. 61 | */ 62 | constructor(callOnDispose: () => void) {} 63 | 64 | /** 65 | * Dispose this object. 66 | */ 67 | dispose(): void {} 68 | } 69 | 70 | export class Uri implements UriType { 71 | static parse(value: string, strict?: boolean): Uri { 72 | return new Uri('', '', value, '', ''); 73 | } 74 | 75 | static file(path: string): Uri { 76 | return new Uri('', '', path, '', ''); 77 | } 78 | 79 | static joinPath(base: Uri, ...pathSegments: string[]): Uri { 80 | return new Uri('', '', '', '', ''); 81 | } 82 | 83 | static from({ 84 | scheme, 85 | authority = '', 86 | path = '', 87 | query = '', 88 | fragment = '', 89 | }: { 90 | scheme: string; 91 | authority?: string; 92 | path?: string; 93 | query?: string; 94 | fragment?: string; 95 | }): Uri { 96 | return new Uri(scheme, authority, path, query, fragment); 97 | } 98 | 99 | private constructor( 100 | readonly scheme: string, 101 | readonly authority: string, 102 | readonly path: string, 103 | readonly query: string, 104 | readonly fragment: string 105 | ) {} 106 | 107 | readonly fsPath: string = ''; 108 | 109 | with({ 110 | scheme, 111 | authority, 112 | path, 113 | query, 114 | fragment, 115 | }: { 116 | scheme?: string; 117 | authority?: string; 118 | path?: string; 119 | query?: string; 120 | fragment?: string; 121 | }): Uri { 122 | return new Uri( 123 | scheme || this.scheme, 124 | authority || this.authority, 125 | path || this.path, 126 | query || this.query, 127 | fragment || this.fragment 128 | ); 129 | } 130 | 131 | toString(skipEncoding?: boolean): string { 132 | return `${this.scheme}${this.authority}${this.path}${this.query}${this.fragment}`; 133 | } 134 | 135 | toJSON(): any { 136 | return { 137 | scheme: this.scheme, 138 | authority: this.authority, 139 | path: this.path, 140 | query: this.query, 141 | fragment: this.fragment, 142 | }; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /packages/extension/src/analytics/__mocks__/posthog-node.ts: -------------------------------------------------------------------------------- 1 | let createdInstance: Posthog | null = null; 2 | 3 | export function resetPosthogMock(): void { 4 | createdInstance = null; 5 | } 6 | 7 | export function getPosthogMockInstance(): Posthog | null { 8 | return createdInstance; 9 | } 10 | 11 | export default class Posthog { 12 | constructor(readonly projectApiKey: string, readonly options: Record) { 13 | if (createdInstance !== null) { 14 | throw new Error('Instance already created! Consider calling resetMock()'); 15 | } 16 | createdInstance = this; 17 | } 18 | 19 | readonly identify = jest.fn(); 20 | 21 | readonly capture = jest.fn(); 22 | } 23 | -------------------------------------------------------------------------------- /packages/extension/src/analytics/askToOptInToAnalyticsReporter.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as nls from 'vscode-nls'; 3 | import { Configuration } from '../configuration'; 4 | import { IConfigurationAccessor } from '../configuration/configurationAccessor'; 5 | 6 | const localize = nls.loadMessageBundle(); 7 | const analyticsDocumentation: vscode.Uri = vscode.Uri.parse( 8 | 'https://github.com/swissmanu/rxjs-debugging-for-vscode/blob/main/ANALYTICS.md' 9 | ); 10 | 11 | export default async function askToOptInToAnalyticsReporter( 12 | configurationAccessor: IConfigurationAccessor 13 | ): Promise { 14 | if (configurationAccessor.hasGlobal(Configuration.EnableAnalytics)) { 15 | return; 16 | } 17 | 18 | const text = localize( 19 | 'rxjs-debugging.askToOptInToAnalyticsReporter.text', 20 | 'To improve RxJS Debugging, the extension would like to collect anonymous usage analytics data.\nClick on "Learn More" for a full disclosure on what data would be collected and why.\nClose this notification to disable analytics data collection.' 21 | ); 22 | const learnMore = localize('rxjs-debugging.askToOptInToAnalyticsReporter.action.learnMore', 'Learn More'); 23 | const enable = localize('rxjs-debugging.askToOptInToAnalyticsReporter.action.enable', 'Enable Anonymous Analytics'); 24 | const result = await vscode.window.showInformationMessage(text, learnMore, enable); 25 | 26 | if (result === enable) { 27 | configurationAccessor.update(Configuration.EnableAnalytics, true, true); 28 | } else if (result === learnMore) { 29 | vscode.env.openExternal(analyticsDocumentation); 30 | askToOptInToAnalyticsReporter(configurationAccessor); 31 | } else { 32 | configurationAccessor.update(Configuration.EnableAnalytics, false, true); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/extension/src/analytics/events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/swissmanu/rxjs-debugging-for-vscode/blob/main/ANALYTICS.md 3 | */ 4 | export type AnalyticsEvents = 5 | | 'operator log point enabled' 6 | | 'operator log point disabled' 7 | | 'debug session started' 8 | | 'debug session stopped'; 9 | 10 | type NoDataPoints = Record; 11 | 12 | /** 13 | * @see https://github.com/swissmanu/rxjs-debugging-for-vscode/blob/main/ANALYTICS.md 14 | */ 15 | export interface AnalyticsEventDataPoints { 16 | 'debug session started': { 17 | runtime: 'webpack' | 'nodejs' | 'unknown'; 18 | }; 19 | 'debug session stopped': NoDataPoints; 20 | 'operator log point enabled': { 21 | operatorName?: string; 22 | }; 23 | 'operator log point disabled': { 24 | operatorName?: string; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/extension/src/analytics/isBuiltInOperatorName.test.ts: -------------------------------------------------------------------------------- 1 | import isBuiltInOperatorName, { knownBuiltInOperatorNames } from './isBuiltInOperatorName'; 2 | 3 | describe('Analytics', () => { 4 | describe('isBuiltInOperatorName()', () => { 5 | test.each(knownBuiltInOperatorNames.map((s) => [s]))('returns true for %s', (operatorName) => { 6 | expect(isBuiltInOperatorName(operatorName)).toBe(true); 7 | }); 8 | 9 | test.each([['myCustomOperator'], ['FLATMAP'], ['SwitchMap']])('returns false for %s', (operatorName) => { 10 | expect(isBuiltInOperatorName(operatorName)).toBe(false); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/extension/src/analytics/isBuiltInOperatorName.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Predicate to check if given operator name is a known built-in operator of RxJS. 3 | * 4 | * @param operatorName 5 | * @returns 6 | */ 7 | export default function isBuiltInOperatorName(operatorName: string): boolean { 8 | return knownBuiltInOperatorNames.includes(operatorName); 9 | } 10 | 11 | /** 12 | * A list of known built-in RxJS operators. 13 | * 14 | * @see https://rxjs.dev/api?type=function, last updated 2021-11-18 15 | * 16 | */ 17 | export const knownBuiltInOperatorNames = [ 18 | 'audit', 19 | 'auditTime', 20 | 'buffer', 21 | 'bufferCount', 22 | 'bufferTime', 23 | 'bufferToggle', 24 | 'bufferWhen', 25 | 'catchError', 26 | 'combineLatest', 27 | 'combineLatestAll', 28 | 'combineLatestWith', 29 | 'concat', 30 | 'concatAll', 31 | 'concatMap', 32 | 'concatMapTo', 33 | 'concatWith', 34 | 'connect', 35 | 'count', 36 | 'debounce', 37 | 'debounceTime', 38 | 'defaultIfEmpty', 39 | 'delay', 40 | 'delayWhen', 41 | 'dematerialize', 42 | 'distinct', 43 | 'distinctUntilChanged', 44 | 'distinctUntilKeyChanged', 45 | 'elementAt', 46 | 'endWith', 47 | 'every', 48 | 'exhaustAll', 49 | 'exhaustMap', 50 | 'expand', 51 | 'filter', 52 | 'finalize', 53 | 'find', 54 | 'findIndex', 55 | 'first', 56 | 'groupBy', 57 | 'ignoreElements', 58 | 'isEmpty', 59 | 'last', 60 | 'map', 61 | 'mapTo', 62 | 'materialize', 63 | 'max', 64 | 'merge', 65 | 'mergeAll', 66 | 'mergeMap', 67 | 'mergeMapTo', 68 | 'mergeScan', 69 | 'mergeWith', 70 | 'min', 71 | 'multicast', 72 | 'observeOn', 73 | 'onErrorResumeNext', 74 | 'pairwise', 75 | 'partition', 76 | 'pluck', 77 | 'publish', 78 | 'publishBehavior', 79 | 'publishLast', 80 | 'publishReplay', 81 | 'race', 82 | 'raceWith', 83 | 'reduce', 84 | 'refCount', 85 | 'repeat', 86 | 'repeatWhen', 87 | 'retry', 88 | 'retryWhen', 89 | 'sample', 90 | 'sampleTime', 91 | 'scan', 92 | 'sequenceEqual', 93 | 'share', 94 | 'shareReplay', 95 | 'single', 96 | 'skip', 97 | 'skipLast', 98 | 'skipUntil', 99 | 'skipWhile', 100 | 'startWith', 101 | 'subscribeOn', 102 | 'switchAll', 103 | 'switchMap', 104 | 'switchMapTo', 105 | 'switchScan', 106 | 'take', 107 | 'takeLast', 108 | 'takeUntil', 109 | 'takeWhile', 110 | 'tap', 111 | 'throttle', 112 | 'throttleTime', 113 | 'throwIfEmpty', 114 | 'timeInterval', 115 | 'timeout', 116 | 'timeoutWith', 117 | 'timestamp', 118 | 'toArray', 119 | 'window', 120 | 'windowCount', 121 | 'windowTime', 122 | 'windowToggle', 123 | 'windowWhen', 124 | 'withLatestFrom', 125 | 'zip', 126 | 'zipAll', 127 | 'zipWith ', 128 | ]; 129 | -------------------------------------------------------------------------------- /packages/extension/src/analytics/posthogConfiguration.ts: -------------------------------------------------------------------------------- 1 | export const IPosthogConfiguration = Symbol('PosthogConfiguration'); 2 | 3 | export interface IPosthogConfiguration { 4 | projectApiKey: string; 5 | host: string; 6 | } 7 | 8 | /** 9 | * Creates an `IPosthogConfiguration` using the default, global variables injected by RollupJS. 10 | */ 11 | export default function createPosthogConfiguration(): IPosthogConfiguration { 12 | return { 13 | host: POSTHOG_HOST, 14 | projectApiKey: POSTHOG_PROJECT_API_KEY, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/extension/src/colors.ts: -------------------------------------------------------------------------------- 1 | export enum Colors { 2 | LiveLogLineBackgroundColor = 'rxjsdebugging.liveLogLineBackgroundColor', 3 | LiveLogLineForegroundColor = 'rxjsdebugging.liveLogLineForegroundColor', 4 | } 5 | -------------------------------------------------------------------------------- /packages/extension/src/commands/commands.ts: -------------------------------------------------------------------------------- 1 | import OperatorLogPoint from '../operatorLogPoint'; 2 | 3 | export const enum Commands { 4 | EnableOperatorLogPoint = 'rxjs-debugging-for-vs-code.command.enableOperatorLogPoint', 5 | DisableOperatorLogPoint = 'rxjs-debugging-for-vs-code.command.disableOperatorLogPoint', 6 | ToggleOperatorLogPointGutterIcon = 'rxjs-debugging-for-vs-code.command.toggleOperatorLogPointGutterIcon', 7 | } 8 | 9 | export interface ICommandTypes { 10 | [Commands.EnableOperatorLogPoint]: (operatorLogPoint: OperatorLogPoint | string) => void; 11 | [Commands.DisableOperatorLogPoint]: (operatorLogPoint: OperatorLogPoint | string) => void; 12 | [Commands.ToggleOperatorLogPointGutterIcon]: () => void; 13 | } 14 | -------------------------------------------------------------------------------- /packages/extension/src/commands/executeCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ICommandTypes } from './commands'; 3 | 4 | export default function executeCommand( 5 | ns: typeof vscode.commands, 6 | key: K, 7 | ...args: Parameters 8 | ): Thenable> { 9 | return ns.executeCommand(key, ...args) as Thenable>; 10 | } 11 | -------------------------------------------------------------------------------- /packages/extension/src/commands/getMarkdownCommandWithArgs.ts: -------------------------------------------------------------------------------- 1 | import { ICommandTypes } from './commands'; 2 | 3 | export default function getMarkdownCommandWithArgs( 4 | key: K, 5 | args: Parameters, 6 | serialize: (args: Parameters) => [string] = (args) => [JSON.stringify(args)] 7 | ): string { 8 | return `command:${key}?${encodeURIComponent(JSON.stringify(serialize(args)))}`; 9 | } 10 | -------------------------------------------------------------------------------- /packages/extension/src/commands/operatorLogPointManagement.ts: -------------------------------------------------------------------------------- 1 | import { interfaces } from 'inversify'; 2 | import * as vscode from 'vscode'; 3 | import { ILogger } from '../logger'; 4 | import OperatorLogPoint from '../operatorLogPoint'; 5 | import { IOperatorLogPointManager } from '../operatorLogPoint/manager'; 6 | import { Commands } from './commands'; 7 | import registerCommand from './registerCommand'; 8 | 9 | export default function registerOperatorLogPointManagementCommands( 10 | context: vscode.ExtensionContext, 11 | container: interfaces.Container 12 | ): void { 13 | const manager = container.get(IOperatorLogPointManager); 14 | const logger = container.get(ILogger); 15 | 16 | context.subscriptions.push( 17 | registerCommand(vscode.commands, Commands.EnableOperatorLogPoint, async (operatorLogPoint) => { 18 | if (typeof operatorLogPoint === 'string') { 19 | try { 20 | const parsed = OperatorLogPoint.parse(operatorLogPoint); 21 | manager.enable(parsed); 22 | } catch (e) { 23 | logger.warn( 24 | 'Extension', 25 | `Tried to enable serialized OperatorLogPoint, but could not parse it. ("${operatorLogPoint}")` 26 | ); 27 | } 28 | } else { 29 | manager.enable(operatorLogPoint); 30 | } 31 | }), 32 | 33 | registerCommand(vscode.commands, Commands.DisableOperatorLogPoint, async (operatorLogPoint) => { 34 | if (typeof operatorLogPoint === 'string') { 35 | try { 36 | const parsed = OperatorLogPoint.parse(operatorLogPoint); 37 | manager.disable(parsed); 38 | } catch (e) { 39 | logger.warn( 40 | 'Extension', 41 | `Tried to disable serialized OperatorLogPoint, but could not parse it. ("${operatorLogPoint}")` 42 | ); 43 | } 44 | } else { 45 | manager.disable(operatorLogPoint); 46 | } 47 | }) 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /packages/extension/src/commands/registerCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ICommandTypes } from './commands'; 3 | 4 | /** 5 | * Registers a known command using declared types. 6 | * 7 | * @param ns 8 | * @param key 9 | * @param fn 10 | * @returns 11 | * @see Thanks https://github.com/microsoft/vscode-js-debug/blob/main/src/common/contributionUtils.ts#L171 12 | */ 13 | export default function registerCommand( 14 | ns: typeof vscode.commands, 15 | key: K, 16 | fn: (...args: Parameters) => Thenable> 17 | ): vscode.Disposable { 18 | return ns.registerCommand(key, fn); 19 | } 20 | -------------------------------------------------------------------------------- /packages/extension/src/commands/toggleOperatorLogPointGutterIcon.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Configuration } from '../configuration'; 3 | import { IConfigurationAccessor } from '../configuration/configurationAccessor'; 4 | import { Commands } from './commands'; 5 | import registerCommand from './registerCommand'; 6 | 7 | export default function registerToggleOperatorLogPointDecorationCommand( 8 | context: vscode.ExtensionContext, 9 | configurationAccessor: IConfigurationAccessor 10 | ): void { 11 | context.subscriptions.push( 12 | registerCommand(vscode.commands, Commands.ToggleOperatorLogPointGutterIcon, async () => { 13 | const newValue = !configurationAccessor.get(Configuration.RecommendOperatorLogPointsWithAnIcon, true); 14 | configurationAccessor.update(Configuration.RecommendOperatorLogPointsWithAnIcon, newValue); 15 | }) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/extension/src/configuration/configurationAccessor.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import type * as vscode from 'vscode'; 3 | import { Configuration } from '.'; 4 | import { VsCodeApi } from '../ioc/types'; 5 | 6 | export const IConfigurationAccessor = Symbol('ConfigurationAccessor'); 7 | 8 | /** 9 | * An injectable abstraction over vscodes Configuration API. 10 | */ 11 | export interface IConfigurationAccessor { 12 | get(section: Configuration, defaultValue: T): T; 13 | update(section: Configuration, value: T, updateGlobally?: boolean): Promise; 14 | hasGlobal(section: Configuration): boolean; 15 | onDidChangeConfiguration: vscode.Event; 16 | } 17 | 18 | @injectable() 19 | export default class ConfigurationAccessor implements IConfigurationAccessor { 20 | constructor(@inject(VsCodeApi) private readonly vscodeApi: typeof vscode) {} 21 | 22 | get(section: Configuration, defaultValue: T): T { 23 | const v = this.vscodeApi.workspace.getConfiguration().get(section, defaultValue); 24 | return v; 25 | } 26 | 27 | hasGlobal(section: Configuration): boolean { 28 | const inspection = this.vscodeApi.workspace.getConfiguration().inspect(section); 29 | 30 | if (inspection) { 31 | return inspection.globalValue !== undefined; 32 | } 33 | return false; 34 | } 35 | 36 | async update(section: Configuration, value: T, updateGlobally = true): Promise { 37 | await this.vscodeApi.workspace.getConfiguration().update(section, value, updateGlobally); 38 | } 39 | 40 | onDidChangeConfiguration( 41 | handler: (e: vscode.ConfigurationChangeEvent) => void, 42 | thisArg?: unknown, 43 | disposables?: vscode.Disposable[] 44 | ): vscode.Disposable { 45 | return this.vscodeApi.workspace.onDidChangeConfiguration(handler, thisArg, disposables); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/extension/src/configuration/index.ts: -------------------------------------------------------------------------------- 1 | export enum Configuration { 2 | HideLiveLogWhenStoppingDebugger = 'rxjsDebugging.hideLiveLogWhenStoppingDebugger', 3 | RecommendOperatorLogPointsWithAnIcon = 'rxjsDebugging.recommendOperatorLogPointsWithAnIcon', 4 | LogLevel = 'rxjsDebugging.logLevel', 5 | EnableAnalytics = 'rxjsDebugging.enableUsageAnalytics', 6 | } 7 | -------------------------------------------------------------------------------- /packages/extension/src/debugConfigurationProvider.ts: -------------------------------------------------------------------------------- 1 | import { RUNTIME_PROGRAM_ENV_VAR } from '@rxjs-debugging/runtime/out/consts'; 2 | import { inject, injectable } from 'inversify'; 3 | import { join, resolve } from 'path'; 4 | import { DebugConfiguration, DebugConfigurationProvider, window, WorkspaceFolder } from 'vscode'; 5 | import { ILogger } from './logger'; 6 | import { IRxJSDetector } from './workspaceMonitor/detector'; 7 | 8 | const runtimeNodeJsPath = resolve(join(__dirname, 'runtime-nodejs', 'runtime.js')); 9 | 10 | export const INodeWithRxJSDebugConfigurationResolver = Symbol('NodeWithRxJSDebugConfigurationResolver'); 11 | 12 | @injectable() 13 | export class NodeWithRxJSDebugConfigurationResolver implements DebugConfigurationProvider { 14 | static readonly types = ['node', 'pwa-node']; 15 | 16 | constructor( 17 | @inject(IRxJSDetector) private readonly rxJsDetector: IRxJSDetector, 18 | @inject(ILogger) private readonly logger: ILogger 19 | ) {} 20 | 21 | async resolveDebugConfiguration( 22 | folder: WorkspaceFolder | undefined, 23 | debugConfiguration: DebugConfiguration & { __parentId?: string } 24 | ): Promise { 25 | if (!hasParentDebugConfiguration(debugConfiguration) && folder && (await this.rxJsDetector.detect(folder))) { 26 | this.logger.info('Extension', `Augment debug configuration "${debugConfiguration.name}" with NodeJS Runtime.`); 27 | const originalRuntimeArgs = debugConfiguration.runtimeArgs ?? []; 28 | const augmentedRuntimeArgs = [...originalRuntimeArgs, '-r', runtimeNodeJsPath]; 29 | 30 | const program = debugConfiguration.program ?? ''; 31 | const env = debugConfiguration.env ?? {}; 32 | 33 | if (program === '') { 34 | this.logger.error( 35 | 'Extension', 36 | `Debug configuration "${debugConfiguration.name}" is missing "program" property` 37 | ); 38 | window.showErrorMessage( 39 | `Add "program" property to debug configuration "${debugConfiguration.name}" to enable RxJS Debugging.` 40 | ); 41 | } 42 | 43 | const augmentedConfiguration = { 44 | ...debugConfiguration, 45 | env: { 46 | ...env, 47 | [RUNTIME_PROGRAM_ENV_VAR]: program, 48 | }, 49 | runtimeArgs: augmentedRuntimeArgs, 50 | }; 51 | 52 | return augmentedConfiguration; 53 | } 54 | 55 | return debugConfiguration; 56 | } 57 | } 58 | 59 | /** 60 | * This function checks the presence uf the `__parentId` property in the configuration. 61 | * 62 | * ## Context 63 | * vscode-js-debug creates debug sessions with a parent-child dependency. The child session is usually the actual 64 | * debugging session holding the CDP connection to the application under inspection. Such a child session can be 65 | * identified by the presence of the `__parentId` property in its configuration. This property is private API and might 66 | * change in the future. Use with care. 67 | * 68 | * @param debugConfiguration 69 | * @returns 70 | * @see Could be improved once https://github.com/microsoft/vscode/issues/123403 is resolved. 71 | */ 72 | export function hasParentDebugConfiguration(debugConfiguration: DebugConfiguration & { __parentId?: string }): boolean { 73 | return typeof debugConfiguration.__parentId === 'string'; 74 | } 75 | -------------------------------------------------------------------------------- /packages/extension/src/decoration/decorationSetter.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { DecorationOptions, Range, TextEditor, TextEditorDecorationType } from 'vscode'; 3 | 4 | export const IDecorationSetter = Symbol('DecorationSetter'); 5 | 6 | export interface IDecorationSetter { 7 | set( 8 | textEditor: TextEditor, 9 | decorationType: TextEditorDecorationType, 10 | rangeOrOptions: ReadonlyArray | ReadonlyArray 11 | ): void; 12 | } 13 | 14 | /** 15 | * Default implementation of an `IDecorationSetter`. It simply forwards decorations to the `TextEditor` given. 16 | */ 17 | @injectable() 18 | export default class DecorationSetter implements IDecorationSetter { 19 | set( 20 | textEditor: TextEditor, 21 | decorationType: TextEditorDecorationType, 22 | rangeOrOptions: ReadonlyArray | ReadonlyArray 23 | ): void { 24 | textEditor.setDecorations(decorationType, rangeOrOptions); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/extension/src/decoration/index.ts: -------------------------------------------------------------------------------- 1 | import { DecorationOptions, TextDocument, TextEditor, TextEditorDecorationType } from 'vscode'; 2 | import { IDisposable } from '../util/types'; 3 | import { IDecorationSetter } from './decorationSetter'; 4 | 5 | export interface IDecorationProvider extends IDisposable { 6 | /** 7 | * The `TextEditorDecorationType` provided by this `IDecorationProvider`. 8 | */ 9 | readonly decorationType: TextEditorDecorationType; 10 | 11 | attach(textEditors: ReadonlyArray): void; 12 | 13 | detach(): void; 14 | 15 | /** 16 | * (Re-)create `TextEditor` decorations. 17 | */ 18 | updateDecorations(): void; 19 | } 20 | 21 | /** 22 | * A `DocumentDecorationProvider` belongs to a specific `TextDocument` and provides decorations to multiple 23 | * `TextEditor`s showing that document. 24 | */ 25 | export abstract class DocumentDecorationProvider implements IDecorationProvider { 26 | private textEditors: ReadonlyArray = []; 27 | 28 | /** 29 | * @inheritdoc 30 | */ 31 | abstract decorationType: TextEditorDecorationType; 32 | 33 | constructor(protected readonly document: TextDocument, private readonly decorationSetter: IDecorationSetter) {} 34 | 35 | /** 36 | * Provide decorations to a new set of text editors. `attach` takes care that decorations are only provided if an 37 | * editor shows the document of this `DocumentDecorationProvider`. 38 | * 39 | * @param textEditors 40 | */ 41 | attach(textEditors: ReadonlyArray): void { 42 | this.detach(); 43 | this.textEditors = textEditors.filter((t) => t.document.uri.toString() === this.document.uri.toString()); 44 | this.updateDecorations(); 45 | } 46 | 47 | /** 48 | * Remove decorations from currently tracked `TextEditor`s and reset tracked editors. 49 | */ 50 | detach(): void { 51 | this.setDecorations([]); 52 | this.textEditors = []; 53 | } 54 | 55 | /** 56 | * (Re-)create `TextEditor` decorations. You can use `setDecorations` in the concrete `DocumentDecorationProvider` 57 | * implementation to update all currently tracked text editors with the new decorations. 58 | */ 59 | abstract updateDecorations(): void; 60 | 61 | /** 62 | * Set decorations for all `TextEditor`s currently served through this decoration provider. 63 | * 64 | * @param decorationOptions 65 | */ 66 | protected setDecorations(decorationOptions: DecorationOptions[]): void { 67 | for (const editor of this.textEditors) { 68 | this.decorationSetter.set(editor, this.decorationType, decorationOptions); 69 | } 70 | } 71 | 72 | dispose(): void { 73 | this.detach(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/extension/src/decoration/operatorLogPointDecorationProvider/createHoverMessageForLogPoint.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString } from 'vscode'; 2 | import * as nls from 'vscode-nls'; 3 | import { Commands } from '../../commands/commands'; 4 | import getMarkdownCommandWithArgs from '../../commands/getMarkdownCommandWithArgs'; 5 | import OperatorLogPoint from '../../operatorLogPoint'; 6 | 7 | const localize = nls.loadMessageBundle(); 8 | 9 | export default function createHoverMessageForLogPoint(operatorLogPoint: OperatorLogPoint): MarkdownString { 10 | const command: string = operatorLogPoint.enabled 11 | ? `[${localize( 12 | 'rxjs-debugging.operatorLogPointDecoration.removeOperatorLogPoint.title', 13 | `Remove RxJS operator log point for "{0}"`, 14 | operatorLogPoint.operatorName ?? 'n/a' 15 | )}](${getMarkdownCommandWithArgs(Commands.DisableOperatorLogPoint, [operatorLogPoint], ([o]) => [ 16 | OperatorLogPoint.serialize(o as OperatorLogPoint), 17 | ])} "${localize( 18 | 'rxjs-debugging.operatorLogPointDecoration.removeOperatorLogPoint.description', 19 | 'Stop logging events emitted by this operator.' 20 | )}")` 21 | : `[${localize( 22 | 'rxjs-debugging.operatorLogPointDecoration.addOperatorLogPoint.title', 23 | `Add RxJS operator log point for "{0}"`, 24 | operatorLogPoint.operatorName ?? 'n/a' 25 | )}](${getMarkdownCommandWithArgs(Commands.EnableOperatorLogPoint, [operatorLogPoint], ([o]) => [ 26 | OperatorLogPoint.serialize(o as OperatorLogPoint), 27 | ])} "${localize( 28 | 'rxjs-debugging.operatorLogPointDecoration.addOperatorLogPoint.description', 29 | 'Log events emitted by this operator.' 30 | )}")`; 31 | 32 | const hoverMessage = new MarkdownString(command, true); 33 | hoverMessage.isTrusted = true; 34 | 35 | return hoverMessage; 36 | } 37 | -------------------------------------------------------------------------------- /packages/extension/src/decoration/operatorLogPointGutterIconDecorationProvider/getEnabledState.ts: -------------------------------------------------------------------------------- 1 | import OperatorLogPoint from '../../operatorLogPoint'; 2 | 3 | export type EnabledState = 'all' | 'some' | 'none'; 4 | 5 | export default function getEnabledState(logPoints: ReadonlyArray): EnabledState { 6 | if (logPoints.length === 0) { 7 | return 'none'; 8 | } 9 | 10 | const enabledLogPoints = logPoints.filter(({ enabled }) => enabled); 11 | 12 | if (enabledLogPoints.length === 0) { 13 | return 'none'; 14 | } 15 | 16 | if (enabledLogPoints.length === logPoints.length) { 17 | return 'all'; 18 | } 19 | 20 | return 'some'; 21 | } 22 | -------------------------------------------------------------------------------- /packages/extension/src/decoration/operatorLogPointGutterIconDecorationProvider/getIconForEnabledState.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import { IResourceProvider } from '../../resources'; 3 | import { EnabledState } from './getEnabledState'; 4 | 5 | export default function getIconForEnabledState(resourceProvider: IResourceProvider, enabledState: EnabledState): Uri { 6 | switch (enabledState) { 7 | case 'all': 8 | return resourceProvider.uriForResource('debug-breakpoint-log.svg'); 9 | case 'some': 10 | return resourceProvider.uriForResource('debug-breakpoint-log.svg'); 11 | case 'none': 12 | return resourceProvider.uriForResource('debug-breakpoint-log-unverified.svg'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import * as vscode from 'vscode'; 3 | import * as nls from 'vscode-nls'; 4 | import askToOptInToAnalyticsReporter from './analytics/askToOptInToAnalyticsReporter'; 5 | import registerOperatorLogPointManagementCommands from './commands/operatorLogPointManagement'; 6 | import registerToggleOperatorLogPointDecorationCommand from './commands/toggleOperatorLogPointGutterIcon'; 7 | import { 8 | INodeWithRxJSDebugConfigurationResolver, 9 | NodeWithRxJSDebugConfigurationResolver, 10 | } from './debugConfigurationProvider'; 11 | import type { default as prepareForIntegrationTestType } from './integrationTest/prepareForIntegrationTest'; 12 | import createRootContainer from './ioc/rootContainer'; 13 | import { ILogger } from './logger'; 14 | import { IConfigurationAccessor } from './configuration/configurationAccessor'; 15 | 16 | nls.config({ messageFormat: nls.MessageFormat.file })(); 17 | 18 | // prepareForIntegrationTest might be injected during build time. See rollup.config.js 19 | declare const prepareForIntegrationTest: typeof prepareForIntegrationTestType | undefined; 20 | 21 | export function activate(context: vscode.ExtensionContext): void { 22 | const rootContainer = createRootContainer( 23 | context, 24 | typeof prepareForIntegrationTest === 'function' ? prepareForIntegrationTest(context) : undefined 25 | ); 26 | context.subscriptions.push(rootContainer); 27 | 28 | for (const type of NodeWithRxJSDebugConfigurationResolver.types) { 29 | vscode.debug.registerDebugConfigurationProvider( 30 | type, 31 | rootContainer.get(INodeWithRxJSDebugConfigurationResolver) 32 | ); 33 | } 34 | 35 | const configurationAccessor: IConfigurationAccessor = rootContainer.get(IConfigurationAccessor); 36 | registerOperatorLogPointManagementCommands(context, rootContainer); 37 | registerToggleOperatorLogPointDecorationCommand(context, configurationAccessor); 38 | 39 | void askToOptInToAnalyticsReporter(configurationAccessor); 40 | 41 | rootContainer.get(ILogger).info('Extension', 'Ready'); 42 | } 43 | 44 | export function deactivate(): void { 45 | // Nothing to do. 46 | } 47 | -------------------------------------------------------------------------------- /packages/extension/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A string containing the extensions version. 3 | * Provided during build through RollupJS. 4 | */ 5 | declare const EXTENSION_VERSION: string; 6 | 7 | /** 8 | * URL/Host of the Posthog installation to track analytic events. 9 | * Provided during build through RollupJS. 10 | * 11 | * @see https://posthog.com/docs/integrate/server/node#options 12 | */ 13 | declare const POSTHOG_HOST: string; 14 | 15 | /** 16 | * Posthog Project API key for tracking analytic events. 17 | * Provided during build through RollupJS. 18 | * 19 | * @see https://posthog.com/docs/integrate/server/node#options 20 | */ 21 | declare const POSTHOG_PROJECT_API_KEY: string; 22 | -------------------------------------------------------------------------------- /packages/extension/src/integrationTest/decorationSetterSpy.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import * as path from 'path'; 3 | import 'reflect-metadata'; 4 | import * as vscode from 'vscode'; 5 | import type { IDecorationSetter } from '../decoration/decorationSetter'; 6 | 7 | @injectable() 8 | export default class DecorationSetterSpy implements IDecorationSetter { 9 | static recordedCalls: Map< 10 | string, 11 | ReadonlyArray<{ decorationType: string; ranges: ReadonlyArray; options: ReadonlyArray }> 12 | > = new Map(); 13 | 14 | set( 15 | textEditor: vscode.TextEditor, 16 | decorationType: vscode.TextEditorDecorationType, 17 | rangeOrOptions: ReadonlyArray | ReadonlyArray 18 | ): void { 19 | const file = path.relative( 20 | vscode.workspace.getWorkspaceFolder(textEditor.document.uri)?.uri.fsPath ?? 'n/a', 21 | textEditor.document.fileName 22 | ); 23 | const recordedForFile = DecorationSetterSpy.recordedCalls.get(file) ?? []; 24 | 25 | const ranges: string[] = []; 26 | const options: string[] = []; 27 | 28 | rangeOrOptions.map((x) => { 29 | if (x instanceof vscode.Range) { 30 | ranges.push(`(${x.start.line}:${x.start.character}):(${x.end.line}:${x.end.character})`); 31 | } else { 32 | options.push( 33 | `(${x.range.start.line}:${x.range.start.character}):(${x.range.end.line}:${ 34 | x.range.end.character 35 | })-${x.hoverMessage?.toString()}-${JSON.stringify(x.renderOptions)}` 36 | ); 37 | } 38 | }); 39 | 40 | DecorationSetterSpy.recordedCalls.set(file, [ 41 | ...recordedForFile, 42 | { decorationType: decorationType.key, ranges, options }, 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/extension/src/integrationTest/executeCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ICommandTypes } from '../commands/commands'; 3 | import { ITestCommandTypes } from './testCommands'; 4 | 5 | type AllCommandTypes = ICommandTypes & ITestCommandTypes; 6 | 7 | export default function executeCommand( 8 | ns: typeof vscode.commands, 9 | key: K, 10 | ...args: Parameters 11 | ): Thenable> { 12 | return ns.executeCommand(key, ...args) as Thenable>; 13 | } 14 | -------------------------------------------------------------------------------- /packages/extension/src/integrationTest/index.ts: -------------------------------------------------------------------------------- 1 | import { Commands } from '../commands/commands'; 2 | import { Configuration } from '../configuration'; 3 | import { LogLevel } from '../logger'; 4 | import executeCommand from './executeCommand'; 5 | import { TestCommands } from './testCommands'; 6 | import OperatorLogPoint from '../operatorLogPoint'; 7 | /** 8 | * This is the entry point for extension-integrationtest. It provides a minimal API to interact with the extension 9 | * during a test run. 10 | */ 11 | 12 | export { executeCommand, Commands, TestCommands, Configuration, LogLevel, OperatorLogPoint }; 13 | -------------------------------------------------------------------------------- /packages/extension/src/integrationTest/prepareForIntegrationTest.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { IDecorationSetter } from '../decoration/decorationSetter'; 3 | import LiveLogDecorationProvider from '../decoration/liveLogDecorationProvider'; 4 | import OperatorLogPointDecorationProvider from '../decoration/operatorLogPointDecorationProvider'; 5 | import OperatorLogPointGutterIconDecorationProvider from '../decoration/operatorLogPointGutterIconDecorationProvider'; 6 | import DecorationSetterSpy from './decorationSetterSpy'; 7 | import registerTestCommand from './registerTestCommand'; 8 | import { ITestCommandTypes, TestCommands } from './testCommands'; 9 | 10 | /** 11 | * Prepares the extension for an integration test run. 12 | * 13 | * @param context 14 | * @returns 15 | */ 16 | export default function prepareForIntegrationTest(context: vscode.ExtensionContext): { 17 | DecorationSetter: { new (): IDecorationSetter }; 18 | } { 19 | context.subscriptions.push( 20 | registerTestCommand(vscode.commands, TestCommands.GetDecorationSetterRecording, async (file, decorationType) => { 21 | const recordedDecorations = DecorationSetterSpy.recordedCalls.get(file) ?? []; 22 | const typeKey = decorationTypeKeyForDecorationType(decorationType); 23 | 24 | return recordedDecorations.filter(({ decorationType }) => decorationType === typeKey); 25 | }) 26 | ); 27 | 28 | return { 29 | DecorationSetter: DecorationSetterSpy, 30 | }; 31 | } 32 | 33 | function decorationTypeKeyForDecorationType( 34 | decorationType: Parameters[1] 35 | ): string { 36 | switch (decorationType) { 37 | case 'liveLog': 38 | return LiveLogDecorationProvider.decorationTypeKey; 39 | case 'logPointGutterIcon': 40 | return OperatorLogPointGutterIconDecorationProvider.decorationTypeKey; 41 | case 'logPoints': 42 | return OperatorLogPointDecorationProvider.decorationTypeKey; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/extension/src/integrationTest/registerTestCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ITestCommandTypes } from './testCommands'; 3 | 4 | /** 5 | * Registers a known test command using declared types. 6 | * 7 | * @param ns 8 | * @param key 9 | * @param fn 10 | * @returns 11 | * @see Thanks https://github.com/microsoft/vscode-js-debug/blob/main/src/common/contributionUtils.ts#L171 12 | */ 13 | export default function registerTestCommand( 14 | ns: typeof vscode.commands, 15 | key: K, 16 | fn: (...args: Parameters) => Thenable> 17 | ): vscode.Disposable { 18 | return ns.registerCommand(key, fn); 19 | } 20 | -------------------------------------------------------------------------------- /packages/extension/src/integrationTest/testCommands.ts: -------------------------------------------------------------------------------- 1 | export const enum TestCommands { 2 | GetDecorationSetterRecording = 'rxjs-debugging-for-vs-code.command.test.getDecorationSetterRecording', 3 | } 4 | 5 | export interface ITestCommandTypes { 6 | [TestCommands.GetDecorationSetterRecording]: ( 7 | file: string, 8 | decorationType: 'liveLog' | 'logPoints' | 'logPointGutterIcon' 9 | ) => ReadonlyArray<{ 10 | decorationType: string; 11 | ranges: ReadonlyArray; 12 | options: ReadonlyArray; 13 | }>; 14 | } 15 | -------------------------------------------------------------------------------- /packages/extension/src/ioc/disposableContainer.ts: -------------------------------------------------------------------------------- 1 | import { Container, interfaces } from 'inversify'; 2 | import { IDisposable } from '../util/types'; 3 | import { ILogger } from '../logger'; 4 | 5 | export interface IDisposableContainer extends interfaces.Container, IDisposable {} 6 | 7 | export default class DisposableContainer extends Container implements IDisposableContainer { 8 | private readonly disposables: IDisposable[] = []; 9 | 10 | constructor(readonly name: string, options?: interfaces.ContainerOptions | undefined) { 11 | super(options); 12 | } 13 | 14 | trackDisposableBinding = (_context: interfaces.Context, injectable: T): T => { 15 | this.disposables.push(injectable); 16 | return injectable; 17 | }; 18 | 19 | dispose(): void { 20 | this.get(ILogger).info('IoC', `Dispose IoC Container "${this.name}"`); 21 | 22 | for (const disposable of this.disposables) { 23 | disposable.dispose(); 24 | } 25 | this.unbindAll(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/extension/src/ioc/sessionContainer.ts: -------------------------------------------------------------------------------- 1 | import { interfaces } from 'inversify'; 2 | import Session, { ISession } from '../sessionManager/session'; 3 | import TelemetryBridge, { ITelemetryBridge } from '../telemetryBridge'; 4 | import { ICDPClientAddress } from '../telemetryBridge/cdpClient'; 5 | import DisposableContainer, { IDisposableContainer } from './disposableContainer'; 6 | 7 | export default function createSessionContainer( 8 | parent: interfaces.Container, 9 | name: string, 10 | cdpClientAddress: ICDPClientAddress 11 | ): IDisposableContainer { 12 | const container = new DisposableContainer(name); 13 | container.parent = parent; 14 | 15 | container.bind(ICDPClientAddress).toConstantValue(cdpClientAddress); 16 | 17 | container.bind(ISession).to(Session).inSingletonScope().onActivation(container.trackDisposableBinding); 18 | container 19 | .bind(ITelemetryBridge) 20 | .to(TelemetryBridge) 21 | .inSingletonScope() 22 | .onActivation(container.trackDisposableBinding); 23 | 24 | return container; 25 | } 26 | -------------------------------------------------------------------------------- /packages/extension/src/ioc/types.ts: -------------------------------------------------------------------------------- 1 | export const VsCodeApi = Symbol('VsCodeApi'); 2 | export const ExtensionContext = Symbol('ExtensionContext'); 3 | export const RootContainer = Symbol('RootContainer'); 4 | -------------------------------------------------------------------------------- /packages/extension/src/logger/console.ts: -------------------------------------------------------------------------------- 1 | import { ILogSink, LogDomain, LogLevel } from '.'; 2 | 3 | export default class ConsoleLogSink implements ILogSink { 4 | log(level: LogLevel, domain: LogDomain, message: string): void { 5 | console.log(`${niceLogLevels[level]} (${domain}) ${message}`); 6 | } 7 | } 8 | 9 | const niceLogLevels: Record = { 10 | [LogLevel.Info]: '[INFO] ', 11 | [LogLevel.Warn]: '[WARN] ', 12 | [LogLevel.Error]: '[ERROR]', 13 | [LogLevel.Never]: '[-] ', 14 | }; 15 | -------------------------------------------------------------------------------- /packages/extension/src/logger/index.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | 3 | export const enum LogLevel { 4 | Info = 0, 5 | Warn, 6 | Error, 7 | Never, 8 | } 9 | 10 | export function logLevelFromString(s: string, defaultLogLevel = LogLevel.Never): LogLevel { 11 | switch (s) { 12 | case 'Info': 13 | return LogLevel.Info; 14 | case 'Warn': 15 | return LogLevel.Warn; 16 | case 'Error': 17 | return LogLevel.Error; 18 | case 'Never': 19 | return LogLevel.Never; 20 | default: 21 | return defaultLogLevel; 22 | } 23 | } 24 | 25 | export type LogDomain = 26 | | 'Extension' 27 | | 'IoC' 28 | | 'LogPointManager' 29 | | 'SessionManager' 30 | | 'Session' 31 | | 'DecorationManager' 32 | | 'OperatorLogPointRecommender' 33 | | 'WorkspaceMonitor' 34 | | 'Detector' 35 | | 'TelemetryBridge' 36 | | 'AnalyticsReporter'; 37 | 38 | export const ILogger = Symbol('ILogger'); 39 | 40 | export interface ILogger { 41 | log(level: LogLevel, domain: LogDomain, message: string): void; 42 | info(domain: LogDomain, message: string): void; 43 | warn(domain: LogDomain, message: string): void; 44 | error(domain: LogDomain, message: string): void; 45 | } 46 | 47 | export interface ILogSink { 48 | log: (level: LogLevel, domain: LogDomain, message: string) => void; 49 | } 50 | 51 | @injectable() 52 | export default class Logger implements ILogger { 53 | constructor( 54 | private readonly sinks: ReadonlyArray, 55 | private readonly minLogLevel: LogLevel = LogLevel.Never 56 | ) {} 57 | 58 | log(level: LogLevel, domain: LogDomain, message: string): void { 59 | if (level < this.minLogLevel) { 60 | return; 61 | } 62 | 63 | for (const target of this.sinks) { 64 | target.log(level, domain, message); 65 | } 66 | } 67 | 68 | info(domain: LogDomain, message: string): void { 69 | this.log(LogLevel.Info, domain, message); 70 | } 71 | 72 | warn(domain: LogDomain, message: string): void { 73 | this.log(LogLevel.Warn, domain, message); 74 | } 75 | 76 | error(domain: LogDomain, message: string): void { 77 | this.log(LogLevel.Error, domain, message); 78 | } 79 | 80 | /** 81 | * Create a `Logger` that never logs nothing. 82 | * 83 | * @returns 84 | */ 85 | static nullLogger(): ILogger { 86 | return new Logger([]); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/extension/src/operatorLogPoint/index.fixture.ts: -------------------------------------------------------------------------------- 1 | import { Position, Uri } from 'vscode'; 2 | import OperatorLogPoint from '.'; 3 | 4 | export const logPointFixtureA = new OperatorLogPoint( 5 | Uri.file('/foo.ts'), 6 | new Position(42, 84), 7 | { 8 | operatorIndex: 100, 9 | fileName: 'foo.ts', 10 | line: 101, 11 | character: 102, 12 | }, 13 | 'take' 14 | ); 15 | 16 | export const logPointFixtureB = new OperatorLogPoint( 17 | Uri.file('/bar.ts'), 18 | new Position(128, 256), 19 | { 20 | operatorIndex: 201, 21 | fileName: 'bar.ts', 22 | line: 202, 23 | character: 203, 24 | }, 25 | null, 26 | true 27 | ); 28 | -------------------------------------------------------------------------------- /packages/extension/src/operatorLogPoint/index.test.ts: -------------------------------------------------------------------------------- 1 | import OperatorLogPoint from '.'; 2 | import { logPointFixtureA, logPointFixtureB } from './index.fixture'; 3 | 4 | describe('OperatorLogPoint', () => { 5 | describe('with()', () => { 6 | const original = logPointFixtureA; 7 | 8 | test('creates a copy of the OperatorLogPoint and applies given properties to the copy', () => { 9 | const copy = original.with({ 10 | uri: logPointFixtureB.uri, 11 | sourcePosition: logPointFixtureB.sourcePosition, 12 | operatorIdentifier: logPointFixtureB.operatorIdentifier, 13 | operatorName: logPointFixtureB.operatorName, 14 | enabled: logPointFixtureB.enabled, 15 | }); 16 | 17 | expect(copy).not.toBe(original); 18 | expect(copy.uri).toEqual(logPointFixtureB.uri); 19 | expect(copy.sourcePosition).toEqual(logPointFixtureB.sourcePosition); 20 | expect(copy.operatorIdentifier).toEqual(logPointFixtureB.operatorIdentifier); 21 | expect(copy.operatorName).toEqual(logPointFixtureB.operatorName); 22 | expect(copy.enabled).toEqual(logPointFixtureB.enabled); 23 | }); 24 | }); 25 | 26 | describe('serialize()', () => { 27 | test('returns an IOperatorLogPoint of an OperatorLogPoint', () => { 28 | expect(OperatorLogPoint.serialize(logPointFixtureA)).toMatchInlineSnapshot( 29 | `"{\\"uri\\":{\\"scheme\\":\\"\\",\\"authority\\":\\"\\",\\"path\\":\\"/foo.ts\\",\\"query\\":\\"\\",\\"fragment\\":\\"\\"},\\"sourcePosition\\":{\\"line\\":42,\\"character\\":84},\\"operatorIdentifier\\":{\\"operatorIndex\\":100,\\"fileName\\":\\"foo.ts\\",\\"line\\":101,\\"character\\":102},\\"operatorName\\":\\"take\\",\\"enabled\\":false}"` 30 | ); 31 | }); 32 | }); 33 | 34 | describe('parse()', () => { 35 | test('creates an OperatorLogPoint from its serialized string representation', () => { 36 | expect( 37 | OperatorLogPoint.parse( 38 | '{"uri":{"scheme":"","authority":"","path":"/foo.ts","query":"","fragment":""},"sourcePosition":{"line":42,"character":84},"operatorIdentifier":{"operatorIndex":100,"fileName":"foo.ts","line":101,"character":102},"operatorName":"take","enabled":false}' 39 | ) 40 | ).toEqual(logPointFixtureA); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/extension/src/operatorLogPoint/index.ts: -------------------------------------------------------------------------------- 1 | import { IOperatorIdentifier } from '@rxjs-debugging/telemetry/out/operatorIdentifier'; 2 | import operatorIdentifierToString from '@rxjs-debugging/telemetry/out/operatorIdentifier/toString'; 3 | import { aBoolean, aNull, aNumber, aString, fromObject, or, ParseFn, ParserError, withObject } from 'spicery'; 4 | import { Position, Uri } from 'vscode'; 5 | 6 | interface IOperatorLogPoint { 7 | uri: Uri; 8 | sourcePosition: Position; 9 | operatorIdentifier: IOperatorIdentifier; 10 | operatorName: string | null; 11 | enabled: boolean; 12 | key: string; 13 | } 14 | 15 | export default class OperatorLogPoint implements IOperatorLogPoint { 16 | /** 17 | * The position of the underlying operator in the source code. 18 | */ 19 | public readonly sourcePosition: Position; 20 | 21 | constructor( 22 | readonly uri: Uri, 23 | sourcePosition: Position, 24 | 25 | /** 26 | * The `IOperatorIdentifier` allowing to identify the underlying operator of this log point across vscode and the 27 | * runtime. See `IOperatorIdentifier` for more details. 28 | * 29 | * @see IOperatorIdentifier 30 | */ 31 | readonly operatorIdentifier: IOperatorIdentifier, 32 | 33 | readonly operatorName: string | null, 34 | 35 | readonly enabled = false 36 | ) { 37 | this.sourcePosition = new Position(sourcePosition.line, sourcePosition.character); // Recreate to prevent side effects 🤷‍♂️ 38 | } 39 | 40 | get key(): string { 41 | return operatorIdentifierToString(this.operatorIdentifier); 42 | } 43 | 44 | toString(): string { 45 | return this.key; 46 | } 47 | 48 | with(change: Partial): OperatorLogPoint { 49 | return new OperatorLogPoint( 50 | change.uri ?? this.uri, 51 | change.sourcePosition ?? this.sourcePosition, 52 | change.operatorIdentifier ?? this.operatorIdentifier, 53 | change.operatorName !== undefined ? change.operatorName : this.operatorName, 54 | change.enabled ?? this.enabled 55 | ); 56 | } 57 | 58 | static serialize({ uri, sourcePosition, operatorIdentifier, operatorName, enabled }: OperatorLogPoint): string { 59 | return JSON.stringify({ 60 | uri: uri.toJSON(), 61 | sourcePosition, 62 | operatorIdentifier, 63 | operatorName, 64 | enabled, 65 | }); 66 | } 67 | 68 | static parse(x: string): OperatorLogPoint { 69 | const json = JSON.parse(x); 70 | return withObject( 71 | (o) => 72 | new OperatorLogPoint( 73 | fromObject(o, 'uri', anUri), 74 | fromObject(o, 'sourcePosition', aPosition), 75 | fromObject(o, 'operatorIdentifier', anOperatorIdentifier), 76 | fromObject(o, 'operatorName', or(aString, aNull)), 77 | fromObject(o, 'enabled', aBoolean) 78 | ) 79 | )(json); 80 | } 81 | } 82 | 83 | const anUri: ParseFn = withObject[0]>((o) => { 84 | if (typeof o.scheme !== 'string') { 85 | throw new ParserError('Uri', JSON.stringify(o)); 86 | } 87 | return Uri.from(o); 88 | }); 89 | const aPosition: ParseFn = withObject( 90 | (o) => new Position(fromObject(o, 'line', aNumber), fromObject(o, 'character', aNumber)) 91 | ); 92 | const anOperatorIdentifier: ParseFn = withObject((o) => ({ 93 | character: fromObject(o, 'character', aNumber), 94 | fileName: fromObject(o, 'fileName', aString), 95 | line: fromObject(o, 'line', aNumber), 96 | operatorIndex: fromObject(o, 'operatorIndex', aNumber), 97 | })); 98 | -------------------------------------------------------------------------------- /packages/extension/src/operatorLogPoint/manager/index.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import OperatorLogPointManager from '.'; 3 | import { IAnalyticsReporter } from '../../analytics'; 4 | import Logger from '../../logger'; 5 | import { logPointFixtureA, logPointFixtureB } from '../index.fixture'; 6 | import { IOperatorLogPointRecommender } from '../recommender'; 7 | 8 | describe('OperatorLogPointManager', () => { 9 | let logPointManager: OperatorLogPointManager; 10 | let recommender: IOperatorLogPointRecommender; 11 | let analyticsReporter: IAnalyticsReporter; 12 | 13 | beforeEach(() => { 14 | recommender = { 15 | dispose: jest.fn(), 16 | onRecommendOperatorLogPoints: jest.fn(), 17 | recommend: jest.fn(), 18 | }; 19 | analyticsReporter = { 20 | captureDebugSessionStarted: jest.fn(), 21 | captureDebugSessionStopped: jest.fn(), 22 | captureOperatorLogPointDisabled: jest.fn(), 23 | captureOperatorLogPointEnabled: jest.fn(), 24 | dispose: jest.fn(), 25 | }; 26 | logPointManager = new OperatorLogPointManager(recommender, analyticsReporter, Logger.nullLogger()); 27 | }); 28 | 29 | describe('enable()', () => { 30 | test('calls onDidChangeLogPoints handlers with all enabled log points', () => { 31 | const spy = jest.fn(); 32 | 33 | logPointManager.onDidChangeLogPoints(spy); 34 | logPointManager.enable(logPointFixtureA); 35 | logPointManager.enable(logPointFixtureB); 36 | logPointManager.enable(logPointFixtureA); 37 | 38 | expect(spy).toBeCalledTimes(2); 39 | expect(spy).toHaveBeenNthCalledWith(1, [logPointFixtureA.with({ enabled: true })]); 40 | expect(spy).toHaveBeenNthCalledWith(2, [ 41 | logPointFixtureA.with({ enabled: true }), 42 | logPointFixtureB.with({ enabled: true }), 43 | ]); 44 | }); 45 | }); 46 | 47 | describe('disable()', () => { 48 | test('calls onDidChangeLogPoints handlers with all enabled log points', () => { 49 | const spy = jest.fn(); 50 | 51 | logPointManager.enable(logPointFixtureA); 52 | logPointManager.enable(logPointFixtureB); 53 | logPointManager.onDidChangeLogPoints(spy); 54 | logPointManager.disable(logPointFixtureA); 55 | logPointManager.disable(logPointFixtureA); 56 | 57 | expect(spy).toBeCalledTimes(1); 58 | expect(spy).toHaveBeenNthCalledWith(1, [logPointFixtureB.with({ enabled: true })]); 59 | }); 60 | }); 61 | 62 | describe('logPoints', () => { 63 | test('provides enabled, unique log points as a list', () => { 64 | expect(logPointManager.logPoints).toEqual([]); 65 | 66 | logPointManager.enable(logPointFixtureA); 67 | expect(logPointManager.logPoints).toEqual([logPointFixtureA.with({ enabled: true })]); 68 | 69 | logPointManager.enable(logPointFixtureA); 70 | expect(logPointManager.logPoints).toEqual([logPointFixtureA.with({ enabled: true })]); 71 | 72 | logPointManager.disable(logPointFixtureB); 73 | expect(logPointManager.logPoints).toEqual([logPointFixtureA.with({ enabled: true })]); 74 | 75 | logPointManager.disable(logPointFixtureA); 76 | expect(logPointManager.logPoints).toEqual([]); 77 | }); 78 | }); 79 | 80 | describe('logPointForIdentifier()', () => { 81 | test('returns the matching OperatorLogPoint for an identifier', () => { 82 | logPointManager.enable(logPointFixtureA); 83 | expect(logPointManager.logPointForIdentifier(logPointFixtureA.operatorIdentifier)).toEqual( 84 | logPointFixtureA.with({ enabled: true }) 85 | ); 86 | }); 87 | 88 | test('returns the undefined for an unknown identifier', () => { 89 | expect(logPointManager.logPointForIdentifier(logPointFixtureA.operatorIdentifier)).toBeUndefined(); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/extension/src/operatorLogPoint/manager/merger.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Position } from 'vscode'; 3 | import { logPointFixtureA } from '../index.fixture'; 4 | import OperatorLogPointMerger, { IOperatorLogPointMerger } from './merger'; 5 | 6 | /* This piece of code is the basis for the following tests: 7 | 8 | 01234567801234 9 | 0 interval(10).pipe( 10 | 1 take(1), 11 | 2 map(x => x), // Enabled Log Point 12 | 3 flatMap(x => of(x)) 13 | 4 ) 14 | 15 | The OperatorLogPoints below match this code snippet accordingly: 16 | */ 17 | const logPointTake = logPointFixtureA.with({ 18 | operatorName: 'take', 19 | sourcePosition: new Position(1, 1), 20 | operatorIdentifier: { 21 | fileName: '', 22 | line: 0, 23 | character: 14, 24 | operatorIndex: 0, 25 | }, 26 | }); 27 | const logPointMap = logPointFixtureA.with({ 28 | enabled: true, 29 | operatorName: 'map', 30 | sourcePosition: new Position(2, 1), 31 | operatorIdentifier: { 32 | fileName: '', 33 | line: 0, 34 | character: 14, 35 | operatorIndex: 1, 36 | }, 37 | }); 38 | const logPointFlatMap = logPointFixtureA.with({ 39 | operatorName: 'flatMap', 40 | sourcePosition: new Position(3, 1), 41 | operatorIdentifier: { 42 | fileName: '', 43 | line: 0, 44 | character: 14, 45 | operatorIndex: 2, 46 | }, 47 | }); 48 | 49 | describe('OperatorLogPointMerger', () => { 50 | let merger: IOperatorLogPointMerger; 51 | 52 | beforeEach(() => { 53 | merger = new OperatorLogPointMerger(); 54 | }); 55 | 56 | test('keeps enabled log points, which are still recommended and did not change', () => { 57 | const prev = [logPointMap]; 58 | const next = prev; 59 | const result = merger.merge(prev, next); 60 | 61 | expect(result).toEqual(prev); 62 | }); 63 | 64 | test('discards enabled log points, which are not recommended anymore', () => { 65 | const prev = [logPointMap]; 66 | const result = merger.merge(prev, []); 67 | 68 | expect(result).toHaveLength(0); 69 | }); 70 | 71 | test('does not return log points, which are newly recommended', () => { 72 | const next = [logPointMap]; 73 | const result = merger.merge([], next); 74 | 75 | expect(result).toEqual([]); 76 | }); 77 | 78 | test('merges enabled log points, which got a new source position', () => { 79 | /* Updated code: 80 | 01234567801234 81 | 0 interval(10).pipe( 82 | 1 take(1), 83 | 2 // <-- Blank, new line added 84 | 3 map(x => x), // Expected to get an updated log point for this operator 85 | 4 flatMap(x => of(x)) 86 | 5 ) 87 | */ 88 | const prev = [logPointMap]; 89 | const next = [ 90 | logPointTake, 91 | logPointMap.with({ sourcePosition: new Position(3, 1) }), 92 | logPointFlatMap.with({ sourcePosition: new Position(4, 1) }), 93 | ]; 94 | const result = merger.merge(prev, next); 95 | 96 | expect(result).toEqual([logPointMap.with({ sourcePosition: new Position(3, 1) })]); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /packages/extension/src/operatorLogPoint/recommender/index.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import * as vscode from 'vscode'; 3 | import OperatorLogPoint from '..'; 4 | import { ILogger } from '../../logger'; 5 | import { IDisposable } from '../../util/types'; 6 | import isSupportedDocument from '../../workspaceMonitor/isSupportedDocument'; 7 | import getOperatorPositions from './parser'; 8 | 9 | export const IOperatorLogPointRecommender = Symbol('LogPointRecommender'); 10 | 11 | export interface IOperatorLogPointRecommendationEvent { 12 | documentUri: vscode.Uri; 13 | operatorLogPoints: ReadonlyArray; 14 | } 15 | export interface IOperatorLogPointRecommender extends IDisposable { 16 | onRecommendOperatorLogPoints: vscode.Event; 17 | recommend(document: vscode.TextDocument): void; 18 | } 19 | 20 | @injectable() 21 | export default class OperatorLogPointRecommender implements IOperatorLogPointRecommender { 22 | private _onRecommendOperatorLogPoints = new vscode.EventEmitter(); 23 | get onRecommendOperatorLogPoints(): vscode.Event { 24 | return this._onRecommendOperatorLogPoints.event; 25 | } 26 | 27 | constructor(@inject(ILogger) private readonly logger: ILogger) {} 28 | 29 | async recommend(document: vscode.TextDocument): Promise { 30 | if (!isSupportedDocument(document)) { 31 | return; 32 | } 33 | 34 | this.logger.info('OperatorLogPointRecommender', `Recommend log points for ${document.uri.toString()}`); 35 | 36 | const positions = await getOperatorPositions(document.getText()); 37 | const logPoints = positions.map( 38 | ({ position, operatorName, operatorIdentifier }) => 39 | new OperatorLogPoint( 40 | document.uri, 41 | new vscode.Position(position.line, position.character), 42 | { 43 | fileName: document.uri.fsPath, 44 | ...operatorIdentifier, 45 | }, 46 | operatorName 47 | ) 48 | ); 49 | 50 | this._onRecommendOperatorLogPoints.fire({ 51 | documentUri: document.uri, 52 | operatorLogPoints: logPoints, 53 | }); 54 | } 55 | 56 | dispose(): void { 57 | this._onRecommendOperatorLogPoints.dispose(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/extension/src/operatorLogPoint/recommender/parser.ts: -------------------------------------------------------------------------------- 1 | import { IOperatorIdentifier } from '@rxjs-debugging/telemetry/out/operatorIdentifier'; 2 | import { 3 | CallExpression, 4 | createSourceFile, 5 | LineAndCharacter, 6 | Node, 7 | PropertyAccessExpression, 8 | ScriptTarget, 9 | SourceFile, 10 | SyntaxKind, 11 | } from 'typescript'; 12 | 13 | interface Result { 14 | /** 15 | * The actual position of the operator in the source code. 16 | */ 17 | position: Omit; 18 | 19 | /** 20 | * The name of the operator, e.g. `map` or `take`. 21 | */ 22 | operatorName: string | null; 23 | 24 | /** 25 | * The `IOperatorIdentifier` (without filename) identifying an operator. 26 | * 27 | * @see IOperatorIdentifier 28 | */ 29 | operatorIdentifier: Omit; 30 | } 31 | 32 | export default async function getOperatorPositions(sourceCode: string): Promise> { 33 | const sourceFile = createSourceFile('parsed', sourceCode, ScriptTarget.Latest); 34 | 35 | const operatorPositions: Array = []; 36 | const children = collectChildren(sourceFile, sourceFile); 37 | 38 | for (const node of children) { 39 | if (node.kind === SyntaxKind.CallExpression) { 40 | const callExpression = node as CallExpression; 41 | const [firstCallExpressionChild] = getChildren(callExpression); 42 | 43 | if (firstCallExpressionChild.kind === SyntaxKind.PropertyAccessExpression) { 44 | const propertyAccessExpression = firstCallExpressionChild as PropertyAccessExpression; 45 | const { name } = propertyAccessExpression; 46 | const nameStart = getStartOf(name, sourceFile); 47 | 48 | if (name.getText(sourceFile) === 'pipe') { 49 | for (let i = 0, l = callExpression.arguments.length; i < l; i++) { 50 | const operator = callExpression.arguments[i]; 51 | const operatorName = getOperatorName(operator, sourceFile); 52 | 53 | operatorPositions.push({ 54 | position: getStartOf(operator, sourceFile), 55 | operatorName, 56 | operatorIdentifier: { line: nameStart.line + 1, character: nameStart.character + 1, operatorIndex: i }, 57 | }); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | return operatorPositions; 65 | } 66 | 67 | function collectChildren(parent: Node, sourceFile: SourceFile, collector: Array = []): ReadonlyArray { 68 | parent.forEachChild((c) => { 69 | collector.push(c); 70 | collectChildren(c, sourceFile, collector); 71 | }); 72 | return collector; 73 | } 74 | 75 | function getChildren(parent: Node): ReadonlyArray { 76 | const children: Array = []; 77 | parent.forEachChild((c) => children.push(c)); 78 | return children; 79 | } 80 | 81 | function getStartOf(node: Node, sourceFile: SourceFile): LineAndCharacter { 82 | return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); 83 | } 84 | 85 | function getOperatorName(node: Node, sourceFile: SourceFile): string | null { 86 | if (node.kind === SyntaxKind.CallExpression) { 87 | const callExpression = node as CallExpression; 88 | return callExpression.expression.getText(sourceFile); 89 | } 90 | return null; 91 | } 92 | -------------------------------------------------------------------------------- /packages/extension/src/resources.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { ExtensionContext, Uri } from 'vscode'; 3 | import { ExtensionContext as ExtensionContextFromIoC } from './ioc/types'; 4 | 5 | export const IResourceProvider = Symbol('ResourceProvider'); 6 | 7 | export interface IResourceProvider { 8 | uriForResource(fileName: string): Uri; 9 | } 10 | 11 | @injectable() 12 | export default class DefaultResourceProvider implements IResourceProvider { 13 | constructor(@inject(ExtensionContextFromIoC) private readonly context: ExtensionContext) {} 14 | 15 | uriForResource(fileName: string): Uri { 16 | return Uri.joinPath(this.context.extensionUri, 'resources', fileName); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/extension/src/sessionManager/cdpClientAddressProvider.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import type * as vscodeApiType from 'vscode'; 3 | import { VsCodeApi } from '../ioc/types'; 4 | import { ICDPClientAddress } from '../telemetryBridge/cdpClient'; 5 | 6 | export const ICDPClientAddressProvider = Symbol('CDPClientAddressProvider'); 7 | 8 | export interface ICDPClientAddressProvider { 9 | getCDPClientAddress(debugSessionId: string): Promise; 10 | } 11 | 12 | const JS_DEBUG_REQUEST_CDP_PROXY_COMMAND = 'extension.js-debug.requestCDPProxy'; 13 | 14 | /** 15 | * Checks if the `extension.js-debug.requestCDPProxy` command provided by `js-debug` is present. If present, executes 16 | * the command for the given debug session id and returns the connection information. This information in turn can be 17 | * used to connect to `js-debug`s CDP proxy. 18 | * 19 | * @param debugSessionId 20 | * @returns 21 | */ 22 | @injectable() 23 | export default class DefaultCDPClientAddressProvider implements ICDPClientAddressProvider { 24 | constructor(@inject(VsCodeApi) private readonly vscode: typeof vscodeApiType) {} 25 | 26 | async getCDPClientAddress(debugSessionId: string): Promise { 27 | if (!(await isCDPProxyRequestAvailable(this.vscode))) { 28 | throw new Error(`Installed js-debug extension does not provide "${JS_DEBUG_REQUEST_CDP_PROXY_COMMAND}" command.`); 29 | } 30 | 31 | return await this.vscode.commands.executeCommand(JS_DEBUG_REQUEST_CDP_PROXY_COMMAND, debugSessionId); 32 | } 33 | } 34 | 35 | async function isCDPProxyRequestAvailable(vscode: typeof vscodeApiType): Promise { 36 | const allCommands = await vscode.commands.getCommands(); 37 | const hasRequestCDPProxyCommand = allCommands.find((c) => c === JS_DEBUG_REQUEST_CDP_PROXY_COMMAND); 38 | return typeof hasRequestCDPProxyCommand === 'string'; 39 | } 40 | -------------------------------------------------------------------------------- /packages/extension/src/sessionManager/session.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import Logger from '../logger'; 3 | import { logPointFixtureA } from '../operatorLogPoint/index.fixture'; 4 | import { IOperatorLogPointManager } from '../operatorLogPoint/manager'; 5 | import { ITelemetryBridge } from '../telemetryBridge'; 6 | import Session, { ISession } from './session'; 7 | 8 | describe('Session', () => { 9 | let session: ISession; 10 | let logPointManager: IOperatorLogPointManager; 11 | let telemetryBridge: ITelemetryBridge; 12 | const logPoints = [logPointFixtureA]; 13 | 14 | beforeEach(() => { 15 | logPointManager = { 16 | disable: jest.fn(), 17 | enable: jest.fn(), 18 | logPoints: [], 19 | onDidChangeLogPoints: jest.fn(), 20 | dispose: jest.fn(), 21 | logPointForIdentifier: jest.fn(), 22 | }; 23 | telemetryBridge = { 24 | attach: jest.fn(() => Promise.resolve()), 25 | disableOperatorLogPoint: jest.fn(() => Promise.resolve()), 26 | enableOperatorLogPoint: jest.fn(() => Promise.resolve()), 27 | updateOperatorLogPoints: jest.fn(() => Promise.resolve()), 28 | onRuntimeReady: jest.fn((handler) => handler('nodejs')), // immediately ready 29 | onTelemetryEvent: jest.fn(), 30 | dispose: jest.fn(), 31 | }; 32 | session = new Session(logPointManager, telemetryBridge, Logger.nullLogger()); 33 | }); 34 | 35 | describe('attach()', () => { 36 | test('attaches to the TelemetryBridge', async () => { 37 | await session.attach(); 38 | expect(telemetryBridge.attach).toBeCalled(); 39 | }); 40 | 41 | test('resolves with the RuntimeType provided by the TelemetryBridge', async () => { 42 | const runtimeType = await session.attach(); 43 | expect(runtimeType).toEqual('nodejs'); 44 | }); 45 | 46 | test('sends log points present in the LogPointManager to the TelemetryBridge', async () => { 47 | logPointManager.logPoints = logPoints; 48 | await session.attach(); 49 | expect(telemetryBridge.updateOperatorLogPoints).toBeCalledWith( 50 | logPoints.map(({ operatorIdentifier }) => operatorIdentifier) 51 | ); 52 | }); 53 | }); 54 | 55 | test('forwards changed log points from the LogPointManager to the TelemetryBridge', async () => { 56 | await session.attach(); 57 | expect(telemetryBridge.updateOperatorLogPoints).not.toBeCalledWith(logPoints); 58 | 59 | const onDidChangeLogPointsHandler = (logPointManager.onDidChangeLogPoints as jest.Mock).mock.calls[0][0]; 60 | onDidChangeLogPointsHandler(logPoints); 61 | expect(telemetryBridge.updateOperatorLogPoints).toBeCalledWith( 62 | logPoints.map(({ operatorIdentifier }) => operatorIdentifier) 63 | ); 64 | }); 65 | 66 | test.todo('forwards received telemetry events from the TelemetryBridge to TBD'); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/extension/src/sessionManager/session.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeType } from '@rxjs-debugging/runtime/out/utils/runtimeType'; 2 | import { TelemetryEvent } from '@rxjs-debugging/telemetry'; 3 | import { inject, injectable } from 'inversify'; 4 | import { Event } from 'vscode'; 5 | import { ILogger } from '../logger'; 6 | import OperatorLogPoint from '../operatorLogPoint'; 7 | import { IOperatorLogPointManager } from '../operatorLogPoint/manager'; 8 | import { ITelemetryBridge } from '../telemetryBridge'; 9 | import { IDisposable } from '../util/types'; 10 | 11 | export const ISession = Symbol('Session'); 12 | 13 | export interface ISession extends IDisposable { 14 | attach(): Promise; 15 | onTelemetryEvent: Event; 16 | } 17 | 18 | @injectable() 19 | export default class Session implements ISession { 20 | private disposables: Array = []; 21 | 22 | private attached?: Promise | undefined; 23 | private resolveAttached?: (runtimeType: RuntimeType | undefined) => void | undefined; 24 | 25 | get onTelemetryEvent(): Event { 26 | return this.telemetryBridge.onTelemetryEvent; 27 | } 28 | 29 | constructor( 30 | @inject(IOperatorLogPointManager) private readonly operatorLogPointManager: IOperatorLogPointManager, 31 | @inject(ITelemetryBridge) private readonly telemetryBridge: ITelemetryBridge, 32 | @inject(ILogger) private readonly logger: ILogger 33 | ) {} 34 | 35 | attach(): Promise { 36 | if (this.attached) { 37 | return this.attached; 38 | } 39 | 40 | this.attached = new Promise((resolve) => { 41 | this.resolveAttached = resolve; 42 | this.disposables.push(this.operatorLogPointManager.onDidChangeLogPoints(this.onDidChangeLogPoints)); 43 | this.disposables.push(this.telemetryBridge.onRuntimeReady(this.onRuntimeReady)); 44 | this.telemetryBridge.attach(); 45 | this.logger.info('Session', 'Wait for runtime to become ready'); 46 | }); 47 | return this.attached; 48 | } 49 | 50 | private onRuntimeReady = (runtimeType: RuntimeType | undefined): void => { 51 | if (runtimeType) { 52 | this.logger.info('Session', `${runtimeType} runtime ready`); 53 | } else { 54 | this.logger.warn('Session', 'Unknown runtime ready'); 55 | } 56 | 57 | if (this.resolveAttached) { 58 | this.resolveAttached(runtimeType); 59 | } else { 60 | this.logger.warn('Session', 'resolveAttached was not assigned; This should not happen.'); 61 | } 62 | 63 | this.telemetryBridge.updateOperatorLogPoints( 64 | this.operatorLogPointManager.logPoints.map(({ operatorIdentifier }) => operatorIdentifier) 65 | ); 66 | }; 67 | 68 | private onDidChangeLogPoints = (logPoints: ReadonlyArray): void => { 69 | this.telemetryBridge.updateOperatorLogPoints(logPoints.map(({ operatorIdentifier }) => operatorIdentifier)); 70 | }; 71 | 72 | dispose(): void { 73 | for (const disposable of this.disposables) { 74 | disposable.dispose(); 75 | } 76 | this.disposables = []; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/extension/src/telemetryBridge/cdpClientProvider.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import CDPClient, { ICDPClient, ICDPClientAddress } from './cdpClient'; 3 | 4 | export const ICDPClientProvider = Symbol('CDPClientProvider'); 5 | export interface ICDPClientProvider { 6 | createCDPClient(address: ICDPClientAddress): ICDPClient; 7 | } 8 | 9 | @injectable() 10 | export class DefaultCDPClientProvider implements ICDPClientProvider { 11 | createCDPClient({ host, port, path = '' }: ICDPClientAddress): ICDPClient { 12 | return new CDPClient(host, port, path); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/extension/src/util/environmentInfo.ts: -------------------------------------------------------------------------------- 1 | import type * as vscodeApi from 'vscode'; 2 | 3 | export const IEnvironmentInfo = Symbol('IEnvironmentInfo'); 4 | 5 | /** 6 | * An injectable abstraction for various vscode specific environment information. 7 | */ 8 | export interface IEnvironmentInfo { 9 | version: string; 10 | machineId: string; 11 | language: string; 12 | extensionVersion: string; 13 | } 14 | 15 | export default function createEnvironmentInfo(vscode: typeof vscodeApi): IEnvironmentInfo { 16 | return { 17 | language: vscode.env.language, 18 | machineId: vscode.env.machineId, 19 | version: vscode.version, 20 | extensionVersion: EXTENSION_VERSION, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/extension/src/util/map/difference.test.ts: -------------------------------------------------------------------------------- 1 | import difference from './difference'; 2 | 3 | describe('Util', () => { 4 | describe('Map', () => { 5 | describe('difference()', () => { 6 | test.each([ 7 | [ 8 | new Map(), 9 | new Map([ 10 | [1, '1a'], 11 | [2, '1b'], 12 | [3, '1c'], 13 | [4, '1d'], 14 | ]), 15 | new Map([ 16 | [1, '2a'], 17 | [2, '2b'], 18 | [3, '2c'], 19 | [4, '2d'], 20 | ]), 21 | ], 22 | [ 23 | new Map([ 24 | [1, '1a'], 25 | [2, '1b'], 26 | [3, '1c'], 27 | [4, '1d'], 28 | ]), 29 | new Map([ 30 | [1, '1a'], 31 | [2, '1b'], 32 | [3, '1c'], 33 | [4, '1d'], 34 | ]), 35 | new Map(), 36 | ], 37 | [ 38 | new Map([ 39 | [2, '1b'], 40 | [3, '1c'], 41 | [4, '1d'], 42 | ]), 43 | new Map([ 44 | [1, '1a'], 45 | [2, '1b'], 46 | [3, '1c'], 47 | [4, '1d'], 48 | ]), 49 | new Map([[1, '2a']]), 50 | ], 51 | ])('builds the difference %s when given %s and %s', (result, a, b) => expect(difference(a, b)).toEqual(result)); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/extension/src/util/map/difference.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Apply the difference set operation on two `Map`s. 3 | * 4 | * > "a without b" 5 | * 6 | * @param a 7 | * @param b 8 | * @returns 9 | */ 10 | export default function difference(a: Map, b: Map): Map { 11 | const difference = new Map(a); 12 | for (const [keyInB] of b) { 13 | difference.delete(keyInB); 14 | } 15 | return difference; 16 | } 17 | -------------------------------------------------------------------------------- /packages/extension/src/util/map/intersection.test.ts: -------------------------------------------------------------------------------- 1 | import difference from './intersection'; 2 | 3 | describe('Util', () => { 4 | describe('Map', () => { 5 | describe('intersection()', () => { 6 | test.each([ 7 | [ 8 | new Map([ 9 | [1, '1a'], 10 | [2, '1b'], 11 | [3, '1c'], 12 | [4, '1d'], 13 | ]), 14 | new Map([ 15 | [1, '1a'], 16 | [2, '1b'], 17 | [3, '1c'], 18 | [4, '1d'], 19 | ]), 20 | new Map([ 21 | [1, '2a'], 22 | [2, '2b'], 23 | [3, '2c'], 24 | [4, '2d'], 25 | ]), 26 | ], 27 | [ 28 | new Map(), 29 | new Map([ 30 | [1, '1a'], 31 | [2, '1b'], 32 | [3, '1c'], 33 | [4, '1d'], 34 | ]), 35 | new Map(), 36 | ], 37 | [ 38 | new Map([ 39 | [2, '1b'], 40 | [3, '1c'], 41 | [4, '1d'], 42 | ]), 43 | new Map([ 44 | [1, '1a'], 45 | [2, '1b'], 46 | [3, '1c'], 47 | [4, '1d'], 48 | ]), 49 | new Map([ 50 | [2, '2b'], 51 | [3, '2c'], 52 | [4, '2d'], 53 | ]), 54 | ], 55 | ])('builds the difference %s when given %s and %s', (result, a, b) => expect(difference(a, b)).toEqual(result)); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/extension/src/util/map/intersection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Apply the intersection set operation on two `Map`s. If two keys are present in both given `Map`'s, the value of the 3 | * first `Map`s entry is chosen. 4 | * 5 | * @param a 6 | * @param b 7 | * @returns 8 | */ 9 | export default function intersection(a: Map, b: Map): Map { 10 | const intersection = new Map(); 11 | 12 | const [lessEntries, other] = a.size < b.size ? [a, b] : [b, a]; 13 | 14 | for (const [key] of lessEntries) { 15 | if (other.has(key)) { 16 | intersection.set(key, a.get(key)); // TODO This additional lookup in a might hurt... But is it worse than iterating over the potential larger map? 17 | } 18 | } 19 | 20 | return intersection; 21 | } 22 | -------------------------------------------------------------------------------- /packages/extension/src/util/types.ts: -------------------------------------------------------------------------------- 1 | export interface IDisposable { 2 | dispose: () => void; 3 | } 4 | -------------------------------------------------------------------------------- /packages/extension/src/workspaceMonitor/detector.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { inject, injectable } from 'inversify'; 3 | import { Uri, WorkspaceFolder } from 'vscode'; 4 | import { ILogger } from '../logger'; 5 | 6 | export const IRxJSDetector = Symbol('RxJSDetector'); 7 | 8 | export interface IRxJSDetector { 9 | detect(workspace: WorkspaceFolder): Promise; 10 | } 11 | 12 | @injectable() 13 | export class RxJSDetector implements IRxJSDetector { 14 | constructor(@inject(ILogger) private readonly logger: ILogger) {} 15 | 16 | async detect(workspaceFolder: WorkspaceFolder): Promise { 17 | try { 18 | const packageJson = await readFile(Uri.joinPath(workspaceFolder.uri, 'package.json').fsPath); 19 | const hasRxJSDependency = packageJson.indexOf('"rxjs"') !== -1; 20 | this.logger.info('Detector', `RxJS detected in ${workspaceFolder.uri.fsPath}`); 21 | return hasRxJSDependency; 22 | } catch (_) { 23 | this.logger.info('Detector', `RxJS not detected in ${workspaceFolder.uri.fsPath}`); 24 | return false; 25 | } 26 | } 27 | } 28 | 29 | function readFile(path: string): Promise { 30 | return new Promise((resolve, reject) => { 31 | fs.readFile(path, 'utf8', (err, data) => { 32 | if (!err) { 33 | return resolve(data); 34 | } 35 | reject(err); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /packages/extension/src/workspaceMonitor/index.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import type * as vscodeApiType from 'vscode'; 3 | import { VsCodeApi } from '../ioc/types'; 4 | import { ILogger } from '../logger'; 5 | import { IOperatorLogPointRecommender } from '../operatorLogPoint/recommender'; 6 | import { IDisposable } from '../util/types'; 7 | 8 | export const IWorkspaceMonitor = Symbol('WorkspaceMonitor'); 9 | 10 | export type IWorkspaceMonitor = IDisposable; 11 | 12 | const DID_CHANGE_TEXT_DOCUMENT_DEBOUNCE_DELAY_MS = 500; 13 | 14 | @injectable() 15 | export default class WorkspaceMonitor implements IWorkspaceMonitor { 16 | private readonly disposables: IDisposable[] = []; 17 | 18 | constructor( 19 | @inject(VsCodeApi) readonly vscode: typeof vscodeApiType, 20 | @inject(IOperatorLogPointRecommender) private readonly operatorLogPointRecommender: IOperatorLogPointRecommender, 21 | @inject(ILogger) private readonly logger: ILogger 22 | ) { 23 | this.disposables.push( 24 | vscode.workspace.onDidOpenTextDocument(this.onDidOpenTextDocument), 25 | vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument) 26 | ); 27 | 28 | for (const textEditor of vscode.window.visibleTextEditors) { 29 | this.onDidOpenTextDocument(textEditor.document); 30 | } 31 | } 32 | 33 | private onDidOpenTextDocument = (document: vscodeApiType.TextDocument): void => { 34 | if (document.uri.scheme !== 'file') { 35 | return; 36 | } 37 | 38 | this.logger.info('WorkspaceMonitor', `Opened ${document.fileName} (${document.languageId})`); 39 | 40 | this.operatorLogPointRecommender.recommend(document); 41 | }; 42 | 43 | private onDidChangeTextDocument = debounced(({ document }: vscodeApiType.TextDocumentChangeEvent) => { 44 | if (document.uri.scheme !== 'file') { 45 | return; 46 | } 47 | 48 | this.logger.info('WorkspaceMonitor', `Changed ${document.fileName} (${document.languageId})`); 49 | 50 | this.operatorLogPointRecommender.recommend(document); 51 | }, DID_CHANGE_TEXT_DOCUMENT_DEBOUNCE_DELAY_MS); 52 | 53 | dispose(): void { 54 | for (const disposable of this.disposables) { 55 | disposable.dispose(); 56 | } 57 | } 58 | } 59 | 60 | function debounced(fn: (a: A) => void, delayMs: number): (a: A) => void { 61 | let timeout: NodeJS.Timeout | undefined; 62 | 63 | return (a) => { 64 | if (timeout) { 65 | clearTimeout(timeout); 66 | } 67 | 68 | timeout = setTimeout(() => { 69 | timeout = undefined; 70 | fn(a); 71 | }, delayMs); 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /packages/extension/src/workspaceMonitor/isSupportedDocument.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | const SUPPORTED_LANGUAGES = ['javascript', 'javascriptreact', 'typescript', 'typescriptreact']; 4 | 5 | export default function isSupportedDocument({ languageId }: vscode.TextDocument): boolean { 6 | return SUPPORTED_LANGUAGES.includes(languageId); 7 | } 8 | -------------------------------------------------------------------------------- /packages/extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "es6", 6 | "lib": ["es6"], 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "strict": true, 11 | "noImplicitReturns": true, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true, 14 | "outDir": "out", 15 | "rootDir": "src" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/runtime-nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rxjs-debugging/runtime-nodejs", 3 | "version": "1.1.1", 4 | "license": "MIT", 5 | "author": "Manuel Alabor ", 6 | "private": true, 7 | "main": "out/index.js", 8 | "types": "out/index.d.ts", 9 | "scripts": { 10 | "build": "rollup -c", 11 | "build:prod": "yarn build --configMode=production" 12 | }, 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "21.0.1", 15 | "@rollup/plugin-node-resolve": "13.0.6", 16 | "@rollup/plugin-typescript": "8.3.0", 17 | "@rxjs-debugging/runtime": "^1.1.1", 18 | "@rxjs-debugging/telemetry": "^1.1.1", 19 | "@types/jest": "27.0.3", 20 | "@types/node": "16.11.11", 21 | "@types/webpack": "5.28.0", 22 | "jest": "27.4.3", 23 | "rollup": "2.60.2", 24 | "rollup-plugin-terser": "7.0.2", 25 | "ts-jest": "27.1.0", 26 | "tslib": "2.3.1", 27 | "typescript": "4.5.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/runtime-nodejs/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonJs from '@rollup/plugin-commonjs'; 2 | import nodeResolve from '@rollup/plugin-node-resolve'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | 6 | /** 7 | * @type {import('rollup').RollupOptions} 8 | */ 9 | export default ({ configMode }) => { 10 | const doProductionBuild = configMode === 'production'; 11 | 12 | return { 13 | input: 'src/index.ts', 14 | output: { 15 | file: 'out/runtime.js', 16 | format: 'commonjs', 17 | interop: false, 18 | exports: 'auto', 19 | sourcemap: !doProductionBuild, 20 | }, 21 | plugins: [ 22 | commonJs(), 23 | nodeResolve({ 24 | preferBuiltins: true, 25 | resolveOnly: [ 26 | '@rxjs-debugging/telemetry', 27 | 28 | '@rxjs-debugging/runtime', 29 | 'stacktrace-js', 30 | 'error-stack-parser', 31 | 'stack-generator', 32 | 'stacktrace-gps', 33 | 'stackframe', 34 | 'source-map', 35 | ], 36 | }), 37 | typescript(), 38 | doProductionBuild && 39 | terser({ 40 | format: { comments: () => false }, 41 | 42 | // Apply terser with care so stack traces for source detection keeps working: 43 | compress: false, 44 | mangle: false, 45 | }), 46 | ], 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/runtime-nodejs/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import '@rxjs-debugging/runtime/out/global'; 2 | -------------------------------------------------------------------------------- /packages/runtime-nodejs/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CDP_BINDING_NAME_SEND_TELEMETRY, 3 | RUNTIME_PROGRAM_ENV_VAR, 4 | RUNTIME_TELEMETRY_BRIDGE, 5 | } from '@rxjs-debugging/runtime/out/consts'; 6 | import operatorLogPointInstrumentation from '@rxjs-debugging/runtime/out/instrumentation/operatorLogPoint'; 7 | import patchObservable from '@rxjs-debugging/runtime/out/instrumentation/operatorLogPoint/patchObservable'; 8 | import TelemetryBridge from '@rxjs-debugging/runtime/out/telemetryBridge'; 9 | import isRxJSImport from '@rxjs-debugging/runtime/out/utils/isRxJSImport'; 10 | import waitForCDPBindings from '@rxjs-debugging/runtime/out/utils/waitForCDPBindings'; 11 | import { TelemetryEvent } from '@rxjs-debugging/telemetry'; 12 | import serializeTelemetryEvent from '@rxjs-debugging/telemetry/out/serialize'; 13 | import * as Module from 'module'; 14 | import type { Subscriber as SubscriberType } from 'rxjs'; 15 | 16 | const programPath = process.env[RUNTIME_PROGRAM_ENV_VAR]; 17 | const programModule = Module.createRequire(programPath); 18 | const Subscriber = getSubscriber(programModule); 19 | const createWrapOperatorFunction = operatorLogPointInstrumentation(Subscriber); 20 | 21 | const originalRequire = Module.prototype.require; 22 | let patchedCache = null; 23 | 24 | const telemetryBridge = new TelemetryBridge(defaultSend); 25 | const wrapOperatorFunction = createWrapOperatorFunction(telemetryBridge); 26 | 27 | const patchedRequire: NodeJS.Require = function (id) { 28 | const filename = (Module as unknown as { _resolveFilename: (id: string, that: unknown) => string })._resolveFilename( 29 | id, 30 | this 31 | ); 32 | 33 | if (isRxJSImport(filename)) { 34 | if (patchedCache) { 35 | return patchedCache; 36 | } 37 | 38 | const exports = originalRequire.apply(this, [id]); 39 | patchObservable(exports.Observable, wrapOperatorFunction); 40 | patchedCache = exports; 41 | return exports; 42 | } 43 | 44 | return originalRequire.apply(this, [id]); 45 | }; 46 | patchedRequire.resolve = originalRequire.resolve; 47 | patchedRequire.cache = originalRequire.cache; 48 | patchedRequire.extensions = originalRequire.extensions; 49 | patchedRequire.main = originalRequire.main; 50 | Module.prototype.require = patchedRequire; 51 | 52 | function defaultSend(event: TelemetryEvent): void { 53 | const message = serializeTelemetryEvent(event); 54 | global[CDP_BINDING_NAME_SEND_TELEMETRY](message); // global.sendRxJsDebuggerTelemetry will be provided via CDP Runtime.addBinding eventually: 55 | } 56 | 57 | function getSubscriber( 58 | customRequire: (module: string) => { Subscriber: typeof SubscriberType } 59 | ): typeof SubscriberType { 60 | try { 61 | // Try access Subscriber via /internal first. This works for RxJS >=7.2.0. 62 | return customRequire('rxjs/internal/Subscriber').Subscriber; 63 | } catch (_) { 64 | // If the first attempt failed, fall back to a plain root import: 65 | return customRequire('rxjs').Subscriber; 66 | } 67 | } 68 | 69 | global[RUNTIME_TELEMETRY_BRIDGE] = telemetryBridge; 70 | 71 | waitForCDPBindings('nodejs'); 72 | -------------------------------------------------------------------------------- /packages/runtime-nodejs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "es6", 6 | "declaration": true, 7 | "outDir": "out", 8 | "lib": ["es6"], 9 | "sourceMap": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "strict": true, 13 | "noImplicitReturns": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "rootDir": "src" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/runtime-webpack/.gitignore: -------------------------------------------------------------------------------- 1 | *.tgz 2 | LICENSE 3 | -------------------------------------------------------------------------------- /packages/runtime-webpack/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.json 3 | rollup.config.js 4 | *.tgx 5 | docs/*.mp4 6 | docs/*.mov 7 | docs/*.gif 8 | -------------------------------------------------------------------------------- /packages/runtime-webpack/README.md: -------------------------------------------------------------------------------- 1 | # ![Archie the Debugger Owl](https://github.com/swissmanu/rxjs-debugging-for-vscode/raw/main/docs/brand/archie-small.png) @rxjs-debugging/runtime-webpack 2 | 3 | > Webpack plugin to debug RxJS-based web applications with [RxJS Debugging for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=manuelalabor.rxjs-debugging-for-vs-code). 4 | 5 | [![npm version](https://badge.fury.io/js/@rxjs-debugging%2Fruntime-webpack.svg)](https://badge.fury.io/js/@rxjs-debugging%2Fruntime-webpack) 6 | 7 | In order to debug an RxJS-based web application bundled using Webpack with [RxJS Debugging for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=manuelalabor.rxjs-debugging-for-vs-code), the `@rxjs-debugging/runtime-webpack` plugin is required. 8 | 9 | The plugin augments RxJS so the debugger can communicate with your application at runtime. This augmentation happens *only* during development, hence your production builds will stay clear of any debugging augmentation. 10 | 11 | ![runtime-webpack Demo](https://github.com/swissmanu/rxjs-debugging-for-vscode/raw/main/packages/runtime-webpack/docs/demo.gif) 12 | 13 | 14 | 15 | ## Usage 16 | 17 | 1. Install `@rxjs-debugging/runtime-webpack`: 18 | 19 | ```bash 20 | npm i -D @rxjs-debugging/runtime-webpack 21 | yarn add -D @rxjs-debugging/runtime-webpack 22 | ``` 23 | 24 | 2. Import and add the `RxJSDebuggingPlugin` to your Webpack configuration: 25 | 26 | ```javascript 27 | import RxJSDebuggingPlugin from '@rxjs-debugging/runtime-webpack'; 28 | 29 | export default { 30 | // your configuration 31 | plugins: [ 32 | // your plugins 33 | new RxJSDebuggingPlugin() // <-- Add this line 34 | ] 35 | }; 36 | ``` 37 | 38 | 3. (Re-)start Webpack and debug your web application with [RxJS Debugging for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=manuelalabor.rxjs-debugging-for-vs-code). 39 | 40 | ## Example 41 | 42 | The [Webpack Testbench](https://github.com/swissmanu/rxjs-debugging-for-vscode/tree/main/packages/testbench-webpack) demonstrates how `@rxjs-debugging/runtime-webpack` can be integrated allowing to debug a web application with [RxJS Debugging for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=manuelalabor.rxjs-debugging-for-vs-code). 43 | -------------------------------------------------------------------------------- /packages/runtime-webpack/docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/packages/runtime-webpack/docs/demo.gif -------------------------------------------------------------------------------- /packages/runtime-webpack/docs/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swissmanu/rxjs-debugging-for-vscode/e826882151e2767daa95c1f68b4ba4b1c0c2db8f/packages/runtime-webpack/docs/demo.mp4 -------------------------------------------------------------------------------- /packages/runtime-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rxjs-debugging/runtime-webpack", 3 | "description": "Webpack plugin to debug RxJS-based web applications with \"RxJS Debugging for Visual Studio Code\".", 4 | "version": "1.1.1", 5 | "license": "MIT", 6 | "author": "Manuel Alabor ", 7 | "homepage": "https://github.com/swissmanu/rxjs-debugging-for-vscode/tree/main/packages/runtime-webpack", 8 | "repository": { 9 | "url": "https://github.com/swissmanu/rxjs-debugging-for-vscode" 10 | }, 11 | "main": "out/index.js", 12 | "types": "out/index.d.ts", 13 | "scripts": { 14 | "build": "rollup -c", 15 | "build:prod": "yarn build --configMode=production", 16 | "package": "yarn pack", 17 | "publish-package": "npm publish" 18 | }, 19 | "peerDependencies": { 20 | "webpack": ">=5.60.0" 21 | }, 22 | "devDependencies": { 23 | "@rollup/plugin-commonjs": "21.0.1", 24 | "@rollup/plugin-node-resolve": "13.0.6", 25 | "@rollup/plugin-typescript": "8.3.0", 26 | "@rxjs-debugging/runtime": "^1.1.1", 27 | "@rxjs-debugging/telemetry": "^1.1.1", 28 | "@types/jest": "27.0.3", 29 | "@types/node": "16.11.11", 30 | "@types/webpack": "5.28.0", 31 | "jest": "27.4.3", 32 | "rollup": "2.60.2", 33 | "rollup-plugin-copy": "3.4.0", 34 | "rollup-plugin-terser": "7.0.2", 35 | "ts-jest": "27.1.0", 36 | "tslib": "2.3.1", 37 | "typescript": "4.5.2", 38 | "webpack": "5.64.4" 39 | }, 40 | "publishConfig": { 41 | "access": "public", 42 | "registry": "https://registry.npmjs.org/" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/runtime-webpack/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import nodeResolve from '@rollup/plugin-node-resolve'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | import copy from 'rollup-plugin-copy'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | 7 | /** 8 | * @type {import('rollup').RollupOptions} 9 | */ 10 | export default ({ configMode }) => { 11 | const doProductionBuild = configMode === 'production'; 12 | 13 | return { 14 | input: ['src/index.ts', 'src/loader.ts', 'src/instrumentation.ts'], 15 | output: { 16 | dir: 'out', 17 | format: 'commonjs', 18 | interop: false, 19 | exports: 'auto', 20 | sourcemap: true, 21 | }, 22 | plugins: [ 23 | commonjs(), 24 | nodeResolve({ 25 | preferBuiltins: true, 26 | resolveOnly: [ 27 | '@rxjs-debugging/telemetry', 28 | 29 | '@rxjs-debugging/runtime', 30 | 'stacktrace-js', 31 | 'error-stack-parser', 32 | 'stack-generator', 33 | 'stacktrace-gps', 34 | 'stackframe', 35 | 'source-map', 36 | ], 37 | }), 38 | typescript(), 39 | doProductionBuild && 40 | terser({ 41 | format: { comments: () => false }, 42 | 43 | // Apply terser with care so stack traces for source detection keeps working: 44 | compress: false, 45 | mangle: false, 46 | }), 47 | copy({ 48 | overwrite: true, 49 | targets: [{ src: '../../LICENSE', dest: './' }], 50 | }), 51 | ], 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /packages/runtime-webpack/src/RxJSDebuggingPlugin.ts: -------------------------------------------------------------------------------- 1 | import isRxJSImport from '@rxjs-debugging/runtime/out/utils/isRxJSImport'; 2 | import * as path from 'path'; 3 | import type { Compiler } from 'webpack'; 4 | import { NormalModule } from 'webpack'; 5 | 6 | const PLUGIN_NAME = 'RxJSDebuggingPlugin'; 7 | const loaderPath = require.resolve('./loader.js'); 8 | const here = path.dirname(loaderPath); 9 | 10 | export default class RxJSDebuggingPlugin { 11 | apply(compiler: Compiler): void { 12 | if (compiler.options.mode === 'production') { 13 | return; 14 | } 15 | 16 | compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { 17 | NormalModule.getCompilationHooks(compilation).beforeLoaders.tap(PLUGIN_NAME, (loaders, normalModule) => { 18 | if (normalModule.resource.startsWith(here)) { 19 | // Never add loader to ourselves: 20 | return; 21 | } 22 | 23 | const { userRequest = '' } = normalModule; 24 | if (isRxJSImport(userRequest)) { 25 | loaders.push({ 26 | loader: loaderPath, 27 | options: {}, 28 | ident: null, 29 | type: null, 30 | }); 31 | } 32 | }); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/runtime-webpack/src/index.ts: -------------------------------------------------------------------------------- 1 | import RxJSDebuggingPlugin from './RxJSDebuggingPlugin'; 2 | export default RxJSDebuggingPlugin; 3 | -------------------------------------------------------------------------------- /packages/runtime-webpack/src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | import { TelemetryEvent } from '@rxjs-debugging//telemetry'; 2 | import { CDP_BINDING_NAME_SEND_TELEMETRY, RUNTIME_TELEMETRY_BRIDGE } from '@rxjs-debugging/runtime/out/consts'; 3 | import operatorLogPointInstrumentation from '@rxjs-debugging/runtime/out/instrumentation/operatorLogPoint'; 4 | import patchObservable from '@rxjs-debugging/runtime/out/instrumentation/operatorLogPoint/patchObservable'; 5 | import waitForCDPBindings from '@rxjs-debugging/runtime/out/utils/waitForCDPBindings'; 6 | import serializeTelemetryEvent from '@rxjs-debugging/telemetry/out/serialize'; 7 | import { Subscriber } from 'rxjs/internal/Subscriber'; 8 | import TelemetryBridge from './webpackTelemetryBridge'; 9 | 10 | const telemetryBridge = new TelemetryBridge((event: TelemetryEvent) => { 11 | const message = serializeTelemetryEvent(event); 12 | global[CDP_BINDING_NAME_SEND_TELEMETRY](message); // global.sendRxJsDebuggerTelemetry will be provided via CDP Runtime.addBinding eventually: 13 | }); 14 | global[RUNTIME_TELEMETRY_BRIDGE] = telemetryBridge; 15 | 16 | const createWrapOperatorFunction = operatorLogPointInstrumentation(Subscriber); 17 | const wrapOperatorFunction = createWrapOperatorFunction(telemetryBridge); 18 | 19 | export default function instrumentation(observable: Parameters[0]): void { 20 | patchObservable(observable, wrapOperatorFunction); 21 | } 22 | 23 | waitForCDPBindings('webpack'); 24 | -------------------------------------------------------------------------------- /packages/runtime-webpack/src/loader.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderContext } from 'webpack'; 2 | 3 | export default function patch(this: LoaderContext, source: string): void { 4 | this.callback( 5 | null, 6 | ` 7 | ${source} 8 | import webpackInstrumentation from "${require.resolve('./instrumentation.js')}"; 9 | webpackInstrumentation(Observable); 10 | ` 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/runtime-webpack/src/webpackTelemetryBridge.ts: -------------------------------------------------------------------------------- 1 | import TelemetryBridge from '@rxjs-debugging/runtime/out/telemetryBridge'; 2 | import { TelemetryEvent } from '@rxjs-debugging/telemetry'; 3 | import matchTelemetryEvent from '@rxjs-debugging/telemetry/out/match'; 4 | import { IOperatorIdentifier } from '@rxjs-debugging/telemetry/out/operatorIdentifier'; 5 | 6 | const WEBPACK_PREFIX = 'webpack://'; 7 | 8 | export default class WebpackTelemetryBridge extends TelemetryBridge { 9 | /** 10 | * @inheritdoc 11 | */ 12 | forward(telemetryEvent: TelemetryEvent): void { 13 | const eventToForward = matchTelemetryEvent({ 14 | OperatorLogPoint: (o) => { 15 | const operatorIdentifier = this.getEnabledOperatorIdentifier(o.operator); 16 | if (operatorIdentifier) { 17 | return { ...telemetryEvent, operator: operatorIdentifier }; 18 | } 19 | return; 20 | }, 21 | })(telemetryEvent); 22 | 23 | if (eventToForward) { 24 | this.send(eventToForward); 25 | } 26 | } 27 | 28 | protected getEnabledOperatorIdentifier(operatorIdentifier: IOperatorIdentifier): IOperatorIdentifier | undefined { 29 | if (this.enabledOperatorLogPoints.size === 0) { 30 | return undefined; 31 | } 32 | 33 | // TODO Highly inefficient. Can we improve this by building the index better upfront? 34 | const withoutPrefix = operatorIdentifier.fileName.substr(WEBPACK_PREFIX.length); 35 | const relativeFileName = withoutPrefix.substr(withoutPrefix.indexOf('/') + 1); 36 | 37 | for (const o of this.enabledOperatorLogPoints.values()) { 38 | if ( 39 | o.fileName.endsWith(relativeFileName) && 40 | o.line === operatorIdentifier.line && 41 | o.character - 1 === operatorIdentifier.character && 42 | o.operatorIndex === operatorIdentifier.operatorIndex 43 | ) { 44 | return o; 45 | } 46 | } 47 | 48 | return undefined; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/runtime-webpack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "es6", 6 | "declaration": true, 7 | "outDir": "out", 8 | "lib": ["es6"], 9 | "sourceMap": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "strict": true, 13 | "noImplicitReturns": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "rootDir": "src" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/runtime/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /packages/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rxjs-debugging/runtime", 3 | "version": "1.1.1", 4 | "license": "MIT", 5 | "author": "Manuel Alabor ", 6 | "private": true, 7 | "main": "out/index.js", 8 | "types": "out/index.d.ts", 9 | "scripts": { 10 | "build": "tsc && cp src/global.d.ts out/" 11 | }, 12 | "dependencies": { 13 | "@rxjs-debugging/telemetry": "^1.1.1", 14 | "rxjs": "6.6.7", 15 | "source-map": "0.7.3", 16 | "spicery": "2.1.2", 17 | "stacktrace-js": "2.0.2" 18 | }, 19 | "devDependencies": { 20 | "@types/jest": "27.0.3", 21 | "@types/node": "16.11.11", 22 | "jest": "27.4.3", 23 | "ts-jest": "27.1.0", 24 | "tslib": "2.3.1", 25 | "typescript": "4.5.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/runtime/src/consts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The name of a function injected via CDP bindings to a runtime environment. To be called once the debugger runtime is 3 | * ready for debugging. 4 | */ 5 | export const CDP_BINDING_NAME_RUNTIME_READY = 'rxJsDebuggerRuntimeReady'; 6 | 7 | /** 8 | * The name of a function injected via CDP bindings to a runtime environment. Call it to send a `TelemetryEvent` to the 9 | * extensions debugger. 10 | */ 11 | export const CDP_BINDING_NAME_SEND_TELEMETRY = 'sendRxJsDebuggerTelemetry'; 12 | 13 | export const RUNTIME_TELEMETRY_BRIDGE = 'rxJsDebuggerTelemetryBridge'; 14 | export const RUNTIME_PROGRAM_ENV_VAR = 'RXJS_DEBUGGER_PROGRAM'; 15 | -------------------------------------------------------------------------------- /packages/runtime/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | import type TelemetryBridge from './telemetryBridge'; 3 | import { RuntimeType } from './utils/runtimeType'; 4 | 5 | // Variable names MUST be kept in sync with ./const.ts! 6 | declare global { 7 | // CDP_BINDING_NAME_RUNTIME_READY: 8 | var rxJsDebuggerRuntimeReady: (x: RuntimeType) => void | undefined; 9 | 10 | //CDP_BINDING_NAME_SEND_TELEMETRY 11 | var sendRxJsDebuggerTelemetry: (msg: string) => void | undefined; 12 | 13 | // RUNTIME_TELEMETRY_BRIDGE 14 | var rxJsDebuggerTelemetryBridge: TelemetryBridge | undefined; 15 | 16 | //RUNTIME_PROGRAM_ENV_VAR 17 | var RXJS_DEBUGGER_PROGRAM: string | undefined; 18 | } 19 | -------------------------------------------------------------------------------- /packages/runtime/src/instrumentation/operatorLogPoint/createOperatorLogPointTelemetrySubscriber.ts: -------------------------------------------------------------------------------- 1 | import { TelemetryEventType } from '@rxjs-debugging/telemetry'; 2 | import { ObservableEventType } from '@rxjs-debugging/telemetry/out/observableEvent'; 3 | import { IOperatorIdentifier } from '@rxjs-debugging/telemetry/out/operatorIdentifier'; 4 | import type { PartialObserver, Subscriber as RxJSSubscriber } from 'rxjs'; 5 | import * as StackTrace from 'stacktrace-js'; 6 | import TelemetryBridge from '../../telemetryBridge'; 7 | import operatorIdentifierFromStackFrame from './operatorIdentifierFromStackFrame'; 8 | 9 | type OperatorLogPointTelemetryEventSubscriber = new ( 10 | telemetryBridge: TelemetryBridge, 11 | destination: RxJSSubscriber, 12 | sourceLocation: Promise, 13 | operatorIndex: number 14 | ) => PartialObserver; 15 | 16 | export default function createOperatorLogPointTelemetryEventSubscriber( 17 | Subscriber: typeof RxJSSubscriber 18 | ): OperatorLogPointTelemetryEventSubscriber { 19 | return class OperatorLogPointTelemetryEventSubscriber extends Subscriber { 20 | private operatorIdentifier: Promise; 21 | 22 | constructor( 23 | private readonly telemetryBridge: TelemetryBridge, 24 | destination: RxJSSubscriber, 25 | sourceLocation: Promise, 26 | operatorIndex: number 27 | ) { 28 | super(destination); 29 | 30 | this.operatorIdentifier = sourceLocation.then(([topStackFrame]) => 31 | operatorIdentifierFromStackFrame(topStackFrame, operatorIndex) 32 | ); 33 | 34 | this.operatorIdentifier.then((operator) => { 35 | telemetryBridge.forward({ 36 | type: TelemetryEventType.OperatorLogPoint, 37 | observableEvent: ObservableEventType.Subscribe, 38 | data: undefined, 39 | operator, 40 | }); 41 | }); 42 | } 43 | 44 | _next(value: T): void { 45 | this.operatorIdentifier.then((operator) => 46 | this.telemetryBridge.forward({ 47 | type: TelemetryEventType.OperatorLogPoint, 48 | observableEvent: ObservableEventType.Next, 49 | data: { value: JSON.stringify(value) }, 50 | operator, 51 | }) 52 | ); 53 | super._next(value); 54 | } 55 | 56 | _complete(): void { 57 | this.operatorIdentifier.then((operator) => 58 | this.telemetryBridge.forward({ 59 | type: TelemetryEventType.OperatorLogPoint, 60 | observableEvent: ObservableEventType.Completed, 61 | data: undefined, 62 | operator, 63 | }) 64 | ); 65 | super._complete(); 66 | this.unsubscribe(); // ensure tear down 67 | } 68 | 69 | _error(err: never): void { 70 | this.operatorIdentifier.then((operator) => 71 | this.telemetryBridge.forward({ 72 | type: TelemetryEventType.OperatorLogPoint, 73 | observableEvent: ObservableEventType.Error, 74 | data: { error: err ? JSON.stringify(err) : '' }, 75 | operator, 76 | }) 77 | ); 78 | super._error(err); 79 | this.unsubscribe(); // ensure tear down 80 | } 81 | 82 | unsubscribe(): void { 83 | this.operatorIdentifier.then((operator) => 84 | this.telemetryBridge.forward({ 85 | type: TelemetryEventType.OperatorLogPoint, 86 | observableEvent: ObservableEventType.Unsubscribe, 87 | data: undefined, 88 | operator, 89 | }) 90 | ); 91 | super.unsubscribe(); 92 | } 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /packages/runtime/src/instrumentation/operatorLogPoint/index.ts: -------------------------------------------------------------------------------- 1 | import type { OperatorFunction, Subscriber as RxJSSubscriber, Observable } from 'rxjs'; 2 | import StackTrace from 'stacktrace-js'; 3 | import TelemetryBridge from '../../telemetryBridge'; 4 | import createOperatorLogPointTelemetryEventSubscriber from './createOperatorLogPointTelemetrySubscriber'; 5 | import operate from './operate'; 6 | import { ORIGINAL_PIPE_PROPERTY_NAME } from './patchObservable'; 7 | 8 | /** 9 | * Wraps an `OperatorFunction` so that it produces `OperatorLogPointTelemetryEvent`s. 10 | */ 11 | export type WrapOperatorFn = ( 12 | telemetryBridge: TelemetryBridge 13 | ) => (operator: OperatorFunction, operatorIndex: number) => OperatorFunction; 14 | 15 | /** 16 | * Creates a `WrapOperatorFn` using the `Subscriber` of a specific RxJS version provided as parameter. 17 | * 18 | * @param Subscriber 19 | * @returns 20 | */ 21 | export default function (Subscriber: typeof RxJSSubscriber): WrapOperatorFn { 22 | const OperatorLogPointTelemetryEventSubscriber = createOperatorLogPointTelemetryEventSubscriber(Subscriber); 23 | 24 | return ( 25 | telemetryBridge: TelemetryBridge 26 | ): ((operator: OperatorFunction, operatorIndex: number) => OperatorFunction) => { 27 | return (operator, operatorIndex) => { 28 | const sourceLocation = StackTrace.get().then((sf) => { 29 | const sanitized = sf.slice(3); 30 | return sanitized; 31 | }); 32 | 33 | return operate((source, subscriber) => { 34 | if (hasOriginalPipe(source)) { 35 | const operated = source[ORIGINAL_PIPE_PROPERTY_NAME](operator); 36 | operated.subscribe( 37 | new OperatorLogPointTelemetryEventSubscriber(telemetryBridge, subscriber, sourceLocation, operatorIndex) 38 | ); 39 | } 40 | }); 41 | }; 42 | }; 43 | } 44 | 45 | function hasOriginalPipe( 46 | o: Observable & { [ORIGINAL_PIPE_PROPERTY_NAME]?: typeof Observable.prototype['pipe'] } 47 | ): o is Observable & { [ORIGINAL_PIPE_PROPERTY_NAME]: typeof Observable.prototype['pipe'] } { 48 | return typeof o[ORIGINAL_PIPE_PROPERTY_NAME] === 'function'; 49 | } 50 | -------------------------------------------------------------------------------- /packages/runtime/src/instrumentation/operatorLogPoint/operate.test.ts: -------------------------------------------------------------------------------- 1 | describe('runtime', () => { 2 | describe('operate()', () => { 3 | test.todo(''); 4 | // const operator: jest.Mock>, Parameters>> = jest.fn((source) => source); 5 | // const source = new Subject(); 6 | // const operated = operate() 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/runtime/src/instrumentation/operatorLogPoint/operate.ts: -------------------------------------------------------------------------------- 1 | import type { Observable, OperatorFunction, Subscriber } from 'rxjs'; 2 | 3 | export default function operate( 4 | init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void 5 | ): OperatorFunction { 6 | return (source: Observable) => { 7 | if (hasLift(source)) { 8 | return source.lift(function (this: Subscriber, liftedSource: Observable) { 9 | try { 10 | return init(liftedSource, this); 11 | } catch (err) { 12 | this.error(err); 13 | } 14 | }); 15 | } 16 | throw new TypeError('Unable to lift unknown Observable type'); 17 | }; 18 | } 19 | 20 | function hasLift(source: { 21 | lift: InstanceType['lift']; 22 | }): source is { lift: InstanceType['lift'] } { 23 | return typeof source?.lift === 'function'; 24 | } 25 | -------------------------------------------------------------------------------- /packages/runtime/src/instrumentation/operatorLogPoint/operatorIdentifierFromStackFrame.test.ts: -------------------------------------------------------------------------------- 1 | import StackTrace from 'stacktrace-js'; 2 | import operatorIdentifierFromStackFrame from './operatorIdentifierFromStackFrame'; 3 | 4 | describe('Runtime', () => { 5 | describe('Operator Log Point Instrumentation', () => { 6 | describe('operatorIdentifierFromStackFrame()', () => { 7 | const lineNumber = 42; 8 | const columnNumber = 43; 9 | const fileName = 'some/file.js'; 10 | const operatorIndex = 44; 11 | 12 | test('returns an OperatorIdentifier based on given StackFrame and operator index', () => { 13 | expect( 14 | operatorIdentifierFromStackFrame( 15 | { 16 | lineNumber, 17 | fileName, 18 | columnNumber, 19 | } as StackTrace.StackFrame, 20 | operatorIndex 21 | ) 22 | ).toEqual({ 23 | character: columnNumber, 24 | line: lineNumber, 25 | fileName, 26 | operatorIndex, 27 | }); 28 | }); 29 | 30 | test('returns an OperatorIdentifier with defaulted character, fileName and line if unavailable', () => { 31 | expect(operatorIdentifierFromStackFrame({} as StackTrace.StackFrame, operatorIndex)).toEqual({ 32 | character: -1, 33 | line: -1, 34 | fileName: '', 35 | operatorIndex, 36 | }); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/runtime/src/instrumentation/operatorLogPoint/operatorIdentifierFromStackFrame.ts: -------------------------------------------------------------------------------- 1 | import StackTrace from 'stacktrace-js'; 2 | import { IOperatorIdentifier } from '@rxjs-debugging/telemetry/out/operatorIdentifier'; 3 | 4 | export default function operatorIdentifierFromStackFrame( 5 | stackFrame: StackTrace.StackFrame, 6 | operatorIndex: number 7 | ): IOperatorIdentifier { 8 | return { 9 | character: stackFrame.columnNumber || -1, 10 | fileName: stackFrame.fileName || '', 11 | line: stackFrame.lineNumber || -1, 12 | operatorIndex, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/runtime/src/instrumentation/operatorLogPoint/patchObservable.test.ts: -------------------------------------------------------------------------------- 1 | import patchObservable, { ORIGINAL_PIPE_PROPERTY_NAME } from './patchObservable'; 2 | 3 | describe('runtime', () => { 4 | describe('patchObservable()', () => { 5 | let wrap: jest.Mock; 6 | 7 | beforeEach(() => { 8 | wrap = jest.fn((x) => x); 9 | }); 10 | 11 | test(`keeps the original pipe function of give Observable as ${ORIGINAL_PIPE_PROPERTY_NAME}`, () => { 12 | class FakeObservable { 13 | pipe() {} // eslint-disable-line @typescript-eslint/no-empty-function 14 | [ORIGINAL_PIPE_PROPERTY_NAME]: never; 15 | } 16 | const originalPipe = FakeObservable.prototype.pipe; 17 | 18 | patchObservable(FakeObservable as never, wrap); 19 | 20 | expect(FakeObservable.prototype[ORIGINAL_PIPE_PROPERTY_NAME]).toBe(originalPipe); 21 | }); 22 | 23 | describe('overwrites the original pipe function', () => { 24 | test('so every given operator is wrapped using the wrapOperatorFunction', () => { 25 | class FakeObservable { 26 | pipe(a: string, b: string): string { 27 | return `${a}${b}`; 28 | } 29 | } 30 | 31 | patchObservable(FakeObservable as never, wrap); 32 | 33 | const observable = new FakeObservable(); 34 | const operatorA = () => 'a'; 35 | const operatorB = () => 'b'; 36 | observable.pipe(operatorA(), operatorB()); 37 | 38 | expect(wrap).toBeCalledTimes(2); 39 | expect(wrap).toHaveBeenNthCalledWith(1, 'a', 0); 40 | expect(wrap).toHaveBeenNthCalledWith(2, 'b', 1); 41 | }); 42 | 43 | test('and calls the original pipe using the wrapped operators', () => { 44 | const originalPipeMock = jest.fn((...parameters: unknown[]) => parameters.join()); 45 | 46 | class FakeObservable { 47 | pipe(...parameters: unknown[]): unknown { 48 | return originalPipeMock(...parameters); 49 | } 50 | } 51 | 52 | patchObservable(FakeObservable as never, wrap); 53 | 54 | const observable = new FakeObservable(); 55 | const operatorA = () => 'a'; 56 | const operatorB = () => 'b'; 57 | const result = observable.pipe(operatorA(), operatorB()); 58 | 59 | expect(originalPipeMock).toBeCalledTimes(1); 60 | expect(originalPipeMock).toBeCalledWith('a', 'b'); 61 | expect(result).toEqual('a,b'); 62 | }); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/runtime/src/instrumentation/operatorLogPoint/patchObservable.ts: -------------------------------------------------------------------------------- 1 | import type { Observable as RxJSObservable, OperatorFunction } from 'rxjs'; 2 | import { WrapOperatorFn } from '.'; 3 | 4 | export const ORIGINAL_PIPE_PROPERTY_NAME = '__RxJSDebugger_originalPipe'; 5 | 6 | /** 7 | * Patches the `pipe` function in the prototype of the RxJS `Observable`. The original `pipe` function well be kept as 8 | * `__RxJSDebugger_originalPipe`. 9 | * 10 | * ## Caution! 11 | * This function is NOT pure! It modifies the given `Observable` parameter in-place. 12 | * 13 | * @param Observable 14 | * @param wrapOperatorFunction 15 | * @returns 16 | */ 17 | export default function patchObservable( 18 | Observable: RxJSObservable & { 19 | prototype: RxJSObservable & { [ORIGINAL_PIPE_PROPERTY_NAME]?: RxJSObservable['pipe'] }; 20 | }, 21 | wrapOperatorFunction: ReturnType 22 | ): void { 23 | const origPipe = Observable.prototype.pipe; 24 | Observable.prototype[ORIGINAL_PIPE_PROPERTY_NAME] = origPipe; 25 | Observable.prototype.pipe = function (...operators: OperatorFunction[]) { 26 | return origPipe.apply(this, operators.map((o, i) => wrapOperatorFunction(o, i)) as never); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/runtime/src/telemetryBridge.test.ts: -------------------------------------------------------------------------------- 1 | import { OperatorLogPointTelemetryEvent, TelemetryEventType } from '@rxjs-debugging/telemetry'; 2 | import { ObservableEventType } from '@rxjs-debugging/telemetry/out/observableEvent'; 3 | import TelemetryBridge from './telemetryBridge'; 4 | 5 | const defaultFileName = 'foo/bar.ts'; 6 | const defaultLine = 42; 7 | const defaultCharacter = 43; 8 | const defaultOperatorIndex = 44; 9 | 10 | function createEvent({ 11 | fileName = defaultFileName, 12 | line = defaultLine, 13 | character = defaultCharacter, 14 | operatorIndex = defaultOperatorIndex, 15 | }: { 16 | fileName?: string; 17 | line?: number; 18 | character?: number; 19 | operatorIndex?: number; 20 | } = {}): OperatorLogPointTelemetryEvent { 21 | return { 22 | type: TelemetryEventType.OperatorLogPoint, 23 | data: undefined, 24 | observableEvent: ObservableEventType.Subscribe, 25 | operator: { 26 | character, 27 | fileName, 28 | line, 29 | operatorIndex, 30 | }, 31 | }; 32 | } 33 | 34 | describe('Runtime', () => { 35 | describe('TelemetryBridge', () => { 36 | const send = jest.fn(); 37 | let telemetryBridge: TelemetryBridge; 38 | 39 | beforeEach(() => { 40 | send.mockClear(); 41 | telemetryBridge = new TelemetryBridge(send); 42 | }); 43 | 44 | describe('forward()', () => { 45 | test('does not send TelemetryEvent for a source which is not enabled', () => { 46 | telemetryBridge.forward(createEvent()); 47 | expect(send).not.toBeCalled(); 48 | }); 49 | 50 | test('sends TelemetryEvent for a source which was enabled using enable()', () => { 51 | const enabledSource = createEvent(); 52 | const anotherFile = createEvent({ fileName: 'another.ts' }); 53 | const anotherLine = createEvent({ line: 0 }); 54 | const anotherCharacter = createEvent({ character: 0 }); 55 | 56 | telemetryBridge.enableOperatorLogPoint(enabledSource.operator); 57 | 58 | telemetryBridge.forward(enabledSource); 59 | telemetryBridge.forward(anotherFile); 60 | telemetryBridge.forward(anotherLine); 61 | telemetryBridge.forward(anotherCharacter); 62 | 63 | expect(send).toHaveBeenCalledTimes(1); 64 | expect(send).toHaveBeenCalledWith(enabledSource); 65 | }); 66 | 67 | test('sends TelemetryEvent for a source which was enabled using update()', () => { 68 | const enabledEvent = createEvent(); 69 | const disabledEvent = createEvent({ 70 | fileName: 'disabled.ts', 71 | line: 100, 72 | character: 1, 73 | }); 74 | 75 | telemetryBridge.enableOperatorLogPoint(disabledEvent.operator); 76 | telemetryBridge.updateOperatorLogPoints([enabledEvent.operator]); // Overwrite previously enabled source 77 | 78 | telemetryBridge.forward(enabledEvent); 79 | telemetryBridge.forward(disabledEvent); 80 | 81 | expect(send).toHaveBeenCalledTimes(1); 82 | expect(send).toHaveBeenCalledWith(enabledEvent); 83 | }); 84 | 85 | test('does not send TelemetryEvent for a source which got disabled again', () => { 86 | const event = createEvent(); 87 | telemetryBridge.enableOperatorLogPoint(event.operator); 88 | telemetryBridge.disableOperatorLogPoint(event.operator); 89 | 90 | telemetryBridge.forward(event); 91 | 92 | expect(send).not.toBeCalled(); 93 | }); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /packages/runtime/src/telemetryBridge.ts: -------------------------------------------------------------------------------- 1 | import { TelemetryEvent } from '@rxjs-debugging/telemetry'; 2 | import matchTelemetryEvent from '@rxjs-debugging/telemetry/out/match'; 3 | import { IOperatorIdentifier } from '@rxjs-debugging/telemetry/out/operatorIdentifier'; 4 | import operatorIdentifierToString from '@rxjs-debugging/telemetry/out/operatorIdentifier/toString'; 5 | 6 | /** 7 | * The `TelemetryBridge` manages a `Map` of `IOperatorIdentifier`s. Using a given `send` function, `forward` can send 8 | * `TelemetryEvent`s to the vscode extension. 9 | */ 10 | export default class TelemetryBridge { 11 | protected enabledOperatorLogPoints: Map = new Map(); 12 | 13 | /** 14 | * @param send A function to send a TelemetryEvent to the extension 15 | */ 16 | constructor(protected readonly send: (event: TelemetryEvent) => void) {} 17 | 18 | /** 19 | * Adds an `IOperatorIdentifier` to enable an operator log point. This function is called via CDP from the extension. 20 | * 21 | * @param operatorIdentifier 22 | */ 23 | enableOperatorLogPoint(operatorIdentifier: IOperatorIdentifier): void { 24 | this.enabledOperatorLogPoints.set(operatorIdentifierToString(operatorIdentifier), operatorIdentifier); 25 | } 26 | 27 | /** 28 | * Removes an `IOperatorIdentifier` from the enabled operator log points. This function is called via CDP from the 29 | * extension. 30 | * 31 | * @param operatorIdentifier 32 | */ 33 | disableOperatorLogPoint(operatorIdentifier: IOperatorIdentifier): void { 34 | this.enabledOperatorLogPoints.delete(operatorIdentifierToString(operatorIdentifier)); 35 | } 36 | 37 | /** 38 | * Replaces all `IOperatorIdentifier`s with a list of new ones. This function is called via CDP from the extension. 39 | * 40 | * @param operatorIdentifiers 41 | */ 42 | updateOperatorLogPoints(operatorIdentifiers: ReadonlyArray): void { 43 | this.enabledOperatorLogPoints = new Map(operatorIdentifiers.map((s) => [operatorIdentifierToString(s), s])); 44 | } 45 | 46 | /** 47 | * Forward given `TelemetryEvent` if its source is currently enabled. 48 | * 49 | * @param telemetryEvent 50 | */ 51 | forward(telemetryEvent: TelemetryEvent): void { 52 | const isEnabled = matchTelemetryEvent({ 53 | OperatorLogPoint: (o) => !!this.getEnabledOperatorIdentifier(o.operator), 54 | })(telemetryEvent); 55 | 56 | if (isEnabled) { 57 | this.send(telemetryEvent); 58 | } 59 | } 60 | 61 | /** 62 | * Tries to return an `IOperatorIdentifier` from `enabledOperatorLogPoints`. If no matching entry is present, 63 | * `undefined` is returned instead. 64 | * 65 | * @param operatorIdentifier 66 | * @returns 67 | */ 68 | protected getEnabledOperatorIdentifier(operatorIdentifier: IOperatorIdentifier): IOperatorIdentifier | undefined { 69 | return this.enabledOperatorLogPoints.get(operatorIdentifierToString(operatorIdentifier)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/runtime/src/utils/isRxJSImport.test.ts: -------------------------------------------------------------------------------- 1 | import isRxJSImport from './isRxJSImport'; 2 | 3 | describe('Runtime', () => { 4 | describe('isRxJSImport()', () => { 5 | test.each([ 6 | [true, '/node_modules/rxjs/dist/esm5/internal/Observable.js'], 7 | [true, '/node_modules/rxjs/dist/esm5/internal/Observable'], 8 | [true, '/node_modules/rxjs/esm5/internal/Observable.js'], 9 | [true, '/node_modules/rxjs/esm5/internal/Observable'], 10 | [true, '/node_modules/rxjs/_esm5/internal/Observable.js'], 11 | [true, '/node_modules/rxjs/_esm5/internal/Observable'], 12 | [true, '/node_modules/rxjs/_esm2015/internal/Observable.js'], 13 | [true, '/node_modules/rxjs/_esm2015/internal/Observable'], 14 | [true, '/node_modules/rxjs/internal/Observable.js'], 15 | [true, '/node_modules/rxjs/internal/Observable'], 16 | [true, 'rxjs/internal/Observable'], 17 | [true, 'C:\\projects\\rxjsdebugger\\rxjs\\internal\\Observable.js'], 18 | [false, 'rxjs'], 19 | [false, 'rxjs/Observable'], 20 | [false, 'Observable'], 21 | [false, ''], 22 | ])('returns %s for %s', (expected, path) => { 23 | expect(isRxJSImport(path)).toBe(expected); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/runtime/src/utils/isRxJSImport.ts: -------------------------------------------------------------------------------- 1 | const OBSERVABLE_MODULE_REGEX = 2 | /rxjs(\/|\\)(dist(\/|\\)|esm(\/|\\)|esm5(\/|\\)|_esm5(\/|\\)|_esm2015(\/|\\)|cjs(\/|\\))*internal(\/|\\)Observable(\.js)?$/; 3 | 4 | /** 5 | * Tests if a given path is leads to RxJS' `Observable.js` file. 6 | * 7 | * @param path 8 | * @returns 9 | */ 10 | export default function isRxJSImport(path: string): boolean { 11 | const match = OBSERVABLE_MODULE_REGEX.exec(path); 12 | return match !== null; 13 | } 14 | -------------------------------------------------------------------------------- /packages/runtime/src/utils/runtimeType.ts: -------------------------------------------------------------------------------- 1 | import { ParseFn, aString, ParserError } from 'spicery'; 2 | export type RuntimeType = 'webpack' | 'nodejs'; 3 | 4 | export const parseRuntimeType: ParseFn = (x) => { 5 | const s = aString(x); 6 | switch (s) { 7 | case 'nodejs': 8 | case 'webpack': 9 | return s; 10 | default: 11 | throw new ParserError('RuntimeType', s); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /packages/runtime/src/utils/waitForCDPBindings.ts: -------------------------------------------------------------------------------- 1 | import { CDP_BINDING_NAME_RUNTIME_READY, CDP_BINDING_NAME_SEND_TELEMETRY } from '../consts'; 2 | import { RuntimeType } from './runtimeType'; 3 | 4 | /** 5 | * Wait for CDP bindings to be present, then signal ready to the vscode extension. If bindings are unavailable, retry 6 | * until 10 retries reached. 7 | */ 8 | export default function waitForCDPBindings(runtimeType: RuntimeType, numberOfTries = 0): void { 9 | if (numberOfTries >= 10) { 10 | throw new Error('Bindings still not available after 10 tries. Abort.'); 11 | } 12 | 13 | if ( 14 | typeof global[CDP_BINDING_NAME_RUNTIME_READY] === 'function' && 15 | typeof global[CDP_BINDING_NAME_SEND_TELEMETRY] === 'function' 16 | ) { 17 | global[CDP_BINDING_NAME_RUNTIME_READY](runtimeType); 18 | } else { 19 | setTimeout(() => waitForCDPBindings(runtimeType, numberOfTries + 1), 500); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/runtime/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "ES6", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "outDir": "out", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "noImplicitReturns": true, 13 | "rootDir": "src" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/telemetry/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /packages/telemetry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rxjs-debugging/telemetry", 3 | "version": "1.1.1", 4 | "license": "MIT", 5 | "author": "Manuel Alabor ", 6 | "private": true, 7 | "main": "out/index.js", 8 | "types": "out/index.d.ts", 9 | "scripts": { 10 | "build": "tsc", 11 | "clean": "rm -rf out" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "27.0.3", 15 | "@types/node": "16.11.11", 16 | "jest": "27.4.3", 17 | "ts-jest": "27.1.0", 18 | "tslib": "2.3.1", 19 | "typescript": "4.5.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/telemetry/src/index.ts: -------------------------------------------------------------------------------- 1 | import { IObservableEventData, ObservableEventType } from './observableEvent'; 2 | import { IOperatorIdentifier } from './operatorIdentifier'; 3 | 4 | export const enum TelemetryEventType { 5 | OperatorLogPoint = 'OperatorLogPoint', 6 | } 7 | 8 | export type TelemetryEvent = OperatorLogPointTelemetryEvent; 9 | 10 | export type OperatorLogPointTelemetryEvent = 11 | | IOperatorLogPointTelemetryEvent 12 | | IOperatorLogPointTelemetryEvent 13 | | IOperatorLogPointTelemetryEvent 14 | | IOperatorLogPointTelemetryEvent 15 | | IOperatorLogPointTelemetryEvent; 16 | 17 | interface IOperatorLogPointTelemetryEvent { 18 | type: TelemetryEventType.OperatorLogPoint; 19 | observableEvent: O; 20 | data: IObservableEventData[O]; 21 | operator: IOperatorIdentifier; 22 | } 23 | -------------------------------------------------------------------------------- /packages/telemetry/src/match.ts: -------------------------------------------------------------------------------- 1 | import { TelemetryEvent, TelemetryEventType } from '.'; 2 | 3 | type TelemetryEventPattern = { 4 | [T in TelemetryEventType]: (telemetryEvent: TelemetryEvent) => R; 5 | }; 6 | 7 | export default function matchTelemetryEvent( 8 | pattern: TelemetryEventPattern 9 | ): (telemetryEvent: TelemetryEvent) => R { 10 | return (e) => { 11 | switch (e.type) { 12 | case TelemetryEventType.OperatorLogPoint: 13 | return pattern.OperatorLogPoint(e); 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/telemetry/src/observableEvent/index.ts: -------------------------------------------------------------------------------- 1 | export const enum ObservableEventType { 2 | Completed = 'Completed', 3 | Error = 'Error', 4 | Next = 'Next', 5 | Subscribe = 'Subscribe', 6 | Unsubscribe = 'Unsubscribe', 7 | } 8 | 9 | export interface IObservableEventData { 10 | [ObservableEventType.Completed]: void; 11 | [ObservableEventType.Error]: { error: string }; 12 | [ObservableEventType.Next]: { value: string }; 13 | [ObservableEventType.Subscribe]: void; 14 | [ObservableEventType.Unsubscribe]: void; 15 | } 16 | -------------------------------------------------------------------------------- /packages/telemetry/src/observableEvent/match.ts: -------------------------------------------------------------------------------- 1 | import { ObservableEventType, IObservableEventData } from '.'; 2 | 3 | type ObservableEventPattern = { 4 | [T in ObservableEventType]: (data: IObservableEventData[T]) => R; 5 | }; 6 | 7 | export default function matchObservableEvent( 8 | pattern: ObservableEventPattern 9 | ): (x: { observableEvent: T; data: IObservableEventData[T] }) => R { 10 | return (e) => { 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | return pattern[e.observableEvent](e.data as any); // TODO Improve Typing and get rid of any 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/telemetry/src/operatorIdentifier/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Identifies an operator within its surrounding `pipe` statement in a specific file. 3 | * 4 | * ### Example 5 | * The `map` operator can be identified with the position information of the `pipe` statement (L 1, C 15) and the 6 | * argument index (1). `fileName` is obviously the path to the actual source file. 7 | * 8 | * ``` 9 | interval(1000).pipe( // pipe: line 1, character 15 10 | take(2), // operatorIndex: 0 11 | map(x => x - 1) // operatorIndex: 1 12 | ); 13 | ``` 14 | */ 15 | export interface IOperatorIdentifier { 16 | fileName: string; 17 | 18 | /** 19 | * The 1-based line index of the operators `pipe` statement. 20 | */ 21 | line: number; 22 | 23 | /** 24 | * The 1-based character index of the operators `pipe` statement. 25 | */ 26 | character: number; 27 | 28 | /** 29 | * 0-based index of the operator within its `pipe` statement. 30 | */ 31 | operatorIndex: number; 32 | } 33 | -------------------------------------------------------------------------------- /packages/telemetry/src/operatorIdentifier/serialize.test.ts: -------------------------------------------------------------------------------- 1 | import serialize from './serialize'; 2 | 3 | describe('Telemetry', () => { 4 | describe('OperatorIdentifier', () => { 5 | describe('serialize', () => { 6 | test('returns an IOperatorIdentifier as JSON string', () => { 7 | const fileName = '/foo/bar/baz.ts'; 8 | const line = 10; 9 | const character = 42; 10 | const operatorIndex = 2; 11 | 12 | expect( 13 | serialize({ 14 | fileName, 15 | character, 16 | line, 17 | operatorIndex, 18 | }) 19 | ).toEqual(JSON.stringify({ character, fileName, line, operatorIndex })); 20 | }); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/telemetry/src/operatorIdentifier/serialize.ts: -------------------------------------------------------------------------------- 1 | import { IOperatorIdentifier } from '.'; 2 | 3 | export default function serializeOperatorIdentifier({ 4 | character, 5 | fileName, 6 | line, 7 | operatorIndex, 8 | }: IOperatorIdentifier): string { 9 | return JSON.stringify({ character, fileName, line, operatorIndex }); 10 | } 11 | -------------------------------------------------------------------------------- /packages/telemetry/src/operatorIdentifier/toString.test.ts: -------------------------------------------------------------------------------- 1 | import toString from './toString'; 2 | 3 | describe('Telemetry', () => { 4 | describe('OperatorIdentifier', () => { 5 | describe('toString', () => { 6 | test('returns an IOperatorIdentifer as string', () => { 7 | const fileName = '/foo/bar/baz.ts'; 8 | const line = 10; 9 | const character = 42; 10 | const operatorIndex = 2; 11 | 12 | expect( 13 | toString({ 14 | fileName, 15 | character, 16 | line, 17 | operatorIndex, 18 | }) 19 | ).toEqual(`${fileName}-${line}:${character}-${operatorIndex}`); 20 | }); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/telemetry/src/operatorIdentifier/toString.ts: -------------------------------------------------------------------------------- 1 | import { IOperatorIdentifier } from '.'; 2 | 3 | export default function operatorIdentifierToString({ 4 | fileName, 5 | line, 6 | character, 7 | operatorIndex, 8 | }: IOperatorIdentifier): string { 9 | return `${fileName}-${line}:${character}-${operatorIndex}`; 10 | } 11 | -------------------------------------------------------------------------------- /packages/telemetry/src/serialize.test.ts: -------------------------------------------------------------------------------- 1 | import { TelemetryEventType } from '.'; 2 | import { ObservableEventType } from './observableEvent'; 3 | import serializeTelemetryEvent from './serialize'; 4 | describe('telemetry', () => { 5 | describe('serializeTelemetryEvent()', () => { 6 | test('serializes an OperatorLogPoint TelemetryEvent', () => { 7 | expect( 8 | serializeTelemetryEvent({ 9 | type: TelemetryEventType.OperatorLogPoint, 10 | observableEvent: ObservableEventType.Next, 11 | data: { value: 'foobar' }, 12 | operator: { 13 | fileName: 'foo.ts', 14 | character: 1, 15 | line: 2, 16 | operatorIndex: 3, 17 | }, 18 | }) 19 | ).toMatchInlineSnapshot( 20 | `"{\\"type\\":\\"OperatorLogPoint\\",\\"observableEvent\\":\\"Next\\",\\"data\\":{\\"value\\":\\"foobar\\"},\\"operator\\":{\\"fileName\\":\\"foo.ts\\",\\"character\\":1,\\"line\\":2,\\"operatorIndex\\":3}}"` 21 | ); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/telemetry/src/serialize.ts: -------------------------------------------------------------------------------- 1 | import { TelemetryEvent } from '.'; 2 | 3 | export default function serializeTelemetryEvent(telemetryEvent: TelemetryEvent): string { 4 | return JSON.stringify(telemetryEvent); 5 | } 6 | -------------------------------------------------------------------------------- /packages/telemetry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "ES6", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "outDir": "out", 12 | "rootDir": "src" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/testbench-nodejs-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | plugin/ 3 | -------------------------------------------------------------------------------- /packages/testbench-nodejs-typescript/.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 | "runtimeVersion": "14.18.1", 10 | "request": "launch", 11 | "name": "Launch NodeJS", 12 | "skipFiles": ["/**"], 13 | "program": "${workspaceFolder}/src/node.ts", 14 | "runtimeArgs": ["-r", "ts-node/register"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/testbench-nodejs-typescript/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rxjsDebugging.hideLiveLogWhenStoppingDebugger": true, 3 | "rxjsDebugging.logLevel": "Info", 4 | "rxjsDebugging.recommendOperatorLogPointsWithAnIcon": false 5 | } 6 | -------------------------------------------------------------------------------- /packages/testbench-nodejs-typescript/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "start:node", 9 | "label": "start node", 10 | "isBackground": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/testbench-nodejs-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rxjs-debugging/testbench-nodejs-typescript", 3 | "version": "1.1.0", 4 | "main": "index.js", 5 | "license": "UNLICENSED", 6 | "private": true, 7 | "scripts": { 8 | "start": "node -r ts-node/register ./src/node.ts" 9 | }, 10 | "dependencies": { 11 | "@types/node": "16.11.11", 12 | "rxjs": "6.6.7", 13 | "ts-node": "10.4.0", 14 | "typescript": "4.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/testbench-nodejs-typescript/src/node.ts: -------------------------------------------------------------------------------- 1 | import { exampleObservable } from './observable'; 2 | 3 | exampleObservable().subscribe((v) => { 4 | console.log(v); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/testbench-nodejs-typescript/src/observable.ts: -------------------------------------------------------------------------------- 1 | import { interval, Observable } from 'rxjs'; 2 | import { map, take } from 'rxjs/operators'; 3 | 4 | export function exampleObservable(): Observable { 5 | return interval(1000).pipe( 6 | take(4), 7 | map((i) => i * 2), 8 | map((i) => i * 20) 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/testbench-nodejs-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "outDir": "out", 5 | "rootDir": "src" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/testbench-nodejs/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | plugin/ 3 | -------------------------------------------------------------------------------- /packages/testbench-nodejs/.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 | "runtimeVersion": "14.18.1", 10 | "request": "launch", 11 | "name": "Launch CommonJS", 12 | "skipFiles": ["/**"], 13 | "program": "${workspaceFolder}/src/commonjs/index.js" 14 | }, 15 | { 16 | "type": "node", 17 | "runtimeVersion": "14.18.1", 18 | "request": "launch", 19 | "name": "Launch ESModules", 20 | "skipFiles": ["/**"], 21 | "program": "${workspaceFolder}/src/esmodules/index.mjs" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/testbench-nodejs/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rxjsDebugging.hideLiveLogWhenStoppingDebugger": true, 3 | "rxjsDebugging.logLevel": "Info", 4 | "rxjsDebugging.recommendOperatorLogPointsWithAnIcon": false 5 | } 6 | -------------------------------------------------------------------------------- /packages/testbench-nodejs/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "start:node", 9 | "label": "start node", 10 | "isBackground": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/testbench-nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rxjs-debugging/testbench-nodejs", 3 | "version": "1.1.0", 4 | "main": "index.js", 5 | "license": "UNLICENSED", 6 | "private": true, 7 | "scripts": { 8 | "start": "yarn start:commonjs", 9 | "start:commonjs": "node ./src/commonjs/index.js", 10 | "start:esmodules": "node ./src/esmodules/index.mjs" 11 | }, 12 | "dependencies": { 13 | "@types/node": "16.11.11", 14 | "rxjs": "6.6.7" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/testbench-nodejs/src/commonjs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | '@typescript-eslint/no-var-requires': [0], 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/testbench-nodejs/src/commonjs/index.js: -------------------------------------------------------------------------------- 1 | const exampleObservable = require('./observable'); 2 | 3 | exampleObservable().subscribe((v) => { 4 | console.log(v); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/testbench-nodejs/src/commonjs/observable.js: -------------------------------------------------------------------------------- 1 | const { interval } = require('rxjs'); 2 | const { map, take } = require('rxjs/operators'); 3 | 4 | module.exports = function exampleObservable() { 5 | return interval(1000).pipe( 6 | take(4), 7 | map((i) => i * 2), 8 | map((i) => i * 20) 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/testbench-nodejs/src/esmodules/index.mjs: -------------------------------------------------------------------------------- 1 | import exampleObservable from './observable.mjs'; 2 | 3 | exampleObservable().subscribe((v) => { 4 | console.log(v); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/testbench-nodejs/src/esmodules/observable.mjs: -------------------------------------------------------------------------------- 1 | import { interval } from 'rxjs'; 2 | import { map, take } from 'rxjs/operators/index.js'; 3 | 4 | export default function exampleObservable() { 5 | return interval(1000).pipe( 6 | take(4), 7 | map((i) => i * 2), 8 | map((i) => i * 20) 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/testbench-webpack/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | plugin/ 3 | -------------------------------------------------------------------------------- /packages/testbench-webpack/.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": "chrome", 9 | "request": "launch", 10 | "name": "Launch Browser", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}", 13 | "sourceMaps": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/testbench-webpack/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rxjsDebugging.enableUsageAnalytics": false, 3 | "rxjsDebugging.hideLiveLogWhenStoppingDebugger": true, 4 | "rxjsDebugging.logLevel": "Info", 5 | "rxjsDebugging.recommendOperatorLogPointsWithAnIcon": false 6 | } 7 | -------------------------------------------------------------------------------- /packages/testbench-webpack/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "label": "start", 9 | "script": "start", 10 | "promptOnClose": true, 11 | "isBackground": true, 12 | "problemMatcher": { 13 | "owner": "webpack", 14 | "severity": "error", 15 | "fileLocation": "absolute", 16 | "pattern": [ 17 | { 18 | "regexp": "ERROR in (.*)", 19 | "file": 1 20 | }, 21 | { 22 | "regexp": "\\((\\d+),(\\d+)\\):(.*)", 23 | "line": 1, 24 | "column": 2, 25 | "message": 3 26 | } 27 | ], 28 | "background": { 29 | "activeOnStart": true, 30 | "beginsPattern": "Compiling\\.\\.\\.", 31 | "endsPattern": "Compiled successfully\\." 32 | } 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/testbench-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rxjs-debugging/testbench-webpack", 3 | "version": "1.1.1", 4 | "license": "UNLICENSED", 5 | "private": true, 6 | "scripts": { 7 | "start": "webpack serve" 8 | }, 9 | "dependencies": { 10 | "@rxjs-debugging/runtime-webpack": "^1.1.1", 11 | "html-webpack-plugin": "5.5.0", 12 | "rxjs": "6.6.7", 13 | "ts-loader": "9.2.6", 14 | "typescript": "4.5.2", 15 | "webpack": "5.64.4", 16 | "webpack-cli": "4.9.1", 17 | "webpack-dev-server": "4.6.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/testbench-webpack/src/browser.ts: -------------------------------------------------------------------------------- 1 | import { exampleObservable } from './observable'; 2 | 3 | exampleObservable().subscribe((v) => { 4 | document.querySelector('body').textContent = `Value: ${v}`; 5 | }); 6 | -------------------------------------------------------------------------------- /packages/testbench-webpack/src/observable.ts: -------------------------------------------------------------------------------- 1 | import { interval, Observable } from 'rxjs'; 2 | import { map, take } from 'rxjs/operators'; 3 | 4 | export function exampleObservable(): Observable { 5 | return interval(1000).pipe( 6 | take(4), 7 | map((i) => i * 2), 8 | map((i) => i * 20) 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/testbench-webpack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "outDir": "out", 5 | "rootDir": "src" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/testbench-webpack/webpack.config.mjs: -------------------------------------------------------------------------------- 1 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 2 | import * as path from 'path'; 3 | import * as url from 'url'; 4 | import RxJSDebuggingPlugin from '@rxjs-debugging/runtime-webpack'; 5 | 6 | const dirname = path.dirname(url.fileURLToPath(import.meta.url)); 7 | 8 | /** @type { import('webpack').Configuration } */ 9 | const config = { 10 | mode: 'development', 11 | entry: './src/browser.ts', 12 | output: { 13 | path: path.resolve(dirname, 'dist'), 14 | filename: 'bundle.js', 15 | }, 16 | resolve: { 17 | extensions: ['.ts', '.js'], 18 | }, 19 | devtool: 'source-map', 20 | module: { 21 | rules: [{ test: /.ts$/, loader: 'ts-loader' }], 22 | }, 23 | plugins: [ 24 | new RxJSDebuggingPlugin(), 25 | new HtmlWebpackPlugin({ 26 | title: 'RxJS Debugger for vscode | Example Workspace', 27 | }), 28 | ], 29 | }; 30 | 31 | export default config; 32 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", ":prConcurrentLimit10"] 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "exclude": ["node_modules", "**/*.test.ts", "packages/*/out"] 4 | } 5 | -------------------------------------------------------------------------------- /workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "packages/extension" 5 | }, 6 | { 7 | "path": "packages/extension-integrationtest" 8 | }, 9 | { 10 | "path": "packages/runtime" 11 | }, 12 | { 13 | "path": "packages/runtime-nodejs" 14 | }, 15 | { 16 | "path": "packages/runtime-webpack" 17 | }, 18 | { 19 | "path": "packages/telemetry" 20 | }, 21 | { 22 | "path": "packages/testbench-nodejs" 23 | }, 24 | { 25 | "path": "packages/testbench-nodejs-typescript" 26 | }, 27 | { 28 | "path": "packages/testbench-webpack" 29 | }, 30 | { 31 | "path": ".", 32 | "name": "Root" 33 | } 34 | ], 35 | "settings": { 36 | "jest.disabledWorkspaceFolders": [ 37 | "Root", 38 | "extension-integrationtest", 39 | "testbench-nodejs", 40 | "testbench-nodejs-typescript", 41 | "testbench-webpack" 42 | ], 43 | "search.exclude": { 44 | "**/out": true // set this to false to include "out" folder in search results 45 | } 46 | } 47 | } 48 | --------------------------------------------------------------------------------