Skip to content

Commit 6daed2c

Browse files
committed
feat: support discovering node version node 'node' npm module
Fixes #769
1 parent 2193e4c commit 6daed2c

File tree

14 files changed

+185
-47
lines changed

14 files changed

+185
-47
lines changed

src/common/environmentVars.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/*---------------------------------------------------------
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
4+
import * as path from 'path';
45
import {
5-
getCaseInsensitiveProperty,
66
caseInsensitiveMerge,
7-
removeUndefined,
7+
getCaseInsensitiveProperty,
88
removeNulls,
9+
removeUndefined,
910
} from './objUtils';
10-
import * as path from 'path';
1111

1212
/**
1313
* Container for holding sets of environment variables. Deals with case
@@ -52,16 +52,18 @@ export class EnvironmentVars {
5252
/**
5353
* Adds the given location to the environment PATH.
5454
*/
55-
public addToPath(location: string) {
55+
public addToPath(location: string, prependOrAppend: 'prepend' | 'append' = 'append') {
5656
const prop = EnvironmentVars.platform === 'win32' ? 'Path' : 'PATH';
5757
const delimiter =
5858
EnvironmentVars.platform === 'win32' ? path.win32.delimiter : path.posix.delimiter;
5959

6060
let value = this.lookup(prop);
6161
if (!value) {
6262
value = location;
63-
} else {
63+
} else if (prependOrAppend === 'append') {
6464
value = value + delimiter + location;
65+
} else {
66+
value = location + delimiter + value;
6567
}
6668

6769
return this.update(prop, value);

src/common/fsUtils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export function readFileRaw(path: string): Promise<Buffer> {
109109

110110
export interface IFsUtils {
111111
exists(path: string): Promise<boolean>;
112+
readFile(path: string): Promise<Buffer>;
112113
}
113114

114115
/**
@@ -128,6 +129,10 @@ export class LocalFsUtils implements IFsUtils {
128129
return false;
129130
}
130131
}
132+
133+
public readFile(path: string) {
134+
return this.fs.readFile(path);
135+
}
131136
}
132137

133138
export class RemoteFsThroughDapUtils implements IFsUtils {
@@ -143,6 +148,10 @@ export class RemoteFsThroughDapUtils implements IFsUtils {
143148
return false;
144149
}
145150
}
151+
152+
public readFile(): never {
153+
throw new Error('not implemented');
154+
}
146155
}
147156

148157
/**
@@ -175,6 +184,12 @@ export class LocalAndRemoteFsUtils implements IFsUtils {
175184
);
176185
}
177186

187+
public async readFile(path: string): Promise<Buffer> {
188+
return (this.shouldUseRemoteFileSystem(path) ? this.remoteFsUtils : this.localFsUtils).readFile(
189+
path,
190+
);
191+
}
192+
178193
public shouldUseRemoteFileSystem(path: string) {
179194
return path.startsWith(this.remoteFilePrefix);
180195
}

src/common/urlUtils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,15 @@ export const nearestDirectoryWhere = async (
6565
predicate: (dir: string) => Promise<boolean>,
6666
): Promise<string | undefined> => {
6767
while (true) {
68+
if (await predicate(rootDir)) {
69+
return rootDir;
70+
}
71+
6872
const parent = path.dirname(rootDir);
6973
if (parent === rootDir) {
7074
return undefined;
7175
}
7276

73-
if (await predicate(parent)) {
74-
return parent;
75-
}
76-
7777
rootDir = parent;
7878
}
7979
};

src/ioc.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import { NodeAttacher } from './targets/node/nodeAttacher';
8181
import { INodeBinaryProvider, NodeBinaryProvider } from './targets/node/nodeBinaryProvider';
8282
import { NodeLauncher } from './targets/node/nodeLauncher';
8383
import { INvmResolver, NvmResolver } from './targets/node/nvmResolver';
84+
import { IPackageJsonProvider, PackageJsonProvider } from './targets/node/packageJsonProvider';
8485
import { IProgramLauncher } from './targets/node/processLauncher';
8586
import { RestartPolicyFactory } from './targets/node/restartPolicy';
8687
import { SubprocessProgramLauncher } from './targets/node/subprocessProgramLauncher';
@@ -224,6 +225,7 @@ export const createTopLevelSessionContainer = (parent: Container) => {
224225
container.bind(ILauncher).to(NodeLauncher).onActivation(trackDispose);
225226
container.bind(IProgramLauncher).to(SubprocessProgramLauncher);
226227
container.bind(IProgramLauncher).to(TerminalProgramLauncher);
228+
container.bind(IPackageJsonProvider).to(PackageJsonProvider).inSingletonScope();
227229

228230
if (parent.get(IsVSCode)) {
229231
// dynamic require to not break the debug server
@@ -311,7 +313,6 @@ export const provideLaunchParams = (
311313
dap: Dap.Api,
312314
) => {
313315
container.bind(AnyLaunchConfiguration).toConstantValue(params);
314-
315316
container.bind(SourcePathResolverFactory).toSelf().inSingletonScope();
316317

317318
container

src/targets/node/nodeBinaryProvider.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ErrorCodes } from '../../dap/errors';
1313
import { ProtocolError } from '../../dap/protocolError';
1414
import { Capability, NodeBinary, NodeBinaryProvider } from '../../targets/node/nodeBinaryProvider';
1515
import { testWorkspace } from '../../test/test';
16+
import { IPackageJsonProvider } from './packageJsonProvider';
1617

1718
describe('NodeBinaryProvider', function () {
1819
this.timeout(30 * 1000); // windows lookups in CI seem to be very slow sometimes
@@ -28,7 +29,15 @@ describe('NodeBinaryProvider', function () {
2829
process.platform === 'win32' ? `${binary}.exe` : binary,
2930
);
3031

31-
beforeEach(() => (p = new NodeBinaryProvider(Logger.null, fsPromises)));
32+
let packageJson: IPackageJsonProvider;
33+
34+
beforeEach(() => {
35+
packageJson = {
36+
getPath: () => Promise.resolve(undefined),
37+
getContents: () => Promise.resolve(undefined),
38+
};
39+
p = new NodeBinaryProvider(Logger.null, fsPromises, packageJson);
40+
});
3241

3342
it('rejects not found', async () => {
3443
try {
@@ -106,6 +115,16 @@ describe('NodeBinaryProvider', function () {
106115
expect(binary.has(Capability.UseSpacesInRequirePath)).to.be.false;
107116
});
108117

118+
it('finds node from node_modules when available', async () => {
119+
packageJson.getPath = () =>
120+
Promise.resolve(join(testWorkspace, 'nodePathProvider', 'node-module', 'package.json'));
121+
const binary = await p.resolveAndValidate(env('outdated'), 'npm');
122+
expect(binary.path).to.equal(binaryLocation('outdated', 'npm'));
123+
expect(binary.version).to.deep.equal(new Semver(12, 0, 0));
124+
expect(binary.isPreciselyKnown).to.be.true;
125+
expect(binary.has(Capability.UseSpacesInRequirePath)).to.be.true;
126+
});
127+
109128
describe('electron versioning', () => {
110129
let getVersionText: SinonStub;
111130
let resolveBinaryLocation: SinonStub;

src/targets/node/nodeBinaryProvider.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*--------------------------------------------------------*/
44

55
import { inject, injectable } from 'inversify';
6-
import { basename, isAbsolute } from 'path';
6+
import { basename, dirname, extname, isAbsolute, resolve } from 'path';
77
import * as nls from 'vscode-nls';
88
import { EnvironmentVars } from '../../common/environmentVars';
99
import { ILogger, LogTag } from '../../common/logging';
@@ -13,6 +13,7 @@ import { Semver } from '../../common/semver';
1313
import { cannotFindNodeBinary, ErrorCodes, nodeBinaryOutOfDate } from '../../dap/errors';
1414
import { ProtocolError } from '../../dap/protocolError';
1515
import { FS, FsPromises } from '../../ioc-extras';
16+
import { IPackageJsonProvider } from './packageJsonProvider';
1617

1718
const localize = nls.loadMessageBundle();
1819

@@ -34,6 +35,24 @@ export function hideDebugInfoFromConsole(binary: NodeBinary, env: EnvironmentVar
3435
: env;
3536
}
3637

38+
export const isPackageManager = (exe: string) =>
39+
['npm', 'yarn', 'pnpm'].includes(basename(exe, extname(exe)));
40+
41+
/**
42+
* Detects an "npm run"-style invokation, and if found gets the script that the
43+
* user intends to run.
44+
*/
45+
export const getRunScript = (
46+
runtimeExecutable: string | null,
47+
runtimeArgs: ReadonlyArray<string>,
48+
) => {
49+
if (!runtimeExecutable || !isPackageManager(runtimeExecutable)) {
50+
return;
51+
}
52+
53+
return runtimeArgs.find(a => !a.startsWith('-') && a !== 'run' && a !== 'run-script');
54+
};
55+
3756
const assumedVersion = new Semver(12, 0, 0);
3857
const minimumVersion = new Semver(8, 0, 0);
3958

@@ -120,6 +139,7 @@ export class NodeBinaryProvider {
120139
constructor(
121140
@inject(ILogger) private readonly logger: ILogger,
122141
@inject(FS) private readonly fs: FsPromises,
142+
@inject(IPackageJsonProvider) private readonly packageJson: IPackageJsonProvider,
123143
) {}
124144

125145
/**
@@ -150,6 +170,13 @@ export class NodeBinaryProvider {
150170
// on the path as a fallback.
151171
const exeInfo = exeRe.exec(basename(location).toLowerCase());
152172
if (!exeInfo) {
173+
if (isPackageManager(location)) {
174+
const packageJson = await this.packageJson.getPath();
175+
if (packageJson) {
176+
env = env.addToPath(resolve(dirname(packageJson), 'node_modules/.bin'), 'prepend');
177+
}
178+
}
179+
153180
try {
154181
const realBinary = await this.resolveAndValidate(env, 'node');
155182
return new NodeBinary(location, realBinary.version);

src/targets/node/nodeLauncher.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
*--------------------------------------------------------*/
44

55
import { inject, injectable, multiInject } from 'inversify';
6-
import { basename, extname, resolve } from 'path';
6+
import { extname, resolve } from 'path';
77
import { IBreakpointsPredictor } from '../../adapter/breakpointPredictor';
88
import Cdp from '../../cdp/api';
99
import { DebugType } from '../../common/contributionUtils';
10-
import { readfile, LocalFsUtils, IFsUtils } from '../../common/fsUtils';
10+
import { IFsUtils, LocalFsUtils } from '../../common/fsUtils';
1111
import { ILogger, LogTag } from '../../common/logging';
1212
import { fixDriveLetterAndSlashes } from '../../common/pathUtils';
1313
import { delay } from '../../common/promiseUtil';
@@ -18,12 +18,14 @@ import { fixInspectFlags } from '../../ui/configurationUtils';
1818
import { retryGetWSEndpoint } from '../browser/spawn/endpoints';
1919
import { CallbackFile } from './callback-file';
2020
import {
21+
getRunScript,
2122
hideDebugInfoFromConsole,
2223
INodeBinaryProvider,
2324
NodeBinaryProvider,
2425
} from './nodeBinaryProvider';
2526
import { IProcessTelemetry, IRunData, NodeLauncherBase } from './nodeLauncherBase';
2627
import { INodeTargetLifecycleHooks } from './nodeTarget';
28+
import { IPackageJsonProvider } from './packageJsonProvider';
2729
import { IProgramLauncher } from './processLauncher';
2830
import { CombinedProgram, WatchDogProgram } from './program';
2931
import { IRestartPolicy, RestartPolicyFactory } from './restartPolicy';
@@ -69,6 +71,7 @@ export class NodeLauncher extends NodeLauncherBase<INodeLaunchConfiguration> {
6971
@multiInject(IProgramLauncher) private readonly launchers: ReadonlyArray<IProgramLauncher>,
7072
@inject(RestartPolicyFactory) private readonly restarters: RestartPolicyFactory,
7173
@inject(IFsUtils) fsUtils: LocalFsUtils,
74+
@inject(IPackageJsonProvider) private readonly packageJson: IPackageJsonProvider,
7275
) {
7376
super(pathProvider, logger, fsUtils);
7477
}
@@ -205,30 +208,13 @@ export class NodeLauncher extends NodeLauncherBase<INodeLaunchConfiguration> {
205208
return params.attachSimplePort;
206209
}
207210

208-
const exe = params.runtimeExecutable;
209-
if (!exe) {
210-
return;
211-
}
212-
213-
if (!['npm', 'yarn', 'pnpm'].includes(basename(exe, extname(exe)))) {
214-
return;
215-
}
216-
217-
const script = params.runtimeArgs.find(
218-
a => !a.startsWith('-') && a !== 'run' && a !== 'run-script',
219-
);
211+
const script = getRunScript(params.runtimeExecutable, params.runtimeArgs);
220212
if (!script) {
221213
return;
222214
}
223215

224-
let packageJson: { scripts?: { [name: string]: string } };
225-
try {
226-
packageJson = JSON.parse(await readfile(resolve(params.cwd, 'package.json')));
227-
} catch {
228-
return;
229-
}
230-
231-
if (!packageJson.scripts?.[script]?.includes('--inspect-brk')) {
216+
const packageJson = await this.packageJson.getContents();
217+
if (!packageJson?.scripts?.[script]?.includes('--inspect-brk')) {
232218
return;
233219
}
234220

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { inject, injectable } from 'inversify';
6+
import { join } from 'path';
7+
import { DebugType } from '../../common/contributionUtils';
8+
import { IFsUtils } from '../../common/fsUtils';
9+
import { once } from '../../common/objUtils';
10+
import { nearestDirectoryContaining } from '../../common/urlUtils';
11+
import { AnyLaunchConfiguration } from '../../configuration';
12+
13+
export interface IPackageJson {
14+
scripts?: {
15+
[name: string]: string;
16+
};
17+
dependencies?: {
18+
[name: string]: string;
19+
};
20+
devDependencies?: {
21+
[name: string]: string;
22+
};
23+
}
24+
25+
export interface IPackageJsonProvider {
26+
/**
27+
* Gets the path for the package.json associated with the current launched program.
28+
*/
29+
getPath(): Promise<string | undefined>;
30+
31+
/**
32+
* Gets the path for the package.json associated with the current launched program.
33+
*/
34+
getContents(): Promise<IPackageJson | undefined>;
35+
}
36+
37+
export const IPackageJsonProvider = Symbol('IPackageJsonProvider');
38+
39+
/**
40+
* A package.json provider that never returns path or contents.
41+
*/
42+
export const noPackageJsonProvider = {
43+
getPath: () => Promise.resolve(undefined),
44+
getContents: () => Promise.resolve(undefined),
45+
};
46+
47+
@injectable()
48+
export class PackageJsonProvider implements IPackageJsonProvider {
49+
constructor(
50+
@inject(IFsUtils) private readonly fs: IFsUtils,
51+
@inject(AnyLaunchConfiguration) private readonly config: AnyLaunchConfiguration,
52+
) {}
53+
54+
/**
55+
* Gets the package.json for the debugged program.
56+
*/
57+
public readonly getPath = once(async () => {
58+
if (this.config.type !== DebugType.Node || this.config.request !== 'launch') {
59+
return;
60+
}
61+
62+
const dir = await nearestDirectoryContaining(this.fs, this.config.cwd, 'package.json');
63+
return dir ? join(dir, 'package.json') : undefined;
64+
});
65+
66+
/**
67+
* Gets the package.json contents for the debugged program.
68+
*/
69+
public readonly getContents = once(async () => {
70+
const path = await this.getPath();
71+
if (!path) {
72+
return;
73+
}
74+
75+
try {
76+
const contents = await this.fs.readFile(path);
77+
return JSON.parse(contents.toString()) as IPackageJson;
78+
} catch {
79+
return;
80+
}
81+
});
82+
}

0 commit comments

Comments
 (0)