npq 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/__tests__/cli.parser.test.js +121 -0
- package/__tests__/env-var-integration.test.js +109 -0
- package/__tests__/packageManager.test.js +22 -0
- package/bin/npq-hero.js +4 -1
- package/lib/cli.js +17 -4
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ npq install express
|
|
|
37
37
|
* Package has a LICENSE file
|
|
38
38
|
* Package has pre/post install scripts
|
|
39
39
|
|
|
40
|
-
If npq is prompted to continue with the install, it simply hands over the actual package install job to the package manager (npm by default).
|
|
40
|
+
If npq is prompted to continue with the install, it simply hands over the actual package install job to the package manager (npm by default, or as specified via the `NPQ_PKG_MGR` environment variable). Note that if a package manager is specified via command-line options, it will override the `NPQ_PKG_MGR` environment variable.
|
|
41
41
|
|
|
42
42
|
DISCLAIMER: there's no guaranteed absolute safety; a malicious or vulnerable package could still exist that has no security vulnerabilities publicly disclosed and passes npq's checks.
|
|
43
43
|
|
|
@@ -71,15 +71,26 @@ alias npm='npq-hero'
|
|
|
71
71
|
|
|
72
72
|
### Offload to package managers
|
|
73
73
|
|
|
74
|
-
If you're using `yarn`, or generally want to explicitly tell npq which package manager to use you can specify an environment variable: `NPQ_PKG_MGR
|
|
74
|
+
If you're using `yarn`, `pnpm`, or generally want to explicitly tell npq which package manager to use you can specify an environment variable: `NPQ_PKG_MGR=<package-manager>`
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
Examples:
|
|
77
77
|
|
|
78
|
+
**Using yarn:**
|
|
78
79
|
```bash
|
|
79
80
|
alias yarn="NPQ_PKG_MGR=yarn npq-hero"
|
|
80
81
|
```
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
**Using pnpm:**
|
|
84
|
+
```bash
|
|
85
|
+
NPQ_PKG_MGR=pnpm npx npq install fastify
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Using pnpm with alias:**
|
|
89
|
+
```bash
|
|
90
|
+
alias pnpm="NPQ_PKG_MGR=pnpm npq-hero"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Note: `npq` by default will offload all commands and their arguments to the `npm` (or other package manager as specified) after it finished its due-diligence checks for the respective packages.
|
|
83
94
|
|
|
84
95
|
## Marshalls
|
|
85
96
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// __tests__/env-var-integration.test.js
|
|
2
|
+
|
|
3
|
+
describe('NPQ_PKG_MGR Environment Variable Integration', () => {
|
|
4
|
+
let originalArgv
|
|
5
|
+
let originalNPQ_PKG_MGR
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Save original values
|
|
9
|
+
originalArgv = process.argv
|
|
10
|
+
originalNPQ_PKG_MGR = process.env.NPQ_PKG_MGR
|
|
11
|
+
|
|
12
|
+
// Clear environment variable
|
|
13
|
+
delete process.env.NPQ_PKG_MGR
|
|
14
|
+
|
|
15
|
+
// Mock console methods to avoid output during tests
|
|
16
|
+
jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
17
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
18
|
+
jest.spyOn(process, 'exit').mockImplementation(() => {})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
// Restore original values
|
|
23
|
+
process.argv = originalArgv
|
|
24
|
+
if (originalNPQ_PKG_MGR !== undefined) {
|
|
25
|
+
process.env.NPQ_PKG_MGR = originalNPQ_PKG_MGR
|
|
26
|
+
} else {
|
|
27
|
+
delete process.env.NPQ_PKG_MGR
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Restore mocked methods
|
|
31
|
+
jest.restoreAllMocks()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test.only('should prioritize NPQ_PKG_MGR environment variable over command line options', () => {
|
|
35
|
+
// This test verifies the core functionality:
|
|
36
|
+
// process.env.NPQ_PKG_MGR || values.packageManager || values.pkgMgr || 'npm'
|
|
37
|
+
|
|
38
|
+
process.env.NPQ_PKG_MGR = 'pnpm'
|
|
39
|
+
|
|
40
|
+
// Dynamically require to get fresh instance with environment variable set
|
|
41
|
+
jest.resetModules()
|
|
42
|
+
const { CliParser } = require('../lib/cli')
|
|
43
|
+
|
|
44
|
+
// Mock parseArgs to simulate command line input with --packageManager
|
|
45
|
+
const originalParseArgs = require('node:util').parseArgs
|
|
46
|
+
require('node:util').parseArgs = jest.fn().mockReturnValue({
|
|
47
|
+
values: { packageManager: 'yarn' }, // CLI says yarn
|
|
48
|
+
positionals: ['install', 'express']
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const result = CliParser.parseArgsFull()
|
|
52
|
+
|
|
53
|
+
// Environment variable should win over CLI option
|
|
54
|
+
expect(result.packageManager).toBe('pnpm')
|
|
55
|
+
|
|
56
|
+
// Restore
|
|
57
|
+
require('node:util').parseArgs = originalParseArgs
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('should fall back to command line option when NPQ_PKG_MGR is not set', () => {
|
|
61
|
+
// Ensure environment variable is not set
|
|
62
|
+
delete process.env.NPQ_PKG_MGR
|
|
63
|
+
|
|
64
|
+
jest.resetModules()
|
|
65
|
+
const { CliParser } = require('../lib/cli')
|
|
66
|
+
|
|
67
|
+
const originalParseArgs = require('node:util').parseArgs
|
|
68
|
+
require('node:util').parseArgs = jest.fn().mockReturnValue({
|
|
69
|
+
values: { packageManager: 'yarn' },
|
|
70
|
+
positionals: ['install', 'express']
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const result = CliParser.parseArgsFull()
|
|
74
|
+
|
|
75
|
+
expect(result.packageManager).toBe('yarn')
|
|
76
|
+
|
|
77
|
+
// Restore
|
|
78
|
+
require('node:util').parseArgs = originalParseArgs
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('should fall back to npm default when neither env var nor CLI option provided', () => {
|
|
82
|
+
delete process.env.NPQ_PKG_MGR
|
|
83
|
+
|
|
84
|
+
jest.resetModules()
|
|
85
|
+
const { CliParser } = require('../lib/cli')
|
|
86
|
+
|
|
87
|
+
const originalParseArgs = require('node:util').parseArgs
|
|
88
|
+
require('node:util').parseArgs = jest.fn().mockReturnValue({
|
|
89
|
+
values: {}, // No package manager specified
|
|
90
|
+
positionals: ['install', 'express']
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const result = CliParser.parseArgsFull()
|
|
94
|
+
|
|
95
|
+
expect(result.packageManager).toBe('npm')
|
|
96
|
+
|
|
97
|
+
// Restore
|
|
98
|
+
require('node:util').parseArgs = originalParseArgs
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('should handle empty NPQ_PKG_MGR environment variable', () => {
|
|
102
|
+
process.env.NPQ_PKG_MGR = '' // Empty string
|
|
103
|
+
|
|
104
|
+
jest.resetModules()
|
|
105
|
+
const { CliParser } = require('../lib/cli')
|
|
106
|
+
|
|
107
|
+
const originalParseArgs = require('node:util').parseArgs
|
|
108
|
+
require('node:util').parseArgs = jest.fn().mockReturnValue({
|
|
109
|
+
values: { packageManager: 'yarn' },
|
|
110
|
+
positionals: ['install', 'express']
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const result = CliParser.parseArgsFull()
|
|
114
|
+
|
|
115
|
+
// Empty string should be falsy, so fall back to CLI option
|
|
116
|
+
expect(result.packageManager).toBe('yarn')
|
|
117
|
+
|
|
118
|
+
// Restore
|
|
119
|
+
require('node:util').parseArgs = originalParseArgs
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// __tests__/env-var-integration.test.js
|
|
2
|
+
|
|
3
|
+
// Test the NPQ_PKG_MGR environment variable functionality
|
|
4
|
+
// This tests the key logic change: process.env.NPQ_PKG_MGR || values.packageManager || values.pkgMgr || 'npm'
|
|
5
|
+
|
|
6
|
+
const packageManager = require('../lib/packageManager')
|
|
7
|
+
|
|
8
|
+
const childProcess = require('child_process')
|
|
9
|
+
|
|
10
|
+
jest.mock('child_process', () => {
|
|
11
|
+
return {
|
|
12
|
+
spawn: jest.fn((cmd, args, options) => {
|
|
13
|
+
return { pid: 12345 }
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('NPQ_PKG_MGR Environment Variable Integration', () => {
|
|
19
|
+
let originalNPQ_PKG_MGR
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
originalNPQ_PKG_MGR = process.env.NPQ_PKG_MGR
|
|
23
|
+
delete process.env.NPQ_PKG_MGR
|
|
24
|
+
childProcess.spawn.mockClear()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
if (originalNPQ_PKG_MGR !== undefined) {
|
|
29
|
+
process.env.NPQ_PKG_MGR = originalNPQ_PKG_MGR
|
|
30
|
+
} else {
|
|
31
|
+
delete process.env.NPQ_PKG_MGR
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('package manager process should handle pnpm correctly', async () => {
|
|
36
|
+
process.argv = ['node', 'npq', 'install', 'fastify']
|
|
37
|
+
|
|
38
|
+
// Test that the package manager can spawn pnpm (which would be passed from CLI parsing)
|
|
39
|
+
await packageManager.process('pnpm')
|
|
40
|
+
|
|
41
|
+
expect(childProcess.spawn).toHaveBeenCalledWith('pnpm', ['install', 'fastify'], {
|
|
42
|
+
stdio: 'inherit',
|
|
43
|
+
shell: true
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('package manager process should handle yarn correctly', async () => {
|
|
48
|
+
process.argv = ['node', 'npq', 'install', 'express', 'lodash']
|
|
49
|
+
|
|
50
|
+
await packageManager.process('yarn')
|
|
51
|
+
|
|
52
|
+
expect(childProcess.spawn).toHaveBeenCalledWith('yarn', ['install', 'express', 'lodash'], {
|
|
53
|
+
stdio: 'inherit',
|
|
54
|
+
shell: true
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('package manager process should handle various package managers', async () => {
|
|
59
|
+
const packageManagers = ['npm', 'yarn', 'pnpm', 'bun']
|
|
60
|
+
|
|
61
|
+
for (const pm of packageManagers) {
|
|
62
|
+
process.argv = ['node', 'npq', 'install', 'test-package']
|
|
63
|
+
|
|
64
|
+
await packageManager.process(pm)
|
|
65
|
+
|
|
66
|
+
expect(childProcess.spawn).toHaveBeenCalledWith(pm, ['install', 'test-package'], {
|
|
67
|
+
stdio: 'inherit',
|
|
68
|
+
shell: true
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
childProcess.spawn.mockClear()
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('should demonstrate NPQ_PKG_MGR environment variable precedence logic', () => {
|
|
76
|
+
// This test documents the specific logic that was implemented:
|
|
77
|
+
// packageManager: process.env.NPQ_PKG_MGR || values.packageManager || values.pkgMgr || 'npm'
|
|
78
|
+
|
|
79
|
+
// Test 1: Environment variable takes precedence
|
|
80
|
+
process.env.NPQ_PKG_MGR = 'pnpm'
|
|
81
|
+
const values1 = { packageManager: 'yarn', pkgMgr: 'npm' }
|
|
82
|
+
const result1 = process.env.NPQ_PKG_MGR || values1.packageManager || values1.pkgMgr || 'npm'
|
|
83
|
+
expect(result1).toBe('pnpm')
|
|
84
|
+
|
|
85
|
+
// Test 2: Falls back to packageManager when env var not set
|
|
86
|
+
delete process.env.NPQ_PKG_MGR
|
|
87
|
+
const values2 = { packageManager: 'yarn', pkgMgr: 'npm' }
|
|
88
|
+
const result2 = process.env.NPQ_PKG_MGR || values2.packageManager || values2.pkgMgr || 'npm'
|
|
89
|
+
expect(result2).toBe('yarn')
|
|
90
|
+
|
|
91
|
+
// Test 3: Falls back to pkgMgr when env var and packageManager not set
|
|
92
|
+
delete process.env.NPQ_PKG_MGR
|
|
93
|
+
const values3 = { pkgMgr: 'pnpm' }
|
|
94
|
+
const result3 = process.env.NPQ_PKG_MGR || values3.packageManager || values3.pkgMgr || 'npm'
|
|
95
|
+
expect(result3).toBe('pnpm')
|
|
96
|
+
|
|
97
|
+
// Test 4: Falls back to npm default when nothing is set
|
|
98
|
+
delete process.env.NPQ_PKG_MGR
|
|
99
|
+
const values4 = {}
|
|
100
|
+
const result4 = process.env.NPQ_PKG_MGR || values4.packageManager || values4.pkgMgr || 'npm'
|
|
101
|
+
expect(result4).toBe('npm')
|
|
102
|
+
|
|
103
|
+
// Test 5: Empty string environment variable falls back to CLI options
|
|
104
|
+
process.env.NPQ_PKG_MGR = ''
|
|
105
|
+
const values5 = { packageManager: 'yarn' }
|
|
106
|
+
const result5 = process.env.NPQ_PKG_MGR || values5.packageManager || values5.pkgMgr || 'npm'
|
|
107
|
+
expect(result5).toBe('yarn')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -77,3 +77,25 @@ test("package manager spawns successfully and ignore npq's own internal commands
|
|
|
77
77
|
expect(childProcess.spawn.mock.calls[0][1]).toEqual(['install', 'semver', 'express'])
|
|
78
78
|
childProcess.spawn.mockReset()
|
|
79
79
|
})
|
|
80
|
+
|
|
81
|
+
test('package manager spawns with yarn when provided as parameter', async () => {
|
|
82
|
+
process.argv = ['node', 'script name', 'install', 'express']
|
|
83
|
+
await packageManager.process('yarn')
|
|
84
|
+
expect(childProcess.spawn).toHaveBeenCalled()
|
|
85
|
+
expect(childProcess.spawn.mock.calls.length).toBe(1)
|
|
86
|
+
expect(childProcess.spawn.mock.calls[0][0]).toBe('yarn')
|
|
87
|
+
|
|
88
|
+
expect(childProcess.spawn.mock.calls[0][1]).toEqual(['install', 'express'])
|
|
89
|
+
childProcess.spawn.mockReset()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('package manager spawns with pnpm when provided as parameter', async () => {
|
|
93
|
+
process.argv = ['node', 'script name', 'install', 'lodash']
|
|
94
|
+
await packageManager.process('pnpm')
|
|
95
|
+
expect(childProcess.spawn).toHaveBeenCalled()
|
|
96
|
+
expect(childProcess.spawn.mock.calls.length).toBe(1)
|
|
97
|
+
expect(childProcess.spawn.mock.calls[0][0]).toBe('pnpm')
|
|
98
|
+
|
|
99
|
+
expect(childProcess.spawn.mock.calls[0][1]).toEqual(['install', 'lodash'])
|
|
100
|
+
childProcess.spawn.mockReset()
|
|
101
|
+
})
|
package/bin/npq-hero.js
CHANGED
|
@@ -16,7 +16,10 @@ const { promiseThrottleHelper } = require('../lib/helpers/promiseThrottler')
|
|
|
16
16
|
const PACKAGE_MANAGER_TOOL = process.env.NPQ_PKG_MGR
|
|
17
17
|
|
|
18
18
|
const cliArgs = CliParser.parseArgsMinimal()
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
const silentModeNoPackages = !cliArgs || !cliArgs.packages || cliArgs.packages.length === 0
|
|
21
|
+
|
|
22
|
+
const isInteractive = cliSupport.isInteractiveTerminal() && !silentModeNoPackages
|
|
20
23
|
const spinner = isInteractive ? new Spinner({ text: 'Initiating...' }) : null
|
|
21
24
|
|
|
22
25
|
if (spinner) {
|
package/lib/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ class CliParser {
|
|
|
17
17
|
process.exit(errorCode || -1)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
static _extractPackagesFromPositionals(positionals) {
|
|
20
|
+
static _extractPackagesFromPositionals(positionals, earlyExitNoInstall = false) {
|
|
21
21
|
let packages = []
|
|
22
22
|
if (positionals.length > 0) {
|
|
23
23
|
const command = positionals[0]
|
|
@@ -38,6 +38,12 @@ class CliParser {
|
|
|
38
38
|
packages = positionals.slice(1)
|
|
39
39
|
break
|
|
40
40
|
default:
|
|
41
|
+
if (earlyExitNoInstall) {
|
|
42
|
+
// If no install command, exit early
|
|
43
|
+
// needed for npq-hero command which only runs on 'install' use-cases of npm
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
// Treat first positional as package if no explicit command
|
|
42
48
|
packages = positionals
|
|
43
49
|
break
|
|
@@ -103,7 +109,7 @@ curated by Liran Tal at https://github.com/lirantal/npq`)
|
|
|
103
109
|
|
|
104
110
|
return {
|
|
105
111
|
packages: normalizedPackages,
|
|
106
|
-
packageManager: values.packageManager || values.pkgMgr || 'npm',
|
|
112
|
+
packageManager: values.packageManager || values.pkgMgr || process.env.NPQ_PKG_MGR || 'npm',
|
|
107
113
|
dryRun: values['dry-run'] || false,
|
|
108
114
|
plain: values.plain || false
|
|
109
115
|
}
|
|
@@ -113,12 +119,19 @@ curated by Liran Tal at https://github.com/lirantal/npq`)
|
|
|
113
119
|
const config = {
|
|
114
120
|
allowPositionals: true,
|
|
115
121
|
strict: false,
|
|
116
|
-
options: {
|
|
122
|
+
options: {
|
|
123
|
+
install: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
short: 'i',
|
|
126
|
+
default: 'install'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
const { positionals } = parseArgs(config)
|
|
120
132
|
|
|
121
|
-
const
|
|
133
|
+
const earlyExitNoInstall = true
|
|
134
|
+
const normalizedPackages = this._extractPackagesFromPositionals(positionals, earlyExitNoInstall)
|
|
122
135
|
|
|
123
136
|
return {
|
|
124
137
|
packages: normalizedPackages
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "npq",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "marshall your npm/npm package installs with high quality and class 🎖",
|
|
5
5
|
"bin": {
|
|
6
6
|
"npq": "./bin/npq.js",
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"collectCoverage": true,
|
|
73
73
|
"coverageThreshold": {
|
|
74
74
|
"global": {
|
|
75
|
-
"branches":
|
|
76
|
-
"functions":
|
|
77
|
-
"lines":
|
|
78
|
-
"statements":
|
|
75
|
+
"branches": 75,
|
|
76
|
+
"functions": 75,
|
|
77
|
+
"lines": 75,
|
|
78
|
+
"statements": 75
|
|
79
79
|
},
|
|
80
80
|
"scripts/*": {
|
|
81
81
|
"branches": 60,
|