Skip to content
Merged
63 changes: 55 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ jobs:
Lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v1
- uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: "20.x"
- name: Install dependencies
run: yarn
- name: Lint
Expand All @@ -25,18 +25,65 @@ jobs:
strategy:
fail-fast: false
matrix:
nodeVersion: [ '14.19.1', '16.14.2', '18.0.0' ]
os: [ macos-latest, ubuntu-latest, windows-latest ]
nodeVersion: ["14.x", "16.x", "18.x", "20.x", "22.x"]
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v1
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.nodeVersion }}
- name: Install dependencies
run: yarn
- name: Run tests
run: yarn run test
env:
SNOOPLOGG: '*'
SNOOPLOGG: "*"

MacOSTest:
needs: Lint
name: ${{ matrix.os }} ${{ matrix.nodeVersion }} Tests
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
nodeVersion: ["16.x", "18.x", "20.x", "22.x"]
os: [macos-latest]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.nodeVersion }}
- name: Install dependencies
run: yarn
- name: Run tests
run: yarn run test
env:
SNOOPLOGG: "*"

MacOS14Test:
needs: Lint
name: ${{ matrix.os }} ${{ matrix.nodeVersion }} Tests
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
nodeVersion: ["14.x"]
os: [macos-latest]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.nodeVersion }}
architecture: "x64"
- name: Install dependencies
run: yarn
- name: Run tests
run: yarn run test
env:
SNOOPLOGG: "*"
27 changes: 27 additions & 0 deletions src/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import argvSplit from 'argv-split';
import fs from 'fs-extra';
import E from './errors.js';
import path from 'path';
import os from 'os';
import semver from 'semver';
import which from 'which';
import child_process from 'child_process';
import { fileURLToPath } from 'url';
import { packageDirectorySync } from 'pkg-dir';
import { execPath } from 'process';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -264,3 +267,27 @@ export function wrap(str, width, indent) {
})
.join('\n');
}

// cache to avoid extra lookups
let _nodePath;
export function nodePath() {
if (!_nodePath) {
const execPath = process.execPath;
// cannot exec cmd on windows on new versions of node https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
// CVE-2024-27980. Can't pass shell: true to get around this on windows since it breaks non-shell executions.
// Can't imagine node would be a bat but who knows. It's .cmd on windows often.
if (os.platform() === 'win32' && [ 'cmd', 'bat' ].includes(path.extname(execPath))) {
// try and see if the node.exe lives in the same dir
const newNodePath = execPath.replace(new RegExp(`${path.extname(execPath)}$`), 'exe');
try {
fs.statSync(newNodePath);
_nodePath = newNodePath;
} catch (err) {
_nodePath = 'node.exe';
}
} else {
_nodePath = execPath;
}
}
return _nodePath;
}
7 changes: 5 additions & 2 deletions src/parser/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import E from '../lib/errors.js';
import helpCommand from '../commands/help.js';
import _path from 'path';

import { declareCLIKitClass, filename, findPackage, isExecutable } from '../lib/util.js';
import { declareCLIKitClass, filename, findPackage, isExecutable, nodePath } from '../lib/util.js';
import { spawn } from 'child_process';

