diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a67c551..97b9345 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 @@ -25,13 +25,13 @@ 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 @@ -39,4 +39,51 @@ jobs: - 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: "*" diff --git a/src/lib/util.js b/src/lib/util.js index dd25444..8f1d5f0 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -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)); @@ -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; +} diff --git a/src/parser/extension.js b/src/parser/extension.js index 353bf9f..31fa98d 100644 --- a/src/parser/extension.js +++ b/src/parser/extension.js @@ -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'); @@ -34,6 +34,7 @@ export default class Extension { * @access public */ constructor(pathOrParams, params) { + log({pathOrParams, params}); let path = pathOrParams; if (typeof path === 'string' && !params) { @@ -114,7 +115,7 @@ export default class Extension { const makeDefaultAction = main => { return async ({ __argv, cmd }) => { process.argv = [ - process.execPath, + nodePath(), main ]; @@ -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 diff --git a/test/examples/external-binary/extbin.js b/test/examples/external-binary/extbin.js index 241da9d..9693aca 100644 --- a/test/examples/external-binary/extbin.js +++ b/test/examples/external-binary/extbin.js @@ -1,5 +1,6 @@ import CLI from '../../../src/index.js'; +import { nodePath } from '../../../src/lib/util.js'; new CLI({ - extensions: [ 'node' ] + extensions: [ nodePath() ] }).exec(); diff --git a/test/examples/run-node/run.js b/test/examples/run-node/run.js index e908ab7..780ac57 100644 --- a/test/examples/run-node/run.js +++ b/test/examples/run-node/run.js @@ -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(); diff --git a/test/test-argument.js b/test/test-argument.js index 7de2baf..9d87f09 100644 --- a/test/test-argument.js +++ b/test/test-argument.js @@ -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"'); diff --git a/test/test-extension.js b/test/test-extension.js index 1e54858..1cc7931 100644 --- a/test/test-extension.js +++ b/test/test-extension.js @@ -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)); @@ -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); }); @@ -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); }); @@ -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', @@ -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); }); @@ -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); }); diff --git a/test/test-parser.js b/test/test-parser.js index 466ff21..d397346 100644 --- a/test/test-parser.js +++ b/test/test-parser.js @@ -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)); @@ -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'); }); diff --git a/test/test-util.js b/test/test-util.js index dbc1f91..373c513 100644 --- a/test/test-util.js +++ b/test/test-util.js @@ -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', () => {