Skip to content

Commit

Permalink
feat: support discovering node version node 'node' npm module
Browse files Browse the repository at this point in the history
Fixes #769
  • Loading branch information
connor4312 committed Sep 25, 2020
1 parent 2193e4c commit 6daed2c
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 47 deletions.
12 changes: 7 additions & 5 deletions src/common/environmentVars.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as path from 'path';
import {
getCaseInsensitiveProperty,
caseInsensitiveMerge,
removeUndefined,
getCaseInsensitiveProperty,
removeNulls,
removeUndefined,
} from './objUtils';
import * as path from 'path';

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

let value = this.lookup(prop);
if (!value) {
value = location;
} else {
} else if (prependOrAppend === 'append') {
value = value + delimiter + location;
} else {
value = location + delimiter + value;
}

return this.update(prop, value);
Expand Down
15 changes: 15 additions & 0 deletions src/common/fsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export function readFileRaw(path: string): Promise<Buffer> {

export interface IFsUtils {
exists(path: string): Promise<boolean>;
readFile(path: string): Promise<Buffer>;
}

/**
Expand All @@ -128,6 +129,10 @@ export class LocalFsUtils implements IFsUtils {
return false;
}
}

public readFile(path: string) {
return this.fs.readFile(path);
}
}

export class RemoteFsThroughDapUtils implements IFsUtils {
Expand All @@ -143,6 +148,10 @@ export class RemoteFsThroughDapUtils implements IFsUtils {
return false;
}
}

public readFile(): never {
throw new Error('not implemented');
}
}

/**
Expand Down Expand Up @@ -175,6 +184,12 @@ export class LocalAndRemoteFsUtils implements IFsUtils {
);
}

public async readFile(path: string): Promise<Buffer> {
return (this.shouldUseRemoteFileSystem(path) ? this.remoteFsUtils : this.localFsUtils).readFile(
path,
);
}

public shouldUseRemoteFileSystem(path: string) {
return path.startsWith(this.remoteFilePrefix);
}
Expand Down
8 changes: 4 additions & 4 deletions src/common/urlUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ export const nearestDirectoryWhere = async (
predicate: (dir: string) => Promise<boolean>,
): Promise<string | undefined> => {
while (true) {
if (await predicate(rootDir)) {
return rootDir;
}

const parent = path.dirname(rootDir);
if (parent === rootDir) {
return undefined;
}

if (await predicate(parent)) {
return parent;
}

rootDir = parent;
}
};
Expand Down
3 changes: 2 additions & 1 deletion src/ioc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import { NodeAttacher } from './targets/node/nodeAttacher';
import { INodeBinaryProvider, NodeBinaryProvider } from './targets/node/nodeBinaryProvider';
import { NodeLauncher } from './targets/node/nodeLauncher';
import { INvmResolver, NvmResolver } from './targets/node/nvmResolver';
import { IPackageJsonProvider, PackageJsonProvider } from './targets/node/packageJsonProvider';
import { IProgramLauncher } from './targets/node/processLauncher';
import { RestartPolicyFactory } from './targets/node/restartPolicy';
import { SubprocessProgramLauncher } from './targets/node/subprocessProgramLauncher';
Expand Down Expand Up @@ -224,6 +225,7 @@ export const createTopLevelSessionContainer = (parent: Container) => {
container.bind(ILauncher).to(NodeLauncher).onActivation(trackDispose);
container.bind(IProgramLauncher).to(SubprocessProgramLauncher);
container.bind(IProgramLauncher).to(TerminalProgramLauncher);
container.bind(IPackageJsonProvider).to(PackageJsonProvider).inSingletonScope();

if (parent.get(IsVSCode)) {
// dynamic require to not break the debug server
Expand Down Expand Up @@ -311,7 +313,6 @@ export const provideLaunchParams = (
dap: Dap.Api,
) => {
container.bind(AnyLaunchConfiguration).toConstantValue(params);

container.bind(SourcePathResolverFactory).toSelf().inSingletonScope();

container
Expand Down
21 changes: 20 additions & 1 deletion src/targets/node/nodeBinaryProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ErrorCodes } from '../../dap/errors';
import { ProtocolError } from '../../dap/protocolError';
import { Capability, NodeBinary, NodeBinaryProvider } from '../../targets/node/nodeBinaryProvider';
import { testWorkspace } from '../../test/test';
import { IPackageJsonProvider } from './packageJsonProvider';

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

beforeEach(() => (p = new NodeBinaryProvider(Logger.null, fsPromises)));
let packageJson: IPackageJsonProvider;

beforeEach(() => {
packageJson = {
getPath: () => Promise.resolve(undefined),
getContents: () => Promise.resolve(undefined),
};
p = new NodeBinaryProvider(Logger.null, fsPromises, packageJson);
});

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

it('finds node from node_modules when available', async () => {
packageJson.getPath = () =>
Promise.resolve(join(testWorkspace, 'nodePathProvider', 'node-module', 'package.json'));
const binary = await p.resolveAndValidate(env('outdated'), 'npm');
expect(binary.path).to.equal(binaryLocation('outdated', 'npm'));
expect(binary.version).to.deep.equal(new Semver(12, 0, 0));
expect(binary.isPreciselyKnown).to.be.true;
expect(binary.has(Capability.UseSpacesInRequirePath)).to.be.true;
});

describe('electron versioning', () => {
let getVersionText: SinonStub;
let resolveBinaryLocation: SinonStub;
Expand Down
29 changes: 28 additions & 1 deletion src/targets/node/nodeBinaryProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*--------------------------------------------------------*/

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

const localize = nls.loadMessageBundle();

Expand All @@ -34,6 +35,24 @@ export function hideDebugInfoFromConsole(binary: NodeBinary, env: EnvironmentVar
: env;
}

export const isPackageManager = (exe: string) =>
['npm', 'yarn', 'pnpm'].includes(basename(exe, extname(exe)));

/**
* Detects an "npm run"-style invokation, and if found gets the script that the
* user intends to run.
*/
export const getRunScript = (
runtimeExecutable: string | null,
runtimeArgs: ReadonlyArray<string>,
) => {
if (!runtimeExecutable || !isPackageManager(runtimeExecutable)) {
return;
}

return runtimeArgs.find(a => !a.startsWith('-') && a !== 'run' && a !== 'run-script');
};

const assumedVersion = new Semver(12, 0, 0);
const minimumVersion = new Semver(8, 0, 0);

Expand Down Expand Up @@ -120,6 +139,7 @@ export class NodeBinaryProvider {
constructor(
@inject(ILogger) private readonly logger: ILogger,
@inject(FS) private readonly fs: FsPromises,
@inject(IPackageJsonProvider) private readonly packageJson: IPackageJsonProvider,
) {}

/**
Expand Down Expand Up @@ -150,6 +170,13 @@ export class NodeBinaryProvider {
// on the path as a fallback.
const exeInfo = exeRe.exec(basename(location).toLowerCase());
if (!exeInfo) {
if (isPackageManager(location)) {
const packageJson = await this.packageJson.getPath();
if (packageJson) {
env = env.addToPath(resolve(dirname(packageJson), 'node_modules/.bin'), 'prepend');
}
}

try {
const realBinary = await this.resolveAndValidate(env, 'node');
return new NodeBinary(location, realBinary.version);
Expand Down
30 changes: 8 additions & 22 deletions src/targets/node/nodeLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
*--------------------------------------------------------*/

import { inject, injectable, multiInject } from 'inversify';
import { basename, extname, resolve } from 'path';
import { extname, resolve } from 'path';
import { IBreakpointsPredictor } from '../../adapter/breakpointPredictor';
import Cdp from '../../cdp/api';
import { DebugType } from '../../common/contributionUtils';
import { readfile, LocalFsUtils, IFsUtils } from '../../common/fsUtils';
import { IFsUtils, LocalFsUtils } from '../../common/fsUtils';
import { ILogger, LogTag } from '../../common/logging';
import { fixDriveLetterAndSlashes } from '../../common/pathUtils';
import { delay } from '../../common/promiseUtil';
Expand All @@ -18,12 +18,14 @@ import { fixInspectFlags } from '../../ui/configurationUtils';
import { retryGetWSEndpoint } from '../browser/spawn/endpoints';
import { CallbackFile } from './callback-file';
import {
getRunScript,
hideDebugInfoFromConsole,
INodeBinaryProvider,
NodeBinaryProvider,
} from './nodeBinaryProvider';
import { IProcessTelemetry, IRunData, NodeLauncherBase } from './nodeLauncherBase';
import { INodeTargetLifecycleHooks } from './nodeTarget';
import { IPackageJsonProvider } from './packageJsonProvider';
import { IProgramLauncher } from './processLauncher';
import { CombinedProgram, WatchDogProgram } from './program';
import { IRestartPolicy, RestartPolicyFactory } from './restartPolicy';
Expand Down Expand Up @@ -69,6 +71,7 @@ export class NodeLauncher extends NodeLauncherBase<INodeLaunchConfiguration> {
@multiInject(IProgramLauncher) private readonly launchers: ReadonlyArray<IProgramLauncher>,
@inject(RestartPolicyFactory) private readonly restarters: RestartPolicyFactory,
@inject(IFsUtils) fsUtils: LocalFsUtils,
@inject(IPackageJsonProvider) private readonly packageJson: IPackageJsonProvider,
) {
super(pathProvider, logger, fsUtils);
}
Expand Down Expand Up @@ -205,30 +208,13 @@ export class NodeLauncher extends NodeLauncherBase<INodeLaunchConfiguration> {
return params.attachSimplePort;
}

const exe = params.runtimeExecutable;
if (!exe) {
return;
}

if (!['npm', 'yarn', 'pnpm'].includes(basename(exe, extname(exe)))) {
return;
}

const script = params.runtimeArgs.find(
a => !a.startsWith('-') && a !== 'run' && a !== 'run-script',
);
const script = getRunScript(params.runtimeExecutable, params.runtimeArgs);
if (!script) {
return;
}

let packageJson: { scripts?: { [name: string]: string } };
try {
packageJson = JSON.parse(await readfile(resolve(params.cwd, 'package.json')));
} catch {
return;
}

if (!packageJson.scripts?.[script]?.includes('--inspect-brk')) {
const packageJson = await this.packageJson.getContents();
if (!packageJson?.scripts?.[script]?.includes('--inspect-brk')) {
return;
}

Expand Down
82 changes: 82 additions & 0 deletions src/targets/node/packageJsonProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { inject, injectable } from 'inversify';
import { join } from 'path';
import { DebugType } from '../../common/contributionUtils';
import { IFsUtils } from '../../common/fsUtils';
import { once } from '../../common/objUtils';
import { nearestDirectoryContaining } from '../../common/urlUtils';
import { AnyLaunchConfiguration } from '../../configuration';

export interface IPackageJson {
scripts?: {
[name: string]: string;
};
dependencies?: {
[name: string]: string;
};
devDependencies?: {
[name: string]: string;
};
}

export interface IPackageJsonProvider {
/**
* Gets the path for the package.json associated with the current launched program.
*/
getPath(): Promise<string | undefined>;

/**
* Gets the path for the package.json associated with the current launched program.
*/
getContents(): Promise<IPackageJson | undefined>;
}

export const IPackageJsonProvider = Symbol('IPackageJsonProvider');

/**
* A package.json provider that never returns path or contents.
*/
export const noPackageJsonProvider = {
getPath: () => Promise.resolve(undefined),
getContents: () => Promise.resolve(undefined),
};

@injectable()
export class PackageJsonProvider implements IPackageJsonProvider {
constructor(
@inject(IFsUtils) private readonly fs: IFsUtils,
@inject(AnyLaunchConfiguration) private readonly config: AnyLaunchConfiguration,
) {}

/**
* Gets the package.json for the debugged program.
*/
public readonly getPath = once(async () => {
if (this.config.type !== DebugType.Node || this.config.request !== 'launch') {
return;
}

const dir = await nearestDirectoryContaining(this.fs, this.config.cwd, 'package.json');
return dir ? join(dir, 'package.json') : undefined;
});

/**
* Gets the package.json contents for the debugged program.
*/
public readonly getContents = once(async () => {
const path = await this.getPath();
if (!path) {
return;
}

try {
const contents = await this.fs.readFile(path);
return JSON.parse(contents.toString()) as IPackageJson;
} catch {
return;
}
});
}
Loading

0 comments on commit 6daed2c

Please sign in to comment.