const { log, warn } = debug('cli-kit:extension');
Expand Down Expand Up @@ -34,6 +34,7 @@ export default class Extension {
* @access public
*/
constructor(pathOrParams, params) {
log({pathOrParams, params});
let path = pathOrParams;

if (typeof path === 'string' && !params) {
Expand Down Expand Up @@ -114,7 +115,7 @@ export default class Extension {
const makeDefaultAction = main => {
return async ({ __argv, cmd }) => {
process.argv = [
process.execPath,
nodePath(),
main
];

Expand Down Expand Up @@ -239,6 +240,8 @@ export default class Extension {
*/
registerExtension(name, meta, params) {
log(`Registering extension command: ${highlight(`${this.name}:${name}`)}`);
log(meta);
log(params);
const cmd = new Command(name, {
parent: this,
...params
Expand Down
3 changes: 2 additions & 1 deletion test/examples/external-binary/extbin.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import CLI from '../../../src/index.js';
import { nodePath } from '../../../src/lib/util.js';

new CLI({
extensions: [ 'node' ]
extensions: [ nodePath() ]
}).exec();
3 changes: 2 additions & 1 deletion test/examples/run-node/run.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import CLI from '../../../src/index.js';
import { nodePath } from '../../../src/lib/util.js';

new CLI({
extensions: {
run: `"${process.execPath}" -e`
run: `"${nodePath()}" -e`
}
}).exec();
1 change: 1 addition & 0 deletions test/test-argument.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('Argument', () => {
name: 'foo',
type: 'bar'
});
throw new Error('Expected error');
} catch (err) {
expect(err).to.be.instanceof(Error);
expect(err.message).to.equal('Unsupported type "bar"');
Expand Down
35 changes: 24 additions & 11 deletions test/test-extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import CLI, { ansi, Extension, Terminal } from '../src/index.js';
import path from 'path';
import { expect } from 'chai';
import { fileURLToPath } from 'url';
import { platform } from 'os';
import { spawnSync } from 'child_process';
import { WritableStream } from 'memory-streams';
import { nodePath } from '../src/lib/util.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -55,14 +57,19 @@ describe('Extension', () => {

const env = { ...process.env };
delete env.SNOOPLOGG;

const { status, stdout, stderr } = spawnSync(process.execPath, [
const args = [
path.join(__dirname, 'examples', 'external-binary', 'extbin.js'),
// this is the command name!
'node',
'-e',
'console.log(\'foo\');'
], { env });
expect(stdout.toString().trim() + stderr.toString().trim()).to.match(/foo/im);
];

const { status, stdout, stderr } = spawnSync(nodePath(), args, {
env
});
expect(stdout.toString().trim()).to.equal('foo');
expect(stderr.toString().trim()).to.equal('');
expect(status).to.equal(0);
});

Expand All @@ -73,9 +80,11 @@ describe('Extension', () => {
const env = { ...process.env };
delete env.SNOOPLOGG;

const { status, stdout, stderr } = spawnSync(process.execPath, [
const { status, stdout, stderr } = spawnSync(nodePath(), [
path.join(__dirname, 'examples', 'run-node', 'run.js'), 'run', 'console.log(\'It works\')'
], { env });
], {
env
});
expect(status).to.equal(0);
expect(stdout.toString().trim() + stderr.toString().trim()).to.match(/It works/m);
});
Expand All @@ -86,7 +95,7 @@ describe('Extension', () => {
const cli = new CLI({
colors: false,
extensions: {
echo: 'node -e \'console.log("hi " + process.argv.slice(1).join(" "))\''
echo: nodePath() + ' -e \'console.log("hi " + process.argv.slice(1).join(" "))\''
},
help: true,
name: 'test-cli',
Expand Down Expand Up @@ -166,9 +175,11 @@ describe('Extension', () => {
const env = { ...process.env };
delete env.SNOOPLOGG;

const { status, stdout, stderr } = spawnSync(process.execPath, [
const { status, stdout, stderr } = spawnSync(nodePath(), [
path.join(__dirname, 'examples', 'external-js-file', 'extjsfile.js'), 'simple', 'foo', 'bar'
], { env });
], {
env
});
expect(stdout.toString().trim() + stderr.toString().trim()).to.equal(`${process.version} foo bar`);
expect(status).to.equal(0);
});
Expand All @@ -180,9 +191,11 @@ describe('Extension', () => {
const env = { ...process.env };
delete env.SNOOPLOGG;

const { status, stdout, stderr } = spawnSync(process.execPath, [
const { status, stdout, stderr } = spawnSync(nodePath(), [
path.join(__dirname, 'examples', 'external-module', 'extmod.js'), 'foo', 'bar'
], { env });
], {
env
});
expect(stdout.toString().trim() + stderr.toString().trim()).to.equal(`${process.version} bar`);
expect(status).to.equal(0);
});
Expand Down
5 changes: 4 additions & 1 deletion test/test-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { expect } from 'chai';
import { fileURLToPath } from 'url';
import { spawnSync } from 'child_process';
import { WritableStream } from 'memory-streams';
import { nodePath } from '../src/lib/util.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -86,7 +87,9 @@ describe('Parser', () => {
const env = Object.assign({}, process.env);
delete env.SNOOPLOGG;

const { status, stdout } = spawnSync(process.execPath, [ path.join(__dirname, 'examples', 'version-test', 'ver.js'), '--version' ], { env });
const { status, stdout } = spawnSync(nodePath(), [ path.join(__dirname, 'examples', 'version-test', 'ver.js'), '--version' ], {
env
});
expect(status).to.equal(0);
expect(stdout.toString()).to.equal('1.2.3\n');
});
Expand Down
15 changes: 5 additions & 10 deletions test/test-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,12 @@ describe('util', () => {
describe('findPackage()', () => {
it('should throw error if package.json has syntax error', () => {
const dir = path.resolve(__dirname, 'fixtures', 'bad-pkg-json');
expectThrow(() => {
try {
findPackage(dir);
}, {
type: Error,
msg: 'Failed to parse package.json: Unexpected token { in JSON at position 1',
code: 'ERR_INVALID_PACKAGE_JSON',
file: path.join(dir, 'package.json'),
name: 'package.json.bad',
scope: 'util.findPackage',
value: /{{{{{{{{{{\r?\n/
});
throw new Error('Expected error');
} catch (err) {
expect(err.message).to.match(/Failed to parse package.json:/);
}
});

it('should throw error if package.json is not an object', () => {
Expand Down