Skip to content

Commit

Permalink
feat: add overrideProperties option to debugAsync
Browse files Browse the repository at this point in the history
BREAKING CHANGE by default variables will override context properties; rename "babel" entry point to "babel-plugin"
  • Loading branch information
Dmitry Steblyuk committed Jul 23, 2021
1 parent add7f71 commit 4dccaf1
Show file tree
Hide file tree
Showing 16 changed files with 180 additions and 107 deletions.
39 changes: 27 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Async Debugger
# AsyncDebugger

[![npm](https://img.shields.io/npm/v/async-debugger/latest.svg)](https://www.npmjs.com/package/async-debugger)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
Expand All @@ -19,11 +19,11 @@ Thus it is impossible to test any async logic in browser console when debugging/

## Solution

Async Debugger pauses only async functions that are being debugged (have reached debugger statement) and does not block JS execution.
AsyncDebugger pauses async functions when they reach the debugger statement and does not block async JS execution.

The access to the variables in the scope is ensured with a **babel plugin** it implements.
The access to the variables in the scope is ensured by the **babel plugin** it implements.

For example, this code below:
For example, the code below:

```javascript
const a = 'abc';
Expand All @@ -41,20 +41,21 @@ let b = 123;
await debugAsync({a, b});
```

`debugAsync` in its turn will expose the variables to global object and REPL console in Node.
`debugAsync` in its turn will expose the variables to the global object and REPL console in Node.

## Example
## Examples

### Node
### In Node

Create a file `server.js` as follows:

```javascript
const {createServer} = require('http');

const getAllUsers = () => Promise.resolve(
[{id: 0, name: 'Luke'}, {id: 1, name: 'Leia'}, {id: 2, name: 'Chewie'}]
);
const getAllUsers = async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
return [{id: 0, name: 'Alpha'}, {id: 1, name: 'Bravo'}, {id: 2, name: 'Charlie'}];
};
const getUserById = async (id) => {
const allUsers = await getAllUsers();
return allUsers.find((user) => user.id === id);
Expand All @@ -78,12 +79,26 @@ const server = createServer(async (request, response) => {
server.listen(3000, () => console.log('Listening on', server.address()));
```

To enable Async Debugger you can run it like this:
To enable AsyncDebugger you can run it like this:

```bash
node --require async-debugger/register --experimental-repl-await server.js
# Or with `ts-node`:
# NODE_OPTIONS="--experimental-repl-await" ts-node --require async-debugger/register server.ts
```

Then make a GET request to `http://localhost:3000/users/2` and use the REPL launched automatically in your terminal:
Then make a GET request to `http://localhost:3000/users/1` and use the REPL launched automatically in your terminal:

![alt text](assets/repl.png)

Alternatively you can add `--inspect` flag when running node and use AsyncDebugger in a browser console. More in https://nodejs.org/en/docs/guides/debugging-getting-started/

### In Browser

Add Babel for javascript/typescript files transpilation and include `async-debugger/babel-plugin` to the list of plugins.

On `await 'debugger'` statement in the async function it will be paused and you will be able to debug it in the browser console.

Available plugin options:

- `debugAsyncDeclarationHeader` - a string that will be inserted in the beginning of any file that has `await 'debugger'` statements. It should declare `__debugAsync__` function. This step will be skipped if `__debugAsync__` is already declared in the debugged scope. You can also set `debugAsyncDeclarationHeader` to an empty string and define `__debugAsync__` in the global scope manually. Default value is `const {debugAsync: __debugAsync__} = require('async-debugger');`.
Binary file modified assets/repl.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
4 changes: 2 additions & 2 deletions browser/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"main": "../lib/babel-plugin.js",
"types": "../lib/babel-plugin.d.ts"
"main": "../lib/debug-async-browser.js",
"types": "../lib/debug-async-browser.d.ts"
}
4 changes: 2 additions & 2 deletions core/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"main": "../lib/debug-async-node.js",
"types": "../lib/debug-async-node.d.ts"
"main": "../lib/index.js",
"types": "../lib/index.d.ts"
}
4 changes: 2 additions & 2 deletions node/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"main": "../lib/babel-plugin.js",
"types": "../lib/babel-plugin.d.ts"
"main": "../lib/debug-async-node.js",
"types": "../lib/debug-async-node.d.ts"
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
"description": "Run async code at breakpoints in Browser and Node.",
"keywords": [
"async",
"debugger",
"repl",
"debugging",
"development"
"babel",
"plugin",
"babel-plugin"
],
"main": "./lib/debug-async-node.js",
"browser": "./lib/debug-async-browser.js",
Expand All @@ -15,7 +17,7 @@
"repository": "https://github.com/dmitrysteblyuk/async-debugger.git",
"license": "MIT",
"files": [
"babel",
"babel-plugin",
"browser",
"core",
"lib",
Expand Down
2 changes: 1 addition & 1 deletion register.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require('@babel/register')({
sourceType: 'unambiguous',
plugins: ['async-debugger/babel'],
plugins: ['async-debugger/babel-plugin'],
extensions: ['.es6', '.es', '.jsx', '.js', '.mjs', '.ts', '.tsx']
});
24 changes: 12 additions & 12 deletions src/babel-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import * as types from '@babel/types';

const DEBUGGER_LITERAL = 'debugger';
const DEBUG_ASYNC_FUNCTION = '__debugAsync__';
const IMPORT_DEBUG_ASYNC_LINE_DEFAULT = (
const DEBUG_ASYNC_DECLARATION_DEFAULT = (
`const {debugAsync: ${DEBUG_ASYNC_FUNCTION}} = require('async-debugger');`
);

export = (
_api: {},
{
importDebugAsyncLine = IMPORT_DEBUG_ASYNC_LINE_DEFAULT
debugAsyncDeclarationHeader = DEBUG_ASYNC_DECLARATION_DEFAULT
}: {
importDebugAsyncLine?: string;
debugAsyncDeclarationHeader?: string;
} = {}
): core.PluginObj => {
const awaitDebuggerExpressions = new Set<core.NodePath<types.AwaitExpression>>();
Expand All @@ -29,24 +29,24 @@ export = (
}
},
post(file) {
let requiresPauseToDebug = false;
let requiresDebugAsyncDeclaration = false;
for (const path of awaitDebuggerExpressions) {
if (handleAwaitDebuggerExpression(path)) {
requiresPauseToDebug = true;
requiresDebugAsyncDeclaration = true;
}
}
if (requiresPauseToDebug) {
addPauseToDebugRequire(file.path, importDebugAsyncLine);
if (requiresDebugAsyncDeclaration) {
insertDebugAsyncDeclarationHeader(file.path, debugAsyncDeclarationHeader);
}
}
};
};

function addPauseToDebugRequire(
function insertDebugAsyncDeclarationHeader(
path: core.NodePath<types.Program>,
importDebugAsyncLine: string
debugAsyncDeclarationHeader: string
) {
const result = parseSync(importDebugAsyncLine);
const result = parseSync(debugAsyncDeclarationHeader);
if (result === null) {
return;
}
Expand All @@ -65,7 +65,7 @@ function handleAwaitDebuggerExpression(
return false;
});
const set = new Set(variableNames);
const requiresPauseToDebug = !set.has(DEBUG_ASYNC_FUNCTION);
const requiresDebugAsyncDeclaration = !set.has(DEBUG_ASYNC_FUNCTION);
variableNames = [...set];

path.replaceWith(
Expand All @@ -83,5 +83,5 @@ function handleAwaitDebuggerExpression(
path.node
])
);
return requiresPauseToDebug;
return requiresDebugAsyncDeclaration;
}
29 changes: 26 additions & 3 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
export * from './create-debugger-api';
export * from './extend-context';
export * from './types';
export const API_NAMESPACE = 'AsyncDebugger';

