Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add pathMapping to node.js #1015

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions OPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"${workspaceFolder}/**/*.js",
"!**/node_modules/**"
]</pre></code><h4>outputCapture</h4><p>From where to capture output messages: the default debug API if set to <code>console</code>, or stdout/stderr streams if set to <code>std</code>.</p>
<h5>Default value:</h4><pre><code>"console"</pre></code><h4>pauseForSourceMap</h4><p>Whether to wait for source maps to load for each incoming script. This has a performance overhead, and might be safely disabled when running off of disk, so long as <code>rootPath</code> is not disabled.</p>
<h5>Default value:</h4><pre><code>"console"</pre></code><h4>pathMapping</h4><p>A mapping of folders from one path to another. To resolve scripts to their original locations. Typical use is to map scripts in <code>node_modules</code> to their sources that locate in another folder.</p>
<h5>Default value:</h4><pre><code>{}</pre></code><h4>pauseForSourceMap</h4><p>Whether to wait for source maps to load for each incoming script. This has a performance overhead, and might be safely disabled when running off of disk, so long as <code>rootPath</code> is not disabled.</p>
<h5>Default value:</h4><pre><code>false</pre></code><h4>port</h4><p>Debug port to attach to. Default is 9229.</p>
<h5>Default value:</h4><pre><code>9229</pre></code><h4>processId</h4><p>ID of process to attach to.</p>
<h5>Default value:</h4><pre><code>undefined</pre></code><h4>remoteRoot</h4><p>Absolute path to the remote directory containing the program.</p>
Expand Down Expand Up @@ -68,7 +69,8 @@
"${workspaceFolder}/**/*.js",
"!**/node_modules/**"
]</pre></code><h4>outputCapture</h4><p>From where to capture output messages: the default debug API if set to <code>console</code>, or stdout/stderr streams if set to <code>std</code>.</p>
<h5>Default value:</h4><pre><code>"console"</pre></code><h4>pauseForSourceMap</h4><p>Whether to wait for source maps to load for each incoming script. This has a performance overhead, and might be safely disabled when running off of disk, so long as <code>rootPath</code> is not disabled.</p>
<h5>Default value:</h4><pre><code>"console"</pre></code><h4>pathMapping</h4><p>A mapping of folders from one path to another. To resolve scripts to their original locations. Typical use is to map scripts in <code>node_modules</code> to their sources that locate in another folder.</p>
<h5>Default value:</h4><pre><code>{}</pre></code><h4>pauseForSourceMap</h4><p>Whether to wait for source maps to load for each incoming script. This has a performance overhead, and might be safely disabled when running off of disk, so long as <code>rootPath</code> is not disabled.</p>
<h5>Default value:</h4><pre><code>false</pre></code><h4>profileStartup</h4><p>If true, will start profiling as soon as the process launches</p>
<h5>Default value:</h4><pre><code>false</pre></code><h4>program</h4><p>Absolute path to the program. Generated value is guessed by looking at package.json and opened files. Edit this attribute.</p>
<h5>Default value:</h4><pre><code>""</pre></code><h4>remoteRoot</h4><p>Absolute path to the remote directory containing the program.</p>
Expand Down Expand Up @@ -116,7 +118,8 @@
"${workspaceFolder}/**/*.js",
"!**/node_modules/**"
]</pre></code><h4>outputCapture</h4><p>From where to capture output messages: the default debug API if set to <code>console</code>, or stdout/stderr streams if set to <code>std</code>.</p>
<h5>Default value:</h4><pre><code>"console"</pre></code><h4>pauseForSourceMap</h4><p>Whether to wait for source maps to load for each incoming script. This has a performance overhead, and might be safely disabled when running off of disk, so long as <code>rootPath</code> is not disabled.</p>
<h5>Default value:</h4><pre><code>"console"</pre></code><h4>pathMapping</h4><p>A mapping of folders from one path to another. To resolve scripts to their original locations. Typical use is to map scripts in <code>node_modules</code> to their sources that locate in another folder.</p>
<h5>Default value:</h4><pre><code>{}</pre></code><h4>pauseForSourceMap</h4><p>Whether to wait for source maps to load for each incoming script. This has a performance overhead, and might be safely disabled when running off of disk, so long as <code>rootPath</code> is not disabled.</p>
<h5>Default value:</h4><pre><code>false</pre></code><h4>remoteRoot</h4><p>Absolute path to the remote directory containing the program.</p>
<h5>Default value:</h4><pre><code>null</pre></code><h4>resolveSourceMapLocations</h4><p>A list of minimatch patterns for locations (folders and URLs) in which source maps can be used to resolve local files. This can be used to avoid incorrectly breaking in external source mapped code. Patterns can be prefixed with &quot;!&quot; to exclude them. May be set to an empty array or null to avoid restriction.</p>
<h5>Default value:</h4><pre><code>[
Expand Down Expand Up @@ -162,7 +165,8 @@
<h5>Default value:</h4><pre><code>[
"${workspaceFolder}/out/**/*.js"
]</pre></code><h4>outputCapture</h4><p>From where to capture output messages: the default debug API if set to <code>console</code>, or stdout/stderr streams if set to <code>std</code>.</p>
<h5>Default value:</h4><pre><code>"console"</pre></code><h4>pauseForSourceMap</h4><p>Whether to wait for source maps to load for each incoming script. This has a performance overhead, and might be safely disabled when running off of disk, so long as <code>rootPath</code> is not disabled.</p>
<h5>Default value:</h4><pre><code>"console"</pre></code><h4>pathMapping</h4><p>A mapping of folders from one path to another. To resolve scripts to their original locations. Typical use is to map scripts in <code>node_modules</code> to their sources that locate in another folder.</p>
<h5>Default value:</h4><pre><code>{}</pre></code><h4>pauseForSourceMap</h4><p>Whether to wait for source maps to load for each incoming script. This has a performance overhead, and might be safely disabled when running off of disk, so long as <code>rootPath</code> is not disabled.</p>
<h5>Default value:</h4><pre><code>false</pre></code><h4>remoteRoot</h4><p>Absolute path to the remote directory containing the program.</p>
<h5>Default value:</h4><pre><code>null</pre></code><h4>rendererDebugOptions</h4><p>Chrome launch options used when attaching to the renderer process, with <code>debugWebviews</code> or <code>debugWebWorkerHost</code>.</p>
<h5>Default value:</h4><pre><code>{}</pre></code><h4>resolveSourceMapLocations</h4><p>A list of minimatch patterns for locations (folders and URLs) in which source maps can be used to resolve local files. This can be used to avoid incorrectly breaking in external source mapped code. Patterns can be prefixed with &quot;!&quot; to exclude them. May be set to an empty array or null to avoid restriction.</p>
Expand Down
86 changes: 74 additions & 12 deletions src/adapter/breakpointPredictor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ISearchStrategy } from '../common/sourceMaps/sourceMapRepository';
import { ISourcePathResolver } from '../common/sourcePathResolver';
import { getOptimalCompiledPosition } from '../common/sourceUtils';
import * as urlUtils from '../common/urlUtils';
import { AnyLaunchConfiguration } from '../configuration';
import { AnyLaunchConfiguration, PathMapping } from '../configuration';
import Dap from '../dap/api';
import { logPerf } from '../telemetry/performance';

