├── .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 | 
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 | 
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 | #  RxJS Debugging for Visual Studio Code
2 |
3 | [](https://marketplace.visualstudio.com/items?itemName=manuelalabor.rxjs-debugging-for-vs-code) [](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 | 
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 |
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 |
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 |
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 |
60 |
--------------------------------------------------------------------------------
/packages/extension/resources/debug-breakpoint-log.svg:
--------------------------------------------------------------------------------
1 |
2 |
57 |
--------------------------------------------------------------------------------
/packages/extension/resources/rxjs-operator-log-point-all-enabled.svg:
--------------------------------------------------------------------------------
1 |
2 |
58 |
--------------------------------------------------------------------------------
/packages/extension/resources/rxjs-operator-log-point-disabled.svg:
--------------------------------------------------------------------------------
1 |
2 |
58 |
--------------------------------------------------------------------------------
/packages/extension/resources/rxjs-operator-log-point-some-enabled.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 | #  @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 | [](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 | 
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 |
--------------------------------------------------------------------------------