diff --git a/.mocharc.unit.js b/.mocharc.unit.js new file mode 100644 index 000000000..22a02592a --- /dev/null +++ b/.mocharc.unit.js @@ -0,0 +1,5 @@ +module.exports = { + require: ['source-map-support/register', './out/src/test/testHooks'], + spec: 'out/src/**/*.test.js', + ignore: ['out/src/test/**/*.js'], +}; diff --git a/.vscode/launch.json b/.vscode/launch.json index 8fd446224..6d1b70cdb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -36,7 +36,6 @@ "type": "extensionHost", "request": "launch", "skipFiles": ["/**"], - "runtimeExecutable": "${execPath}", "args": [ "--enable-proposed-api=ms-vscode.js-debug", "--extensionDevelopmentPath=${workspaceFolder}/out", @@ -52,7 +51,6 @@ "type": "extensionHost", "request": "launch", "skipFiles": ["/**"], - "runtimeExecutable": "${execPath}", "args": [ "--enable-proposed-api=ms-vscode.js-debug", "--extensionDevelopmentPath=${workspaceFolder}/dist" @@ -67,7 +65,6 @@ "type": "pwa-extensionHost", "request": "launch", "skipFiles": ["/**"], - "runtimeExecutable": "${execPath}", "args": [ "--enable-proposed-api=ms-vscode.js-debug", "--extensionDevelopmentPath=${workspaceFolder}/out" @@ -76,12 +73,31 @@ "${workspaceFolder}/out/**/*.js" ], }, + { + "name": "Extension and Auto Launch", + "type": "pwa-extensionHost", + "request": "launch", + "skipFiles": ["/**"], + "args": [ + "--enable-proposed-api=ms-vscode.js-debug", + "--extensionDevelopmentPath=${workspaceFolder}/../vscode/extensions/debug-auto-launch", + "--extensionDevelopmentPath=${workspaceFolder}/out" + ], + "resolveSourceMapLocations": [ + "${workspaceFolder}/**", + "${workspaceFolder}/../vscode/extensions/debug-auto-launch/out/**", + "!**/node_modules/**" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js", + "${workspaceFolder}/../vscode-js-debug-companion/out/**/*.js", + ], + }, { "name": "Extension and Companion", "type": "pwa-extensionHost", "request": "launch", "skipFiles": ["/**"], - "runtimeExecutable": "${execPath}", "args": [ "--enable-proposed-api=ms-vscode.js-debug", "--extensionDevelopmentPath=${workspaceFolder}/../vscode-js-debug-companion", @@ -105,7 +121,6 @@ "name": "Run Tests", "type": "pwa-extensionHost", "request": "launch", - "runtimeExecutable": "${execPath}", "skipFiles": ["/**"], "args": [ "--extensionDevelopmentPath=${workspaceFolder}/out", @@ -118,7 +133,6 @@ "name": "Reset Results", "type": "extensionHost", "request": "launch", - "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/src/test/testRunner" diff --git a/OPTIONS.md b/OPTIONS.md index 02853ea04..47d84581d 100644 --- a/OPTIONS.md +++ b/OPTIONS.md @@ -5,9 +5,9 @@

address

TCP/IP address of process to be debugged. Default is 'localhost'.

Default value:
"localhost"

attachExistingChildren

Whether to attempt to attach to already-spawned child processes.

Default value:
true

autoAttachChildProcesses

Attach debugger to new child processes automatically.

-
Default value:
true

continueOnAttach

If true, we'll automatically resume programs launched and waiting on --inspect-brk

-
Default value:
false

customDescriptionGenerator

Customize the textual description the debugger shows for objects (local variables, etc...). Samples:
1. this.toString() // will call toString to print all objects
2. this.customDescription ? this.customDescription() : defaultValue // Use customDescription method if available, if not return defaultValue
3. function (def) { return this.customDescription ? this.customDescription() : def } // Use customDescription method if available, if not return defaultValue

-
Default value:
undefined

cwd

Absolute path to the working directory of the program being debugged.

+
Default value:
true

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

continueOnAttach

If true, we'll automatically resume programs launched and waiting on --inspect-brk

+
Default value:
false

cwd

Absolute path to the working directory of the program being debugged.

Default value:
"${workspaceFolder}"

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

env

Environment variables passed to the program. The value null removes the variable from the environment.

Default value:
{}

envFile

Absolute path to a file containing environment variable definitions.

@@ -40,22 +40,17 @@
Default value:
true

timeout

Retry for this number of milliseconds to connect to Node.js. Default is 10000 ms.

Default value:
10000

timeouts

Timeouts for several debugger operations

Default value:
{}

trace

Configures what diagnostic output is produced.

-
Default value:
false
+
Default value:
false

websocketAddress

Exact websocket address to attach to. If unspecified, it will be discovered from the address and port.

+
Default value:
undefined
### pwa-node: launch

args

Command line arguments passed to the program.

Default value:
[]

attachSimplePort

If set, attaches to the process via the given port. This is generally no longer necessary for Node.js programs and loses the ability to debug child processes, but can be useful in more esoteric scenarios such as with Deno and Docker launches. If set to 0, a random port will be chosen and --inspect-brk added to the launch arguments automatically.

Default value:
null

autoAttachChildProcesses

Attach debugger to new child processes automatically.

-
Default value:
true

console

Where to launch the debug target.

-
Default value:
"internalConsole"

customDescriptionGenerator

Customize the textual description the debugger shows for objects (local variables, etc...). Samples:
1. this.toString() // will call toString to print all objects
2. this.customDescription ? this.customDescription() : defaultValue // Use customDescription method if available, if not return defaultValue
3. function (def) { return this.customDescription ? this.customDescription() : def } // Use customDescription method if available, if not return defaultValue

-
Default value:
undefined

cwd

Absolute path to the working directory of the program being debugged.

-
Default value:
"${workspaceFolder}"

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

-
Default value:
true

env

Environment variables passed to the program. The value null removes the variable from the environment.

Default value:
{}

envFile

Absolute path to a file containing environment variable definitions.

Default value:
null

localRoot

Path to the local directory containing the program.

Default value:
null

nodeVersionHint

Retry for this number of milliseconds to connect to Node.js. Default is 10000 ms.

-
Default value:
undefined

outFiles

If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with ! the files are excluded. If not specified, the generated code is expected in the same directory as its source.

Default value:
[
   "${workspaceFolder}/**/*.js",
   "!**/node_modules/**"
@@ -91,8 +86,8 @@
 ### node-terminal: launch
 
 

autoAttachChildProcesses

Attach debugger to new child processes automatically.

-
Default value:
true

command

Command to run in the launched terminal. If not provided, the terminal will open without launching a program.

-
Default value:
undefined

customDescriptionGenerator

Customize the textual description the debugger shows for objects (local variables, etc...). Samples:
1. this.toString() // will call toString to print all objects
2. this.customDescription ? this.customDescription() : defaultValue // Use customDescription method if available, if not return defaultValue
3. function (def) { return this.customDescription ? this.customDescription() : def } // Use customDescription method if available, if not return defaultValue

+
Default value:
true

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

command

Command to run in the launched terminal. If not provided, the terminal will open without launching a program.

Default value:
undefined

cwd

Absolute path to the working directory of the program being debugged.

Default value:
"${workspaceFolder}"

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

env

Environment variables passed to the program. The value null removes the variable from the environment.

@@ -133,8 +128,8 @@
Default value:
[
   "--extensionDevelopmentPath=${workspaceFolder}"
 ]

autoAttachChildProcesses

Attach debugger to new child processes automatically.

-
Default value:
false

customDescriptionGenerator

Customize the textual description the debugger shows for objects (local variables, etc...). Samples:
1. this.toString() // will call toString to print all objects
2. this.customDescription ? this.customDescription() : defaultValue // Use customDescription method if available, if not return defaultValue
3. function (def) { return this.customDescription ? this.customDescription() : def } // Use customDescription method if available, if not return defaultValue

-
Default value:
undefined

cwd

Absolute path to the working directory of the program being debugged.

+
Default value:
false

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

cwd

Absolute path to the working directory of the program being debugged.

Default value:
"${workspaceFolder}"

debugWebviews

Configures whether we should try to attach to webviews in the launched VS Code instance. Note: at the moment this requires the setting "webview.experimental.useExternalEndpoint": true to work properly, and will only work in desktop VS Code.

Default value:
false

debugWebWorkerHost

Configures whether we should try to attach to the web worker extension host.

Default value:
false

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

@@ -172,9 +167,9 @@ ### pwa-chrome: launch

browserLaunchLocation

Forces the browser to be launched in one location. In a remote workspace (through ssh or WSL, for example) this can be used to open the browser on the remote machine rather than locally.

-
Default value:
"workspace"

cleanUp

What clean-up to do after the debugging session finishes. Close only the tab being debug, vs. close the whole browser.

-
Default value:
"wholeBrowser"

customDescriptionGenerator

Customize the textual description the debugger shows for objects (local variables, etc...). Samples:
1. this.toString() // will call toString to print all objects
2. this.customDescription ? this.customDescription() : defaultValue // Use customDescription method if available, if not return defaultValue
3. function (def) { return this.customDescription ? this.customDescription() : def } // Use customDescription method if available, if not return defaultValue

-
Default value:
undefined

cwd

Optional working directory for the runtime executable.

+
Default value:
"workspace"

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

cleanUp

What clean-up to do after the debugging session finishes. Close only the tab being debug, vs. close the whole browser.

+
Default value:
"wholeBrowser"

cwd

Optional working directory for the runtime executable.

Default value:
null

disableNetworkCache

Controls whether to skip the network cache for each request

Default value:
true

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

env

Optional dictionary of environment key/value pairs for the browser.

@@ -218,13 +213,9 @@ ### pwa-chrome: attach

address

IP address or hostname the debugged browser is listening on.

-
Default value:
"localhost"

customDescriptionGenerator

Customize the textual description the debugger shows for objects (local variables, etc...). Samples:
1. this.toString() // will call toString to print all objects
2. this.customDescription ? this.customDescription() : defaultValue // Use customDescription method if available, if not return defaultValue
3. function (def) { return this.customDescription ? this.customDescription() : def } // Use customDescription method if available, if not return defaultValue

-
Default value:
undefined

disableNetworkCache

Controls whether to skip the network cache for each request

-
Default value:
true

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

inspectUri

Format to use to rewrite the inspectUri: It's a template string that interpolates keys in {curlyBraces}. Available keys are:
- url.* is the parsed address of the running application. For instance, {url.port}, {url.hostname}
- port is the debug port that Chrome is listening on.
- browserInspectUri is the inspector URI on the launched browser
- wsProtocol is the hinted websocket protocol. This is set to wss if the original URL is https, or ws otherwise.

Default value:
undefined

outFiles

If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with ! the files are excluded. If not specified, the generated code is expected in the same directory as its source.

Default value:
[
-  "${workspaceFolder}/**/*.js",
   "!**/node_modules/**"
 ]

outputCapture

From where to capture output messages: the default debug API if set to console, or stdout/stderr streams if set to std.

Default value:
"console"

pathMapping

A mapping of URLs/paths to local folders, to resolve scripts in the Browser to scripts on disk

@@ -258,9 +249,9 @@

address

When debugging webviews, the IP address or hostname the webview is listening on. Will be automatically discovered if not set.

Default value:
"localhost"

browserLaunchLocation

Forces the browser to be launched in one location. In a remote workspace (through ssh or WSL, for example) this can be used to open the browser on the remote machine rather than locally.

-
Default value:
"workspace"

cleanUp

What clean-up to do after the debugging session finishes. Close only the tab being debug, vs. close the whole browser.

-
Default value:
"wholeBrowser"

customDescriptionGenerator

Customize the textual description the debugger shows for objects (local variables, etc...). Samples:
1. this.toString() // will call toString to print all objects
2. this.customDescription ? this.customDescription() : defaultValue // Use customDescription method if available, if not return defaultValue
3. function (def) { return this.customDescription ? this.customDescription() : def } // Use customDescription method if available, if not return defaultValue

-
Default value:
undefined

cwd

Optional working directory for the runtime executable.

+
Default value:
"workspace"

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

cleanUp

What clean-up to do after the debugging session finishes. Close only the tab being debug, vs. close the whole browser.

+
Default value:
"wholeBrowser"

cwd

Optional working directory for the runtime executable.

Default value:
null

disableNetworkCache

Controls whether to skip the network cache for each request

Default value:
true

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

env

Optional dictionary of environment key/value pairs for the browser.

@@ -305,8 +296,9 @@ ### pwa-msedge: attach

address

IP address or hostname the debugged browser is listening on.

-
Default value:
"localhost"

customDescriptionGenerator

Customize the textual description the debugger shows for objects (local variables, etc...). Samples:
1. this.toString() // will call toString to print all objects
2. this.customDescription ? this.customDescription() : defaultValue // Use customDescription method if available, if not return defaultValue
3. function (def) { return this.customDescription ? this.customDescription() : def } // Use customDescription method if available, if not return defaultValue

-
Default value:
undefined

disableNetworkCache

Controls whether to skip the network cache for each request

+
Default value:
"localhost"

browserAttachLocation

Forces the browser to attach in one location. In a remote workspace (through ssh or WSL, for example) this can be used to attach to a browser on the remote machine rather than locally.

+
Default value:
"workspace"

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

disableNetworkCache

Controls whether to skip the network cache for each request

Default value:
true

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

inspectUri

Format to use to rewrite the inspectUri: It's a template string that interpolates keys in {curlyBraces}. Available keys are:
- url.* is the parsed address of the running application. For instance, {url.port}, {url.hostname}
- port is the debug port that Chrome is listening on.
- browserInspectUri is the inspector URI on the launched browser
- wsProtocol is the hinted websocket protocol. This is set to wss if the original URL is https, or ws otherwise.

Default value:
undefined

outFiles

If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with ! the files are excluded. If not specified, the generated code is expected in the same directory as its source.

diff --git a/demos/babel/package-lock.json b/demos/babel/package-lock.json index 3685ab062..4897c93bc 100644 --- a/demos/babel/package-lock.json +++ b/demos/babel/package-lock.json @@ -1039,9 +1039,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "loose-envify": { "version": "1.4.0", diff --git a/demos/webpack/package-lock.json b/demos/webpack/package-lock.json index 6c53c0642..eb2d877c4 100644 --- a/demos/webpack/package-lock.json +++ b/demos/webpack/package-lock.json @@ -1237,9 +1237,9 @@ } }, "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "dev": true, "requires": { "bn.js": "^4.4.0", diff --git a/gulpfile.js b/gulpfile.js index 5bde853ac..94358903b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -273,6 +273,17 @@ gulp.task('flatSessionBundle:webpack-bundle', async () => { return runWebpack({ packages, devtool: 'nosources-source-map' }); }); +gulp.task('package:bootloader-as-cdp', done => { + const bootloaderFilePath = path.resolve(distSrcDir, 'bootloader.bundle.js'); + fs.appendFile(bootloaderFilePath, '\n//# sourceURL=bootloader.bundle.cdp', done); +}); + +/** Run webpack to bundle into the VS debug server */ +gulp.task('vsDebugServerBundle:webpack-bundle', async () => { + const packages = [{ entry: `${buildSrcDir}/vsDebugServer.js`, library: true }]; + return runWebpack({ packages, devtool: 'nosources-source-map' }); +}); + /** Copy the extension static files */ gulp.task('package:copy-extension-files', () => merge( @@ -367,6 +378,7 @@ gulp.task( 'compile:static', 'compile:dynamic', 'package:webpack-bundle', + 'package:bootloader-as-cdp', 'package:copy-extension-files', 'nls:bundle-create', 'package:createVSIX', @@ -379,6 +391,21 @@ gulp.task( 'clean', 'compile', 'flatSessionBundle:webpack-bundle', + 'package:bootloader-as-cdp', + 'package:copy-extension-files', + gulp.parallel('nls:bundle-download', 'nls:bundle-create'), + ), +); + +// for now, this task will build both flat session and debug server until we no longer need flat session +gulp.task( + 'vsDebugServerBundle', + gulp.series( + 'clean', + 'compile', + 'vsDebugServerBundle:webpack-bundle', + 'flatSessionBundle:webpack-bundle', + 'package:bootloader-as-cdp', 'package:copy-extension-files', gulp.parallel('nls:bundle-download', 'nls:bundle-create'), ), diff --git a/package-lock.json b/package-lock.json index fbfc35975..fe6b0b62a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "js-debug", - "version": "1.48.1", + "version": "1.49.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -833,12 +833,6 @@ "@types/node": "*" } }, - "@types/lodash": { - "version": "4.14.149", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", - "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==", - "dev": true - }, "@types/long": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", @@ -2916,6 +2910,11 @@ "type": "^1.0.1" } }, + "data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" + }, "dateformat": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", @@ -3419,9 +3418,9 @@ "dev": true }, "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "dev": true, "requires": { "bn.js": "^4.4.0", diff --git a/package.json b/package.json index 5224d3daa..f43f78115 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "js-debug", "displayName": "JavaScript Debugger", - "version": "1.48.1", + "version": "1.49.2", "publisher": "ms-vscode", "author": { "name": "Microsoft Corporation" @@ -17,7 +17,7 @@ "description": "An extension for debugging Node.js programs and Chrome.", "license": "MIT", "engines": { - "vscode": "^1.47.0-insider", + "vscode": "^1.49.0-insider", "node": ">=10" }, "icon": "resources/logo.png", @@ -45,14 +45,16 @@ "publish": "gulp publish", "updatetypes": "cd src/typings && vscode-dts dev && vscode-dts master", "generateapis": "node out/src/build/generateDap.js && node out/src/build/generateCdp.js", - "test": "gulp && npm-run-all --parallel test:types test:golden test:lint", + "test": "gulp && npm-run-all --parallel test:unit test:types test:golden test:lint", "test:types": "tsc --noEmit", + "test:unit": "mocha --config .mocharc.unit.js", "test:golden": "node ./out/src/test/runTest.js", "test:lint": "gulp lint" }, "dependencies": { "@c4312/chromehash": "^0.2.0", "color": "^3.1.2", + "data-uri-to-buffer": "^3.0.1", "default-browser": "^2.0.1", "execa": "^4.0.0", "glob-stream": "^6.1.0", @@ -97,7 +99,6 @@ "@types/gulp": "^4.0.6", "@types/js-beautify": "^1.8.2", "@types/json-schema": "^7.0.4", - "@types/lodash": "^4.14.149", "@types/long": "^4.0.1", "@types/marked": "^0.7.4", "@types/micromatch": "^4.0.1", @@ -134,7 +135,6 @@ "gulp-tsb": "^4.0.5", "gulp-util": "^3.0.8", "husky": "^4.2.3", - "lodash": "^4.17.15", "marked": "^1.1.0", "merge2": "^1.3.0", "minimist": "^1.2.5", diff --git a/src/adapter/breakpoints.ts b/src/adapter/breakpoints.ts index 22763748e..37bbed5a5 100644 --- a/src/adapter/breakpoints.ts +++ b/src/adapter/breakpoints.ts @@ -145,6 +145,8 @@ export class BreakpointManager { this._sourceContainer = sourceContainer; this.pauseForSourceMaps = launchConfig.pauseForSourceMap; + _breakpointsPredictor?.onLongParse(() => dap.longPrediction({})); + this._scriptSourceMapHandler = async (script, sources) => { if ( !logger.assert( @@ -451,20 +453,14 @@ export class BreakpointManager { this._predictorDisabledForTest = disabled; } - async _updateSourceMapHandler(thread: Thread) { + private _updateSourceMapHandler(thread: Thread) { this._sourceMapHandlerWasUpdated = true; - await thread.setScriptSourceMapHandler(true, this._scriptSourceMapHandler); - - if (!this._breakpointsPredictor || this.pauseForSourceMaps) { - return; + if (this._breakpointsPredictor && !this.pauseForSourceMaps) { + return thread.setScriptSourceMapHandler(false, this._scriptSourceMapHandler); + } else { + return thread.setScriptSourceMapHandler(true, this._scriptSourceMapHandler); } - - // If we set a predictor and don't want to pause, we still wait to wait - // for the predictor to finish running. Uninstall the sourcemap handler - // once we see the predictor is ready to roll. - await this._breakpointsPredictor.prepareToPredict(); - thread.setScriptSourceMapHandler(false, this._scriptSourceMapHandler); } private _setBreakpoint(b: Breakpoint, thread: Thread): void { diff --git a/src/adapter/breakpoints/conditions/logPoint.ts b/src/adapter/breakpoints/conditions/logPoint.ts index 1e2c85cd8..c0dfe2a7d 100644 --- a/src/adapter/breakpoints/conditions/logPoint.ts +++ b/src/adapter/breakpoints/conditions/logPoint.ts @@ -21,13 +21,6 @@ import { returnErrorsFromStatements } from '../../../common/sourceCodeManipulati */ @injectable() export class LogPointCompiler { - /** - * Gets whether the url looks like a log point source. - */ - public static isLogPointUrl(url: string) { - return /logpoint-[a-f0-9]+.vs$/.test(url); - } - constructor( @inject(ILogger) private readonly logger: ILogger, @inject(IEvaluator) private readonly evaluator: IEvaluator, diff --git a/src/adapter/completions.ts b/src/adapter/completions.ts index 076109770..b08088109 100644 --- a/src/adapter/completions.ts +++ b/src/adapter/completions.ts @@ -2,15 +2,15 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +import { inject, injectable } from 'inversify'; import * as ts from 'typescript'; -import Dap from '../dap/api'; import Cdp from '../cdp/api'; -import { StackFrame } from './stackTrace'; +import { ICdpApi } from '../cdp/connection'; import { positionToOffset } from '../common/sourceUtils'; -import { enumerateProperties, enumeratePropertiesTemplate } from './templates/enumerateProperties'; -import { injectable, inject } from 'inversify'; +import Dap from '../dap/api'; import { IEvaluator, returnValueStr } from './evaluator'; -import { ICdpApi } from '../cdp/connection'; +import { StackFrame } from './stackTrace'; +import { enumerateProperties, enumeratePropertiesTemplate } from './templates/enumerateProperties'; /** * Context in which a completion is being evaluated. diff --git a/src/adapter/console/console.ts b/src/adapter/console/console.ts new file mode 100644 index 000000000..ed06c3de6 --- /dev/null +++ b/src/adapter/console/console.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { inject, injectable } from 'inversify'; +import { IConsole } from '.'; +import Cdp from '../../cdp/api'; +import { assertNever } from '../../common/objUtils'; +import Dap from '../../dap/api'; +import { IDapApi } from '../../dap/connection'; +import { Thread } from '../threads'; +import { ClearMessage, EndGroupMessage, IConsoleMessage } from './consoleMessage'; +import { ReservationQueue } from './reservationQueue'; +import { + AssertMessage, + ErrorMessage, + LogMessage, + StartGroupMessage, + TableMessage, + TraceMessage, + WarningMessage, +} from './textualMessage'; + +const duplicateNodeJsLogFunctions = new Set(['group', 'assert', 'count']); + +@injectable() +export class Console implements IConsole { + private isDirty = false; + + private readonly queue = new ReservationQueue(events => { + for (const event of events) { + this.dap.output(event); + } + }); + + /** + * Fires when the queue is drained. + */ + public readonly onDrained = this.queue.onDrained; + + /** + * Gets the current length of the queue. + */ + public get length() { + return this.queue.length; + } + + constructor(@inject(IDapApi) private readonly dap: Dap.Api) {} + + /** + * @inheritdoc + */ + public dispose() { + this.queue.dispose(); + } + + /** + * @inheritdoc + */ + public dispatch(thread: Thread, event: Cdp.Runtime.ConsoleAPICalledEvent) { + const parsed = this.parse(event); + if (parsed) { + this.enqueue(thread, parsed); + } + } + + /** + * @inheritdoc + */ + public enqueue(thread: Thread, message: IConsoleMessage) { + if (!(message instanceof ClearMessage)) { + this.isDirty = true; + } else if (this.isDirty) { + this.isDirty = false; + } else { + return; + } + + this.queue.enqueue(message.toDap(thread)); + } + + /** + * @inheritdoc + */ + public parse(event: Cdp.Runtime.ConsoleAPICalledEvent): IConsoleMessage | undefined { + if (event.type === 'log') { + // Ignore the duplicate group events that Node.js can emit: + // See: https://github.com/nodejs/node/issues/31973 + const firstFrame = event.stackTrace?.callFrames[0]; + if ( + firstFrame && + firstFrame.url === 'internal/console/constructor.js' && + duplicateNodeJsLogFunctions.has(firstFrame.functionName) + ) { + return; + } + } + + switch (event.type) { + case 'clear': + return new ClearMessage(); + case 'endGroup': + return new EndGroupMessage(); + case 'assert': + return new AssertMessage(event); + case 'table': + return new TableMessage(event); + case 'startGroup': + case 'startGroupCollapsed': + return new StartGroupMessage(event); + case 'debug': + case 'log': + case 'info': + return new LogMessage(event); + case 'trace': + return new TraceMessage(event); + case 'error': + return new ErrorMessage(event); + case 'warning': + return new WarningMessage(event); + case 'dir': + case 'dirxml': + return new LogMessage(event); // a normal object inspection + case 'count': + return new LogMessage(event); // contents are like a normal log + case 'profile': + case 'profileEnd': + return new LogMessage(event); // non-standard events, not implemented in Chrome it seems + case 'timeEnd': + return new LogMessage(event); // contents are like a normal log + default: + try { + assertNever(event.type, 'unknown console message type'); + } catch { + // ignore + } + } + } +} diff --git a/src/adapter/console/consoleMessage.ts b/src/adapter/console/consoleMessage.ts new file mode 100644 index 000000000..b288c65fc --- /dev/null +++ b/src/adapter/console/consoleMessage.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import Dap from '../../dap/api'; +import { Thread } from '../threads'; + +export interface IConsoleMessage { + toDap(thread: Thread): Promise | Dap.OutputEventParams; +} + +export class ClearMessage implements IConsoleMessage { + /** + * @inheritdoc + */ + public toDap(): Dap.OutputEventParams { + return { + category: 'console', + output: '\x1b[2J', + }; + } +} + +export class EndGroupMessage implements IConsoleMessage { + /** + * @inheritdoc + */ + public toDap(): Dap.OutputEventParams { + return { category: 'stdout', output: '', group: 'end' }; + } +} diff --git a/src/adapter/console/exceptionMessage.ts b/src/adapter/console/exceptionMessage.ts new file mode 100644 index 000000000..06d4d44b5 --- /dev/null +++ b/src/adapter/console/exceptionMessage.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import Cdp from '../../cdp/api'; +import Dap from '../../dap/api'; +import { previewException } from '../objectPreview'; +import { Thread } from '../threads'; +import { TextualMessage } from './textualMessage'; + +/** + * Special console message formed from an unhandled exception. + */ +export class ExceptionMessage extends TextualMessage { + /** + * @override + */ + public async toDap(thread: Thread): Promise { + const preview = this.event.exception ? previewException(this.event.exception) : { title: '' }; + + let message = preview.title; + if (!message.startsWith('Uncaught')) { + message = 'Uncaught ' + message; + } + + const stackTrace = this.stackTrace(thread); + const args = this.event.exception && !preview.stackTrace ? [this.event.exception] : []; + + return { + category: 'stderr', + output: message, + variablesReference: + stackTrace || args.length + ? await thread.replVariables.createVariableForOutput(message, args, stackTrace) + : undefined, + ...(await this.getUiLocation(thread)), + }; + } +} diff --git a/src/adapter/console/index.ts b/src/adapter/console/index.ts new file mode 100644 index 000000000..f333954e1 --- /dev/null +++ b/src/adapter/console/index.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import type { Event } from 'vscode'; +import Cdp from '../../cdp/api'; +import { IDisposable } from '../../common/disposable'; +import { Thread } from '../threads'; +import { IConsoleMessage } from './consoleMessage'; + +export * from './queryObjectsMessage'; +export * from './exceptionMessage'; + +export const IConsole = Symbol('IConsole'); + +export interface IConsole extends IDisposable { + /** + * Fires when the output queue is drained. + */ + readonly onDrained: Event; + + /** + * Gets the current length of the output queue. + */ + readonly length: number; + + /** + * Translates and sends the event to the underlying DAP connection. + */ + dispatch(thread: Thread, event: Cdp.Runtime.ConsoleAPICalledEvent): void; + + /** + * Parses the event to a console message. Returns undefined if the message + * cannot be parsed or should not be sent to the client. + */ + parse(event: Cdp.Runtime.ConsoleAPICalledEvent): IConsoleMessage | undefined; + + /** + * Schedules the message, or promise of a message, to be written to the console. + */ + enqueue(thread: Thread, message: IConsoleMessage): void; +} diff --git a/src/adapter/console/queryObjectsMessage.ts b/src/adapter/console/queryObjectsMessage.ts new file mode 100644 index 000000000..b41c89e81 --- /dev/null +++ b/src/adapter/console/queryObjectsMessage.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; +import Cdp from '../../cdp/api'; +import Dap from '../../dap/api'; +import { previewRemoteObject } from '../objectPreview'; +import { previewThis } from '../templates/previewThis'; +import { Thread } from '../threads'; +import { IConsoleMessage } from './consoleMessage'; + +const localize = nls.loadMessageBundle(); + +/** + * Message sent as the result of querying objects on the runtime. + */ +export class QueryObjectsMessage implements IConsoleMessage { + constructor(private readonly protoObj: Cdp.Runtime.RemoteObject, private readonly cdp: Cdp.Api) {} + + public async toDap(thread: Thread): Promise { + if (!this.protoObj.objectId) { + return { + category: 'stderr', + output: localize('queryObject.invalidObject', 'Only objects can be queried'), + }; + } + + const response = await this.cdp.Runtime.queryObjects({ + prototypeObjectId: this.protoObj.objectId, + objectGroup: 'console', + }); + + await this.cdp.Runtime.releaseObject({ objectId: this.protoObj.objectId }); + if (!response) { + return { + category: 'stderr', + output: localize('queryObject.couldNotQuery', 'Could not query the provided object'), + }; + } + + let withPreview: Cdp.Runtime.RemoteObject; + try { + withPreview = await previewThis({ + cdp: this.cdp, + args: [], + objectId: response.objects.objectId, + objectGroup: 'console', + generatePreview: true, + }); + } catch (e) { + return { + category: 'stderr', + output: localize('queryObject.errorPreview', 'Could generate preview: {0}', e.message), + }; + } + + const text = '\x1b[32mobjects: ' + previewRemoteObject(withPreview, 'repl') + '\x1b[0m'; + const variablesReference = + (await thread.replVariables.createVariableForOutput(text, [withPreview])) || 0; + + return { + category: 'stdout', + output: '', + variablesReference, + }; + } +} diff --git a/src/adapter/console/reservationQueue.test.ts b/src/adapter/console/reservationQueue.test.ts new file mode 100644 index 000000000..53ef23726 --- /dev/null +++ b/src/adapter/console/reservationQueue.test.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { expect } from 'chai'; +import { delay } from '../../common/promiseUtil'; +import { ReservationQueue } from './reservationQueue'; + +describe('ReservationQueue', () => { + let sunk: number[][]; + let queue: ReservationQueue; + + beforeEach(() => { + sunk = []; + queue = new ReservationQueue(items => { + sunk.push(items); + if (items.includes(-1)) { + queue.dispose(); + } + }); + }); + + it('enqueues sync', () => { + queue.enqueue(1); + queue.enqueue(2); + queue.enqueue(3); + expect(sunk).to.deep.equal([[1], [2], [3]]); + }); + + it('enqueues async with order', async () => { + queue.enqueue(delay(6).then(() => 1)); + queue.enqueue(delay(2).then(() => 2)); + queue.enqueue(delay(4).then(() => 3)); + await delay(10); + expect(sunk).to.deep.equal([[1, 2, 3]]); + }); + + it('bulks after async resolution', async () => { + queue.enqueue(1); + queue.enqueue(delay(6).then(() => 2)); + queue.enqueue(delay(2).then(() => 3)); + queue.enqueue(4); + queue.enqueue(delay(4).then(() => 5)); + queue.enqueue(delay(8).then(() => 6)); + await delay(10); + expect(sunk).to.deep.equal([[1], [2, 3, 4, 5], [6]]); + }); + + it('stops when disposed', async () => { + queue.enqueue(delay(2).then(() => 1)); + queue.enqueue(delay(4).then(() => -1)); + queue.enqueue(delay(6).then(() => 3)); + await delay(4); + expect(sunk).to.deep.equal([[1], [-1]]); + }); +}); diff --git a/src/adapter/console/reservationQueue.ts b/src/adapter/console/reservationQueue.ts new file mode 100644 index 000000000..0f2136cee --- /dev/null +++ b/src/adapter/console/reservationQueue.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { IDisposable } from '../../common/disposable'; +import { EventEmitter } from '../../common/events'; + +/** + * A queue that allows inserting items that are built asynchronously, while + * preserving insertion order. + */ +export class ReservationQueue implements IDisposable { + private q: Reservation[] = []; + private disposed = false; + private onDrainedEmitter = new EventEmitter(); + + /** + * Fires when the queue is drained. + */ + public readonly onDrained = this.onDrainedEmitter.event; + + /** + * Gets the current length of the queue. + */ + public get length() { + return this.q.length; + } + + constructor(private readonly sink: (items: T[]) => void) {} + + /** + * Enqueues an item or a promise for an item in the queue. + */ + public enqueue(value: T | Promise) { + if (this.disposed) { + return; + } + + this.q.push(new Reservation(value)); + if (this.q.length === 1) { + this.process(); + } + } + + /** + * Cancels processing of all pending items. + * @inheritdoc + */ + public dispose() { + this.disposed = true; + this.q = []; + } + + private async process(): Promise { + const toIndex = this.q.findIndex(r => r.value === unsettled); + if (toIndex === 0) { + await this.q[0].wait; + } else if (toIndex === -1) { + this.sink(extractResolved(this.q)); + this.q = []; + } else { + this.sink(extractResolved(this.q.slice(0, toIndex))); + this.q = this.q.slice(toIndex); + } + + if (this.q.length) { + this.process(); + } else { + this.onDrainedEmitter.fire(); + } + } +} + +const extractResolved = (list: ReadonlyArray>) => + list.map(i => i.value).filter((v): v is T => v !== rejected); + +const unsettled = Symbol('unsettled'); +const rejected = Symbol('unsettled'); + +/** + * Item in the queue. + */ +class Reservation { + /** + * Promise that is resolved when `value` is rejected or resolved. + */ + public wait?: Promise; + + /** + * Current value, or an indication that the promise is pending or rejected. + */ + public value: typeof unsettled | typeof rejected | T = unsettled; + + constructor(rawValue: T | Promise) { + if (!(rawValue instanceof Promise)) { + this.value = rawValue; + this.wait = Promise.resolve(); + } else { + this.wait = rawValue.then( + r => { + this.value = r; + }, + () => { + this.value = rejected; + }, + ); + } + } +} diff --git a/src/adapter/console/textualMessage.ts b/src/adapter/console/textualMessage.ts new file mode 100644 index 000000000..79a33f3ae --- /dev/null +++ b/src/adapter/console/textualMessage.ts @@ -0,0 +1,197 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; +import Cdp from '../../cdp/api'; +import { once } from '../../common/objUtils'; +import Dap from '../../dap/api'; +import { formatMessage } from '../messageFormat'; +import { formatAsTable, messageFormatters, previewAsObject } from '../objectPreview'; +import { AnyObject } from '../objectPreview/betterTypes'; +import { StackTrace } from '../stackTrace'; +import { Thread } from '../threads'; +import { IConsoleMessage } from './consoleMessage'; + +const localize = nls.loadMessageBundle(); + +export abstract class TextualMessage + implements IConsoleMessage { + protected readonly stackTrace = once((thread: Thread) => + this.event.stackTrace ? StackTrace.fromRuntime(thread, this.event.stackTrace, 2) : undefined, + ); + + constructor(protected readonly event: T) {} + + /** + * Returns the DAP representation of the console message. + */ + public abstract toDap(thread: Thread): Promise | Dap.OutputEventParams; + + /** + * Gets the UI location where the message was logged. + */ + protected readonly getUiLocation = once(async (thread: Thread) => { + const stackTrace = this.stackTrace(thread); + if (!stackTrace) { + return; + } + + const frames = await stackTrace.loadFrames(1); + const uiLocation = await frames[0].uiLocation; + if (!uiLocation) { + return; + } + + return { + source: await uiLocation.source.toDap(), + line: uiLocation.lineNumber, + column: uiLocation.columnNumber, + }; + }); + + /** + * Default message string formatter. Tries to create a simple string, and + * but if it can't it'll return a variable reference. + * + * Intentionally not async-await as it's a hot path in console logging. + */ + protected formatDefaultString( + thread: Thread, + args: ReadonlyArray, + includeStackInVariables = false, + ) { + const useMessageFormat = args.length > 1 && args[0].type === 'string'; + const formatResult = useMessageFormat + ? formatMessage(args[0].value, args.slice(1) as AnyObject[], messageFormatters) + : formatMessage('', args as AnyObject[], messageFormatters); + + const output = formatResult.result + '\n'; + + if (formatResult.usedAllSubs && !args.some(previewAsObject)) { + return { output }; + } else { + return this.formatComplexStringOutput(thread, output, args, includeStackInVariables); + } + } + + private async formatComplexStringOutput( + thread: Thread, + output: string, + args: ReadonlyArray, + includeStackInVariables: boolean, + ) { + if (args[0].subtype === 'error') { + await this.getUiLocation(thread); // ensure the source is loaded before decoding stack + output = await thread.replacePathsInStackTrace(output); + } + + const variablesReference = await thread.replVariables.createVariableForOutput( + output, + args, + includeStackInVariables ? this.stackTrace(thread) : undefined, + ); + + return { output: '', variablesReference }; + } +} + +export class AssertMessage extends TextualMessage { + /** + * @override + */ + public async toDap(thread: Thread): Promise { + if (this.event.args[0]?.value === 'console.assert') { + this.event.args[0].value = localize('console.assert', 'Assertion failed'); + } + + return { + category: 'stderr', + ...(await this.formatDefaultString(thread, this.event.args, /* includeStack= */ true)), + ...(await this.getUiLocation(thread)), + }; + } +} + +class DefaultMessage extends TextualMessage { + constructor( + event: Cdp.Runtime.ConsoleAPICalledEvent, + private readonly includeStack: boolean, + private readonly category: Required, + ) { + super(event); + } + /** + * @override + */ + public async toDap(thread: Thread): Promise { + return { + category: this.category, + ...(await this.formatDefaultString(thread, this.event.args, this.includeStack)), + ...(await this.getUiLocation(thread)), + }; + } +} + +export class LogMessage extends DefaultMessage { + constructor(event: Cdp.Runtime.ConsoleAPICalledEvent) { + super(event, false, 'stdout'); + } +} + +export class TraceMessage extends DefaultMessage { + constructor(event: Cdp.Runtime.ConsoleAPICalledEvent) { + super(event, true, 'stdout'); + } +} + +export class WarningMessage extends DefaultMessage { + constructor(event: Cdp.Runtime.ConsoleAPICalledEvent) { + super(event, true, 'stderr'); + } +} + +export class ErrorMessage extends DefaultMessage { + constructor(event: Cdp.Runtime.ConsoleAPICalledEvent) { + super(event, true, 'stderr'); + } +} + +export class StartGroupMessage extends TextualMessage { + /** + * @override + */ + public async toDap(thread: Thread): Promise { + return { + category: 'stdout', + group: this.event.type === 'startGroupCollapsed' ? 'startCollapsed' : 'start', + ...(await this.formatDefaultString(thread, this.event.args)), + ...(await this.getUiLocation(thread)), + }; + } +} + +export class TableMessage extends DefaultMessage { + constructor(event: Cdp.Runtime.ConsoleAPICalledEvent) { + super(event, false, 'stdout'); + } + + /** + * @override + */ + public async toDap(thread: Thread): Promise { + if (this.event.args[0]?.preview) { + return { + category: 'stdout', + output: '', + variablesReference: await thread.replVariables.createVariableForOutput( + formatAsTable(this.event.args[0].preview) + '\n', + this.event.args, + ), + ...(await this.getUiLocation(thread)), + }; + } + + return super.toDap(thread); + } +} diff --git a/src/adapter/debugAdapter.ts b/src/adapter/debugAdapter.ts index 8f18fef63..650cf3680 100644 --- a/src/adapter/debugAdapter.ts +++ b/src/adapter/debugAdapter.ts @@ -18,6 +18,7 @@ import { ITelemetryReporter } from '../telemetry/telemetryReporter'; import { IAsyncStackPolicy } from './asyncStackPolicy'; import { BreakpointManager } from './breakpoints'; import { ICompletions } from './completions'; +import { IConsole } from './console'; import { IEvaluator } from './evaluator'; import { IProfileController } from './profileController'; import { BasicCpuProfiler } from './profiling/basicCpuProfiler'; @@ -290,6 +291,7 @@ export class DebugAdapter implements IDisposable { this._services.get(ICompletions), this.launchConfig, this.breakpointManager, + this._services.get(IConsole), ); const profile = this._services.get(IProfileController); diff --git a/src/adapter/evaluator.ts b/src/adapter/evaluator.ts index 9bd48e339..23bcf70c3 100644 --- a/src/adapter/evaluator.ts +++ b/src/adapter/evaluator.ts @@ -2,11 +2,12 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import Cdp from '../cdp/api'; -import * as ts from 'typescript'; import { randomBytes } from 'crypto'; import { inject, injectable } from 'inversify'; +import * as ts from 'typescript'; +import Cdp from '../cdp/api'; import { ICdpApi } from '../cdp/connection'; +import { getSourceSuffix } from './templates'; export const returnValueStr = '$returnValue'; @@ -34,7 +35,10 @@ export interface IEvaluator { * whether we actually need to pause for a custom log evaluation, or whether * we can just send the logpoint as the breakpoint condition directly. */ - prepare(expression: string): { canEvaluateDirectly: boolean; invoke: PreparedCallFrameExpr }; + prepare( + expression: string, + isInternalScript?: boolean, + ): { canEvaluateDirectly: boolean; invoke: PreparedCallFrameExpr }; /** * Evaluates the expression on a call frame. This allows @@ -42,18 +46,23 @@ export interface IEvaluator { */ evaluate( params: Cdp.Debugger.EvaluateOnCallFrameParams, + isInternalScript?: boolean, ): Promise; /** * Evaluates the expression the runtime. */ - evaluate(params: Cdp.Runtime.EvaluateParams): Promise; + evaluate( + params: Cdp.Runtime.EvaluateParams, + isInternalScript?: boolean, + ): Promise; /** * Evaluates the expression the runtime or call frame. */ evaluate( params: Cdp.Runtime.EvaluateParams | Cdp.Debugger.EvaluateOnCallFrameParams, + isInternalScript?: boolean, ): Promise; /** @@ -123,11 +132,20 @@ export class Evaluator implements IEvaluator { */ public evaluate( params: Cdp.Debugger.EvaluateOnCallFrameParams, + isInternalScript?: boolean, ): Promise; - public evaluate(params: Cdp.Runtime.EvaluateParams): Promise; + public evaluate( + params: Cdp.Runtime.EvaluateParams, + isInternalScript?: boolean, + ): Promise; public async evaluate( params: Cdp.Debugger.EvaluateOnCallFrameParams | Cdp.Runtime.EvaluateParams, + isInternalScript = true, ) { + if (isInternalScript) { + params = { ...params, expression: params.expression + getSourceSuffix() }; + } + // no call frame means there will not be any relevant $returnValue to reference if (!('callFrameId' in params)) { return this.cdp.Runtime.evaluate(params); @@ -146,14 +164,14 @@ export class Evaluator implements IEvaluator { if (objectId) { await this.cdp.Runtime.callFunctionOn({ objectId, - functionDeclaration: `function() { globalThis.${hoistedVar} = this; ${dehoist}; }`, + functionDeclaration: `function() { globalThis.${hoistedVar} = this; ${dehoist}; ${getSourceSuffix()} }`, }); } else { await this.cdp.Runtime.evaluate({ - expression: ` - globalThis.${hoistedVar} = ${JSON.stringify(this.returnValue?.value)}; - ${dehoist}; - `, + expression: + `globalThis.${hoistedVar} = ${JSON.stringify(this.returnValue?.value)};` + + `${dehoist};` + + getSourceSuffix(), }); } } diff --git a/src/adapter/messageFormat.ts b/src/adapter/messageFormat.ts index 8940927ec..8c8feff3b 100644 --- a/src/adapter/messageFormat.ts +++ b/src/adapter/messageFormat.ts @@ -15,6 +15,10 @@ const maxMessageFormatLength = 10000; export type Formatters = Map string>; function tokenizeFormatString(format: string, formatterNames: string[]): FormatToken[] { + if (!format.includes('%')) { + return [{ type: 'string', value: format }]; // happy path, no formatting needed + } + const tokens: FormatToken[] = []; function addStringToken(str: string) { diff --git a/src/adapter/notes.js b/src/adapter/notes.js new file mode 100644 index 000000000..0dcc3ce9c --- /dev/null +++ b/src/adapter/notes.js @@ -0,0 +1,7 @@ +((defaultValue) => { + try { + return global.customDebuggerDescription(this, defaultValue); + } catch (e) { + return e.stack || e.message || String(e); + } +})() diff --git a/src/adapter/objectPreview/betterTypes.ts b/src/adapter/objectPreview/betterTypes.ts index 6a324e7bc..c6745e1d4 100644 --- a/src/adapter/objectPreview/betterTypes.ts +++ b/src/adapter/objectPreview/betterTypes.ts @@ -105,6 +105,7 @@ export type AnyObject = | RegExpObj | FunctionObj | StringObj + | NumberObj | BigintObj | UndefinedObj | NullObj; diff --git a/src/adapter/resourceProvider/basicResourceProvider.ts b/src/adapter/resourceProvider/basicResourceProvider.ts index a62a59993..63c0e4cb4 100644 --- a/src/adapter/resourceProvider/basicResourceProvider.ts +++ b/src/adapter/resourceProvider/basicResourceProvider.ts @@ -2,6 +2,7 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +import dataUriToBuffer from 'data-uri-to-buffer'; import got, { OptionsOfTextResponseBody, RequestError } from 'got'; import { inject, injectable } from 'inversify'; import { CancellationToken } from 'vscode'; @@ -23,8 +24,11 @@ export class BasicResourceProvider implements IResourceProvider { cancellationToken: CancellationToken = NeverCancelled, headers?: { [key: string]: string }, ): Promise> { - if (url.startsWith('data:')) { - return this.resolveDataUri(url); + try { + const r = dataUriToBuffer(url); + return { ok: true, body: r.toString('utf-8'), statusCode: 200 }; + } catch { + // assume it's a remote url } const absolutePath = isAbsolute(url) ? url : fileUrlToAbsolutePath(url); @@ -103,24 +107,6 @@ export class BasicResourceProvider implements IResourceProvider { } } - private resolveDataUri(url: string): Response { - const prefix = url.substring(0, url.indexOf(',')); - const match = prefix.match(/data:[^;]*(;[^;]*)?(;[^;]*)?(;[^;]*)?/); - if (!match) { - return { - ok: false, - statusCode: 500, - error: new Error(`Malformed data url prefix '${prefix}'`), - body: url, - }; - } - - const params = new Set(match.slice(1)); - const data = url.substring(prefix.length + 1); - const result = Buffer.from(data, params.has(';base64') ? 'base64' : undefined).toString(); - return { ok: true, statusCode: 200, body: result }; - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars protected createHttpOptions(url: string): Promise { return Promise.resolve({}); diff --git a/src/adapter/scriptSkipper/implementation.ts b/src/adapter/scriptSkipper/implementation.ts index f5e91f24f..5bbcc1647 100644 --- a/src/adapter/scriptSkipper/implementation.ts +++ b/src/adapter/scriptSkipper/implementation.ts @@ -23,6 +23,7 @@ import { SourceContainer, SourceFromMap, } from '../sources'; +import { getSourceSuffix } from '../templates'; interface ISharedSkipToggleEvent { rootTargetId: string; @@ -267,7 +268,7 @@ export class ScriptSkipper { private async _initNodeInternals(target: ITarget): Promise { if (target.type() === 'node' && this._nodeInternalsGlobs && !this._allNodeInternals) { const evalResult = await this.cdp.Runtime.evaluate({ - expression: "require('module').builtinModules", + expression: "require('module').builtinModules" + getSourceSuffix(), returnByValue: true, includeCommandLineAPI: true, }); diff --git a/src/adapter/smartStepping.ts b/src/adapter/smartStepping.ts index 9a1bb628c..100eafbfc 100644 --- a/src/adapter/smartStepping.ts +++ b/src/adapter/smartStepping.ts @@ -9,7 +9,7 @@ import { StackFrame } from './stackTrace'; import { ExpectedPauseReason, IPausedDetails, PausedReason, StepDirection } from './threads'; export async function shouldSmartStepStackFrame(stackFrame: StackFrame): Promise { - const uiLocation = await stackFrame.uiLocation(); + const uiLocation = await stackFrame.uiLocation; if (!uiLocation) { return false; } diff --git a/src/adapter/something.js b/src/adapter/something.js new file mode 100644 index 000000000..2a0aa15dc --- /dev/null +++ b/src/adapter/something.js @@ -0,0 +1,9 @@ +function _generatedCode(defaultValue) { + try { + return (function (def) { + return global.customDebuggerDescription(this, def) + })(defaultValue); + } catch (e) { + return e.stack || e.message || String(e); + } +} diff --git a/src/adapter/sources.ts b/src/adapter/sources.ts index f4582bff8..f6816cefa 100644 --- a/src/adapter/sources.ts +++ b/src/adapter/sources.ts @@ -10,6 +10,7 @@ import { URL } from 'url'; import * as nls from 'vscode-nls'; import Cdp from '../cdp/api'; import { MapUsingProjection } from '../common/datastructure/mapUsingProjection'; +import { EventEmitter } from '../common/events'; import { ILogger, LogTag } from '../common/logging'; import { once } from '../common/objUtils'; import { forceForwardSlashes, isSubdirectoryOf, properResolve } from '../common/pathUtils'; @@ -60,6 +61,14 @@ type ContentGetter = () => Promise; // Each source map has a number of compiled sources referncing it. type SourceMapData = { compiled: Set; map?: SourceMap; loaded: Promise }; +export const enum SourceConstants { + /** + * Extension of evaluated sources internal to the debugger. Sources with + * this suffix will be ignored when displaying sources or stacktracees. + */ + InternalExtension = '.cdp', +} + export type SourceMapTimeouts = { // This is a source map loading delay used for testing. load: number; @@ -449,7 +458,9 @@ export class SourceContainer { */ public scriptsById: Map = new Map(); + private onScriptEmitter = new EventEmitter