Expand All @@ -26,7 +26,12 @@ export interface IWorkspaceLocation {
columnNumber: number; // 1-based
}

type DiscoveredMetadata = ISourceMapMetadata & { sourceUrl: string; resolvedPath: string };
type DiscoveredMetadata = ISourceMapMetadata & {
sourceUrl: string;
resolvedPath: string;
// is the meta for source map or path map
type: 'source' | 'path';
};
type MetadataMap = Map<string, Set<DiscoveredMetadata>>;

const longPredictionWarning = 10 * 1000;
Expand Down Expand Up @@ -162,6 +167,7 @@ export class BreakpointsPredictor implements IBreakpointsPredictor {
private readonly longParseEmitter = new EventEmitter<void>();
private sourcePathToCompiled?: Promise<MetadataMap>;
private cache?: CorrelatedCache<number, { sourceUrl: string; resolvedPath: string }[]>;
private readonly pathMapping: PathMapping;

/**
* Event that fires if it takes a long time to predict sourcemaps.
Expand All @@ -182,6 +188,8 @@ export class BreakpointsPredictor implements IBreakpointsPredictor {
path.join(launchConfig.__workspaceCachePath, 'bp-predict.json'),
);
}

this.pathMapping = launchConfig.pathMapping;
}

private async createInitialMapping(): Promise<MetadataMap> {
Expand Down Expand Up @@ -218,8 +226,9 @@ export class BreakpointsPredictor implements IBreakpointsPredictor {
try {
await this.repo.streamChildrenWithSourcemaps(this.outFiles, async metadata => {
const cached = await this.cache?.lookup(metadata.compiledPath, metadata.mtime);

if (cached) {
cached.forEach(c => addDiscovery({ ...c, ...metadata }));
cached.forEach(c => addDiscovery({ ...c, ...metadata, type: 'source' }));
return;
}

Expand All @@ -240,7 +249,12 @@ export class BreakpointsPredictor implements IBreakpointsPredictor {
continue;
}

const discovery = { ...metadata, resolvedPath, sourceUrl: url };
const discovery: DiscoveredMetadata = {
...metadata,
resolvedPath,
sourceUrl: url,
type: 'source',
};
discovered.push(discovery);
addDiscovery(discovery);
}
Expand All @@ -255,6 +269,40 @@ export class BreakpointsPredictor implements IBreakpointsPredictor {
this.logger.warn(LogTag.RuntimeException, 'Error reading sourcemaps from disk', { error });
}

// search for files that path mapping affects, and add break point predictions for them
// if they don't have source mapping data
if (this.pathMapping && Object.keys(this.pathMapping).length > 0) {
await this.repo.streamChildrenWithPathMaps(
this.pathMapping,
async metadata => {
const cached = await this.cache?.lookup(metadata.compiledPath, -1);
if (cached) {
cached.forEach(c => addDiscovery({ ...c, ...metadata, type: 'path' }));
return;
}

const sourceMapped = await this.cache?.lookup(metadata.compiledPath);

// If a file that has sourcemap meta info, then use that for breakpoint prediction, and not
// the path map info
if (!sourceMapped) {
const sourceUrl = path.relative(metadata.compiledPath, metadata.sourceMapUrl);
addDiscovery({
compiledPath: metadata.compiledPath,
resolvedPath: metadata.sourceMapUrl,
sourceUrl,
sourceMapUrl: '',
type: 'path',
});
}
},
// search for normal .js files and node's ES6 module .mjs
// other extensions like .ts etc should have sourcemap info and should not
// be subjected to breakpoint prediction with path maps.
'**/*.{js,mjs}',
);
}