export function getLogger(
{
info = console.info.bind(console, `[${API_NAMESPACE}]`),
warn = console.warn.bind(console, `[${API_NAMESPACE}]`),
error = console.error.bind(console, `[${API_NAMESPACE}]`)
}: Partial<Logger> = {}
) {
return {info, warn, error};
}

export interface Bindings {
[variableName: string]: () => unknown;
}
export interface Logger {
info: ((...args: unknown[]) => void) | null;
warn: ((...args: unknown[]) => void) | null;
error: ((...args: unknown[]) => void) | null;
}
export interface DebugAsyncCommonOptions {
contexts?: object[];
logger?: Partial<Logger>;
apiNamespace?: string;
overrideProperties?: boolean;
}
26 changes: 16 additions & 10 deletions src/debug-async-browser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {createDebuggerAPI} from './create-debugger-api';
import type {Bindings, DebugAsyncCommonOptions, Logger} from './types';
import {prepareDebugAsync} from './prepare-debug-async';
import {API_NAMESPACE, Bindings, DebugAsyncCommonOptions, getLogger} from './core';

export const debugAsync = createDebugAsyncBrowser();
export interface DebugAsyncBrowserOptions extends DebugAsyncCommonOptions {}
Expand All @@ -8,8 +8,9 @@ export function createDebugAsyncBrowser(
{
defaults: {
contexts: contextsDefault = [globalThis],
logger: defaultLogger = {},
apiNamespace: apiNamespaceDefault = 'AsyncDebugger'
overrideProperties: overridePropertiesDefault = true,
apiNamespace: apiNamespaceDefault = API_NAMESPACE,
logger: loggerDefault
} = {}
}: {
defaults?: DebugAsyncBrowserOptions
Expand All @@ -27,21 +28,26 @@ export function createDebugAsyncBrowser(
}
const {
contexts = contextsDefault,
overrideProperties = overridePropertiesDefault,
apiNamespace = apiNamespaceDefault,
logger: {info = console.info, warn = console.warn} = defaultLogger
logger: defaultLogger = loggerDefault
} = options;
const logger: Logger = {info, warn};
const {resultPromise, applyToContexts} = (
createDebuggerAPI(bindings, apiNamespace, logger)
const logger = getLogger(defaultLogger);
const {resultPromise, applyToContext, startMessage, stopMessage} = (
prepareDebugAsync(bindings, overrideProperties, apiNamespace, logger)
);
const teardown = applyToContexts(contexts);
logger.info?.(startMessage);
const teardowns = contexts.map(applyToContext);

try {
isBeingDebugged = true;
return await resultPromise;
} finally {
for (const teardown of teardowns) {
teardown();
}
isBeingDebugged = false;
teardown();
logger.info?.(stopMessage);
}
};
}
38 changes: 23 additions & 15 deletions src/debug-async-node.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import {start as startRepl, ReplOptions} from 'repl';
import {createDebuggerAPI} from './create-debugger-api';
import type {Bindings, DebugAsyncCommonOptions, Logger} from './types';
import {start as startREPL, ReplOptions} from 'repl';
import {prepareDebugAsync} from './prepare-debug-async';
import {API_NAMESPACE, Bindings, DebugAsyncCommonOptions, getLogger} from './core';

export const debugAsync = createDebugAsyncNode();
export interface DebugAsyncNodeOptions extends DebugAsyncCommonOptions {
replOptions?: ReplOptions;
}

export function createDebugAsyncNode(
{
defaults: {
contexts: contextsDefault = [globalThis],
logger: defaultLogger = {},
apiNamespace: apiNamespaceDefault = 'AsyncDebugger',
overrideProperties: overridePropertiesDefault = true,
apiNamespace: apiNamespaceDefault = API_NAMESPACE,
logger: loggerDefault,
replOptions: replOptionsDefault
} = {}
}: {
Expand All @@ -31,32 +31,40 @@ export function createDebugAsyncNode(
}
const {
contexts = contextsDefault,
overrideProperties = overridePropertiesDefault,
apiNamespace = apiNamespaceDefault,
logger: {info = console.info, warn = console.warn} = defaultLogger,
logger: defaultLogger = loggerDefault,
replOptions = replOptionsDefault
} = options;
const logger: Logger = {info, warn};
const {resultPromise, applyToContexts, api} = (
createDebuggerAPI(bindings, apiNamespace, logger)
const logger = getLogger(defaultLogger);
const {resultPromise, applyToContext, api, startMessage, stopMessage} = (
prepareDebugAsync(bindings, overrideProperties, apiNamespace, logger)
);
const repl = startRepl(replOptions);
logger.info?.(startMessage);

const teardowns = contexts.map(applyToContext);
const repl = startREPL(replOptions);
const exitPromise = new Promise<void>((resolve) => {
repl.once('exit', () => {
api.resumeExecution();
resolve();
});
});
const teardown = applyToContexts([...contexts, repl.context]);
if (!contexts.includes(globalThis)) {
applyToContext(repl.context);
}

try {
isBeingDebugged = true;
const result = await resultPromise;
return result;
return await resultPromise;
} finally {
teardown();
for (const teardown of teardowns) {
teardown();
}
repl.close();
await exitPromise;
isBeingDebugged = false;
logger.info?.(stopMessage);
}
};
}
Loading

0 comments on commit 4dccaf1

Please sign in to comment.