Skip to content

fix: add node info W-18451508 #1112

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

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
8 changes: 8 additions & 0 deletions command-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
[
{
"alias": [],
"command": "node:info",
"flagAliases": [],
"flagChars": [],
"flags": ["flags-dir", "json"],
"plugin": "@salesforce/plugin-trust"
},
{
"alias": [],
"command": "plugins:trust:verify",
Expand Down
7 changes: 7 additions & 0 deletions messages/node.info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# summary

Returns the path to node and npx files bundled by the Salesforce CLI.

# description

Command for internal use only.
32 changes: 32 additions & 0 deletions src/commands/node/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { SfCommand } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { NpmCommand } from '../../shared/npmCommand.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-trust', 'node.info');

export type NodeInfoResult = {
nodePath: string;
npxPath: string;
};

export default class NodeInfo extends SfCommand<NodeInfoResult> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly hidden = true;

// eslint-disable-next-line @typescript-eslint/require-await
public async run(): Promise<NodeInfoResult> {
return {
nodePath: NpmCommand.findNode(this.config.root),
npxPath: NpmCommand.npxCli(),
};
}
}
29 changes: 20 additions & 9 deletions src/shared/npmCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ type NpmPackage = {
};
};

class NpmCommand {
export class NpmCommand {
public static runNpmCmd(cmd: string, options = {} as NpmCommandOptions): NpmCommandResult {
const nodeExecutable = NpmCommand.findNode(options.cliRoot);
const npmCli = NpmCommand.npmCli();
Expand All @@ -74,20 +74,15 @@ class NpmCommand {
return npmCmdResult;
}

/**
* Returns the path to the npm-cli.js file in this package's node_modules
*
* @private
*/
private static npmCli(): string {
public static npxCli(): string {
const require = createRequire(import.meta.url);
const pkgPath = require.resolve('npm/package.json');

const fileData = fs.readFileSync(pkgPath, 'utf8');
const pkgJson = parseJson(fileData, pkgPath) as NpmPackage;

const prjPath = pkgPath.substring(0, pkgPath.lastIndexOf(path.sep));
return path.join(prjPath, pkgJson.bin['npm']);
return path.join(prjPath, pkgJson.bin['npx']);
}

/**
Expand All @@ -99,7 +94,7 @@ class NpmCommand {
*
* @private
*/
private static findNode(root?: string): string {
public static findNode(root?: string): string {
const isExecutable = (filepath: string): boolean => {
if (os.type() === 'Windows_NT') return filepath.endsWith('node.exe');

Expand Down Expand Up @@ -136,6 +131,22 @@ class NpmCommand {
);
}

/**
* Returns the path to the npm-cli.js file in this package's node_modules
*
* @private
*/
private static npmCli(): string {
const require = createRequire(import.meta.url);
const pkgPath = require.resolve('npm/package.json');

const fileData = fs.readFileSync(pkgPath, 'utf8');
const pkgJson = parseJson(fileData, pkgPath) as NpmPackage;

const prjPath = pkgPath.substring(0, pkgPath.lastIndexOf(path.sep));
return path.join(prjPath, pkgJson.bin['npm']);
}

/**
* Finds the bin directory in the sfdx installation root path
*
Expand Down
64 changes: 64 additions & 0 deletions test/nuts/node-info.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import path from 'node:path';
import { expect } from 'chai';
import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit';
import { Messages } from '@salesforce/core';
import shelljs from 'shelljs';
import { ensureObject } from '@salesforce/ts-types';
import { NodeInfoResult } from '../../src/commands/node/info.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
describe('node info command', () => {
let session: TestSession;

before(async () => {
session = await TestSession.create({ devhubAuthStrategy: 'NONE' });
//
// ensure that this repo's version of the plugin is used and NOT the one that shipped with the CLI
execCmd('plugins link .', {
cwd: path.dirname(session.dir),
ensureExitCode: 0,
cli: 'sf',
});
});

after(async () => {
await session?.zip(undefined, 'artifacts');
try {
await session?.clean();
} catch (error) {
// ignore
}
});

it('should return node and npx paths and verify they work', () => {
// Get node and npx paths
const info = ensureObject<NodeInfoResult>(
execCmd('node info --json', {
ensureExitCode: 0,
// IMPORTANT: this NUT should run commands via `sf` to mimic real usage (node path being resolved from sf's rootPath)
// Do not make it run using `./bin/run.js`.
cli: 'sf',
}).jsonOutput?.result
);

expect(info.nodePath).to.be.a('string').and.not.empty;
expect(info.npxPath).to.be.a('string').and.not.empty;

// Verify node path works
const nodeHelp = shelljs.exec(`"${info.nodePath}" --help`, { silent: true });
expect(nodeHelp.code).to.equal(0);
expect(nodeHelp.stdout).to.contain('Usage: node');

// Verify npx path works
const npxHelp = shelljs.exec(`"${info.nodePath}" "${info.npxPath}" --help`, { silent: true });
expect(npxHelp.code).to.equal(0);
expect(npxHelp.stdout).to.contain('Run a command from a local or remote npm package');
});
});
Loading