clearTimeout(warnLongRuntime);
return sourcePathToCompiled;
}
Expand Down Expand Up @@ -288,14 +336,20 @@ export class BreakpointsPredictor implements IBreakpointsPredictor {
return;
}

const sourceMaps = await Promise.all(
[...set].map(metadata =>
this.sourceMapFactory
.load(metadata)
.then(map => ({ map, metadata }))
.catch(() => undefined),
),
);
const byPathMap = [...set].filter(s => s.type === 'path');

const bySourceMap = [...set].filter(s => s.type === 'source');
const sourceMaps =
bySourceMap.length > 0
? await Promise.all(
bySourceMap.map(metadata =>
this.sourceMapFactory
.load(metadata)
.then(map => ({ map, metadata }))
.catch(() => undefined),
),
)
: [];

for (const b of params.breakpoints ?? []) {
const key = `${params.source.path}:${b.line}:${b.column || 1}`;
Expand All @@ -306,6 +360,14 @@ export class BreakpointsPredictor implements IBreakpointsPredictor {
const locations: IWorkspaceLocation[] = [];
this.predictedLocations.set(key, locations);

for (const pathMapMeta of byPathMap) {
locations.push({
absolutePath: pathMapMeta.compiledPath,
lineNumber: b.line,
columnNumber: b.column || 1,
});
}

for (const sourceMapLoad of sourceMaps) {
if (!sourceMapLoad) {
continue;
Expand Down
13 changes: 11 additions & 2 deletions src/build/generate-contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,12 @@ const baseConfigurationAttributes: ConfigurationAttributes<IBaseConfiguration> =
default: [],
description: refString('base.cascadeTerminateToConfigurations.label'),
},
pathMapping: {
type: 'object',
additionalProperties: { type: 'string' },
description: refString('browser.pathMapping.description'),
default: {},
},
};

/**
Expand Down Expand Up @@ -357,6 +363,10 @@ const nodeBaseConfigurationAttributes: ConfigurationAttributes<INodeBaseConfigur
description: refString('node.versionHint.description'),
default: 12,
},
pathMapping: {
...baseConfigurationAttributes.pathMapping,
markdownDescription: refString('node.pathMapping.description'),
},
};

/**
Expand Down Expand Up @@ -679,9 +689,8 @@ const chromiumBaseConfigurationAttributes: ConfigurationAttributes<IChromiumBase
default: true,
},
pathMapping: {
type: 'object',
...baseConfigurationAttributes.pathMapping,
description: refString('browser.pathMapping.description'),
default: {},
},
webRoot: {
type: 'string',
Expand Down
2 changes: 2 additions & 0 deletions src/build/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ const strings = {
'node.sourceMapPathOverrides.description':
'A set of mappings for rewriting the locations of source files from what the sourcemap says, to their locations on disk.',
'node.sourceMaps.description': 'Use JavaScript source maps (if they exist).',
'node.pathMapping.description':
'A mapping of folders from one path to another. To resolve scripts to their original locations. Typical use is to map scripts in `node_modules` to their sources that locate in another folder.',
'node.stopOnEntry.description': 'Automatically stop program after launch.',
'node.timeout.description':
'Retry for this number of milliseconds to connect to Node.js. Default is 10000 ms.',
Expand Down
38 changes: 35 additions & 3 deletions src/common/sourceMaps/codeSearchStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { injectable } from 'inversify';
import type * as vscodeType from 'vscode';
import { LogTag, ILogger } from '../logging';
import { PathMapping } from '../../configuration';
import { FileGlobList } from '../fileGlobList';
import { ILogger, LogTag } from '../logging';
import { forceForwardSlashes } from '../pathUtils';
import { NodeSearchStrategy } from './nodeSearchStrategy';
import { ISourceMapMetadata } from './sourceMap';
import { createMetadataForFile, ISearchStrategy } from './sourceMapRepository';
import { injectable } from 'inversify';
import { FileGlobList } from '../fileGlobList';

/**
* A source map repository that uses VS Code's proposed search API to
Expand Down Expand Up @@ -46,6 +47,37 @@ export class CodeSearchStrategy implements ISearchStrategy {
return this.nodeStrategy.streamAllChildren(files, onChild);
}

/**
* @inheritdoc
*/
public async streamChildrenWithPathMaps<T>(
pathMapping: PathMapping,
onChild: (child: Required<ISourceMapMetadata>) => Promise<T>,
pattern = '**',
): Promise<T[]> {
const todo: Promise<T>[] = [];

// process pathMapping config
const mappedPaths = Object.keys(pathMapping);
for (const path of mappedPaths) {
const files = await this.vscode.workspace.findFiles(
new this.vscode.RelativePattern(path, pattern),
);
for (const file of files) {
const sourceMapUrl = file.path.replace(path, pathMapping[path]);
todo.push(
onChild({
compiledPath: file.path,
mtime: -1,
sourceMapUrl,
}),
);
}
}

return (await Promise.all(todo)).filter((t): t is T => t !== undefined);
}

/**
* @inheritdoc
*/
Expand Down
8 changes: 5 additions & 3 deletions src/common/sourceMaps/mtimeCorrelatedCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
*--------------------------------------------------------*/
import { mkdirSync } from 'fs';
import { dirname } from 'path';
import { debounce } from '../objUtils';
import { IDisposable } from '../disposable';
import { readfile, writeFile } from '../fsUtils';
import { debounce } from '../objUtils';

export class CorrelatedCache<C, V> implements IDisposable {
private cacheData?: Promise<{ [key: string]: { correlation: C; value: V } }>;
Expand All @@ -26,10 +26,12 @@ export class CorrelatedCache<C, V> implements IDisposable {
/**
* Gets the value from the map if it exists and the correlation matches.
*/
public async lookup(key: string, correlation: C): Promise<V | undefined> {
public async lookup(key: string, correlation?: C): Promise<V | undefined> {
const data = await this.getData();
const entry = data[key];
return entry && entry.correlation === correlation ? entry.value : undefined;
return entry && (correlation === undefined || entry.correlation === correlation)
? entry.value
: undefined;
}

/**
Expand Down
22 changes: 16 additions & 6 deletions src/common/sourceMaps/nodeSearchStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { ISourceMapMetadata } from './sourceMap';
import { ISearchStrategy, createMetadataForFile } from './sourceMapRepository';
import globStream from 'glob-stream';
import { LogTag, ILogger } from '../logging';
import { forceForwardSlashes, fixDriveLetterAndSlashes } from '../pathUtils';
import { injectable, inject } from 'inversify';
import { inject, injectable } from 'inversify';
import { PathMapping } from '../../configuration';
import { FileGlobList } from '../fileGlobList';

import { ILogger, LogTag } from '../logging';
import { fixDriveLetterAndSlashes, forceForwardSlashes } from '../pathUtils';
import { ISourceMapMetadata } from './sourceMap';
import { createMetadataForFile, ISearchStrategy } from './sourceMapRepository';
/**
* A source map repository that uses globbing to find candidate files.
*/
Expand Down Expand Up @@ -38,6 +38,16 @@ export class NodeSearchStrategy implements ISearchStrategy {
return (await Promise.all(todo)).filter((t): t is T => t !== undefined);
}

/**
* @inheritdoc
*/
public async streamChildrenWithPathMaps<T>(
_pathMapping: PathMapping,
_onChild: (child: Required<ISourceMapMetadata>) => T | Promise<T>,
): Promise<T[]> {
return Promise.resolve([]);
}

/**
* @inheritdoc
*/
Expand Down
12 changes: 12 additions & 0 deletions src/common/sourceMaps/sourceMapRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { PathMapping } from '../../configuration';
import { FileGlobList } from '../fileGlobList';
import { readfile, stat } from '../fsUtils';
import { parseSourceMappingUrl } from '../sourceUtils';
Expand Down Expand Up @@ -29,6 +30,17 @@ export interface ISearchStrategy {
onChild: (child: Required<ISourceMapMetadata>) => T | Promise<T>,
): Promise<T[]>;

/**
* Recursively finds all children that match the pathMap setting, calling
* `onChild` when children are found and returning promise that resolves
* once all children have been discovered.
*/
streamChildrenWithPathMaps<T>(
pathMapping: PathMapping,
onChild: (child: Required<ISourceMapMetadata>) => T | Promise<T>,
pattern?: string,
): Promise<T[]>;

/**
* Recursively finds all children, calling `onChild` when children are found
* and returning promise that resolves once all children have been discovered.
Expand Down
Loading