npq 3.10.1 → 3.10.2

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.
Files changed (46) hide show
  1. package/package.json +7 -1
  2. package/.editorconfig +0 -49
  3. package/.github/ISSUE_TEMPLATE.md +0 -32
  4. package/.github/Logo Horizontal.png +0 -0
  5. package/.github/Logo Vertical.png +0 -0
  6. package/.github/Logo.png +0 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +0 -35
  8. package/.github/npq-demo-1.gif +0 -0
  9. package/.github/npq-demo-1.png +0 -0
  10. package/.github/npq-demo.gif +0 -0
  11. package/.github/npq.mov +0 -0
  12. package/.github/npq.mp4 +0 -0
  13. package/.github/npq.png +0 -0
  14. package/.github/workflows/automerge.yml +0 -39
  15. package/.github/workflows/main.yml +0 -58
  16. package/.husky/commit-msg +0 -4
  17. package/.husky/post-merge +0 -4
  18. package/.husky/pre-commit +0 -4
  19. package/.husky/pre-push +0 -4
  20. package/.prettierrc.js +0 -9
  21. package/CHANGELOG.md +0 -27
  22. package/CODE_OF_CONDUCT.md +0 -73
  23. package/CONTRIBUTING.md +0 -55
  24. package/SECURITY.md +0 -23
  25. package/__tests__/__fixtures__/test.marshall.js +0 -58
  26. package/__tests__/cli.parser.test.js +0 -121
  27. package/__tests__/cli.test.js +0 -110
  28. package/__tests__/cliPrompt.test.js +0 -409
  29. package/__tests__/env-var-integration.test.js +0 -109
  30. package/__tests__/marshalls.base.test.js +0 -113
  31. package/__tests__/marshalls.classMethods.test.js +0 -34
  32. package/__tests__/marshalls.expiredDomains.test.js +0 -100
  33. package/__tests__/marshalls.newBin.test.js +0 -441
  34. package/__tests__/marshalls.provenance.test.js +0 -383
  35. package/__tests__/marshalls.repo.test.js +0 -126
  36. package/__tests__/marshalls.signatures.test.js +0 -132
  37. package/__tests__/marshalls.snyk.test.js +0 -294
  38. package/__tests__/marshalls.tasks.test.js +0 -130
  39. package/__tests__/marshalls.typosquatting.test.js +0 -76
  40. package/__tests__/marshalls.version-maturity.test.js +0 -194
  41. package/__tests__/mocks/registryPackageOk.mock.json +0 -146
  42. package/__tests__/mocks/registryPackageUnpublished.mock.json +0 -31
  43. package/__tests__/packageManager.test.js +0 -93
  44. package/__tests__/packageRepoUtils.test.js +0 -251
  45. package/__tests__/reportResults.test.js +0 -772
  46. package/__tests__/scripts.test.js +0 -146
@@ -1,110 +0,0 @@
1
- // __tests__/cli.test.js
2
-
3
- // Mock dependencies. These are hoisted by Jest.
4
- jest.mock('../lib/cli', () => ({
5
- CliParser: {
6
- parseArgsFull: jest.fn(),
7
- exit: jest.fn()
8
- }
9
- }))
10
- jest.mock('../lib/helpers/cliSupportHandler', () => ({
11
- isEnvSupport: jest.fn().mockReturnValue(true),
12
- isInteractiveTerminal: jest.fn(),
13
- noSupportError: jest.fn()
14
- }))
15
-
16
- // Create a shared mock for the spinner instance that we can inspect in tests.
17
- const mockSpinnerInstance = {
18
- start: jest.fn(),
19
- stop: jest.fn()
20
- }
21
- jest.mock('../lib/helpers/cliSpinner', () => ({
22
- // The Spinner property is a mock constructor that returns our shared mock instance.
23
- Spinner: jest.fn().mockImplementation(() => mockSpinnerInstance)
24
- }))
25
-
26
- jest.mock('../lib/helpers/sourcePackages', () => ({
27
- getProjectPackages: jest.fn().mockResolvedValue(['express'])
28
- }))
29
- jest.mock('../lib/marshall', () =>
30
- jest.fn(() => ({
31
- process: jest.fn().mockResolvedValue({})
32
- }))
33
- )
34
- jest.mock('../lib/helpers/reportResults', () => ({
35
- reportResults: jest.fn().mockReturnValue({ countErrors: 0, countWarnings: 0 })
36
- }))
37
- jest.mock('../lib/packageManager', () => ({
38
- process: jest.fn()
39
- }))
40
- jest.mock('../lib/helpers/cliPrompt.js', () => ({
41
- prompt: jest.fn().mockResolvedValue({ install: true }),
42
- autoContinue: jest.fn().mockResolvedValue({ install: true })
43
- }))
44
-
45
- describe('npq CLI script', () => {
46
- beforeEach(() => {
47
- // Reset modules to ensure mocks are fresh for each test.
48
- jest.resetModules()
49
- // Clear mock history on the shared instance and the constructor.
50
- const { Spinner } = require('../lib/helpers/cliSpinner')
51
- mockSpinnerInstance.start.mockClear()
52
- mockSpinnerInstance.stop.mockClear()
53
- Spinner.mockClear()
54
- })
55
-
56
- test('should initialize and start spinner in interactive mode without --plain flag', async () => {
57
- // Arrange
58
- const { CliParser } = require('../lib/cli')
59
- const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
60
- const { Spinner } = require('../lib/helpers/cliSpinner')
61
-
62
- CliParser.parseArgsFull.mockReturnValue({ packages: ['express'], plain: false, dryRun: true })
63
- isInteractiveTerminal.mockReturnValue(true)
64
-
65
- // Act: Dynamically require the script to run it.
66
- require('../bin/npq.js')
67
- // Wait for async operations in the script to settle.
68
- await new Promise(process.nextTick)
69
-
70
- // Assert
71
- expect(Spinner).toHaveBeenCalledTimes(1)
72
- expect(mockSpinnerInstance.start).toHaveBeenCalledTimes(1)
73
- })
74
-
75
- test('should not initialize spinner when --plain flag is used', async () => {
76
- // Arrange
77
- const { CliParser } = require('../lib/cli')
78
- const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
79
- const { Spinner } = require('../lib/helpers/cliSpinner')
80
-
81
- CliParser.parseArgsFull.mockReturnValue({ packages: ['express'], plain: true, dryRun: true })
82
- isInteractiveTerminal.mockReturnValue(true)
83
-
84
- // Act
85
- require('../bin/npq.js')
86
- await new Promise(process.nextTick)
87
-
88
- // Assert
89
- expect(Spinner).not.toHaveBeenCalled()
90
- expect(mockSpinnerInstance.start).not.toHaveBeenCalled()
91
- })
92
-
93
- test('should not initialize spinner in non-interactive mode', async () => {
94
- // Arrange
95
- const { CliParser } = require('../lib/cli')
96
- const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
97
- const { Spinner } = require('../lib/helpers/cliSpinner')
98
-
99
- CliParser.parseArgsFull.mockReturnValue({ packages: ['express'], plain: false, dryRun: true })
100
- isInteractiveTerminal.mockReturnValue(false)
101
-
102
- // Act
103
- require('../bin/npq.js')
104
- await new Promise(process.nextTick)
105
-
106
- // Assert
107
- expect(Spinner).not.toHaveBeenCalled()
108
- expect(mockSpinnerInstance.start).not.toHaveBeenCalled()
109
- })
110
- })
@@ -1,409 +0,0 @@
1
- const { prompt } = require('../lib/helpers/cliPrompt')
2
- const readline = require('node:readline/promises')
3
-
4
- // Mock readline module
5
- jest.mock('node:readline/promises', () => ({
6
- createInterface: jest.fn()
7
- }))
8
-
9
- describe('cliPrompt', () => {
10
- let mockRl
11
-
12
- beforeEach(() => {
13
- mockRl = {
14
- question: jest.fn(),
15
- close: jest.fn()
16
- }
17
- readline.createInterface.mockReturnValue(mockRl)
18
-
19
- // Mock console.log to avoid output during tests
20
- jest.spyOn(console, 'log').mockImplementation(() => {})
21
- })
22
-
23
- afterEach(() => {
24
- jest.clearAllMocks()
25
- if (console.log.mockRestore) {
26
- console.log.mockRestore()
27
- }
28
- })
29
-
30
- describe('parameter validation', () => {
31
- test('should throw error when message is missing', async () => {
32
- await expect(prompt({ name: 'test' })).rejects.toThrow('Message is required for prompt')
33
- })
34
-
35
- test('should throw error when name is missing', async () => {
36
- await expect(prompt({ message: 'Test message?' })).rejects.toThrow(
37
- 'Name is required for prompt'
38
- )
39
- })
40
-
41
- test('should throw error when both name and message are missing', async () => {
42
- await expect(prompt({})).rejects.toThrow('Message is required for prompt')
43
- })
44
- })
45
-
46
- describe('successful prompts', () => {
47
- test('should return true for "y" input', async () => {
48
- mockRl.question.mockResolvedValue('y')
49
-
50
- const result = await prompt({
51
- name: 'install',
52
- message: 'Do you want to install?'
53
- })
54
-
55
- expect(result).toEqual({ install: true })
56
- expect(mockRl.question).toHaveBeenCalledWith('Do you want to install? (y/N) ')
57
- expect(mockRl.close).toHaveBeenCalled()
58
- })
59
-
60
- test('should return true for "yes" input', async () => {
61
- mockRl.question.mockResolvedValue('yes')
62
-
63
- const result = await prompt({
64
- name: 'confirm',
65
- message: 'Are you sure?'
66
- })
67
-
68
- expect(result).toEqual({ confirm: true })
69
- })
70
-
71
- test('should return false for "n" input', async () => {
72
- mockRl.question.mockResolvedValue('n')
73
-
74
- const result = await prompt({
75
- name: 'proceed',
76
- message: 'Continue?'
77
- })
78
-
79
- expect(result).toEqual({ proceed: false })
80
- })
81
-
82
- test('should return false for "no" input', async () => {
83
- mockRl.question.mockResolvedValue('no')
84
-
85
- const result = await prompt({
86
- name: 'delete',
87
- message: 'Delete files?'
88
- })
89
-
90
- expect(result).toEqual({ delete: false })
91
- })
92
-
93
- test('should handle case-insensitive input', async () => {
94
- mockRl.question.mockResolvedValue('YES')
95
-
96
- const result = await prompt({
97
- name: 'test',
98
- message: 'Test?'
99
- })
100
-
101
- expect(result).toEqual({ test: true })
102
- })
103
-
104
- test('should trim whitespace from input', async () => {
105
- mockRl.question.mockResolvedValue(' y ')
106
-
107
- const result = await prompt({
108
- name: 'test',
109
- message: 'Test?'
110
- })
111
-
112
- expect(result).toEqual({ test: true })
113
- })
114
- })
115
-
116
- describe('default value handling', () => {
117
- test('should use default false when user presses Enter', async () => {
118
- mockRl.question.mockResolvedValue('')
119
-
120
- const result = await prompt({
121
- name: 'install',
122
- message: 'Install package?'
123
- })
124
-
125
- expect(result).toEqual({ install: false })
126
- expect(mockRl.question).toHaveBeenCalledWith('Install package? (y/N) ')
127
- })
128
-
129
- test('should use default true when specified and user presses Enter', async () => {
130
- mockRl.question.mockResolvedValue('')
131
-
132
- const result = await prompt({
133
- name: 'continue',
134
- message: 'Continue process?',
135
- default: true
136
- })
137
-
138
- expect(result).toEqual({ continue: true })
139
- expect(mockRl.question).toHaveBeenCalledWith('Continue process? (Y/n) ')
140
- })
141
-
142
- test('should show correct prompt indicators for default values', async () => {
143
- mockRl.question.mockResolvedValue('y')
144
-
145
- await prompt({
146
- name: 'test',
147
- message: 'Test with default false?',
148
- default: false
149
- })
150
-
151
- expect(mockRl.question).toHaveBeenCalledWith('Test with default false? (y/N) ')
152
- })
153
- })
154
-
155
- describe('invalid input handling', () => {
156
- test('should log error message when invalid input is provided', () => {
157
- // Test the logic path for invalid input without actual recursion
158
- const invalidInputs = ['maybe', '1', 'true', 'false', 'ok', 'sure', 'xyz']
159
-
160
- invalidInputs.forEach((input) => {
161
- const normalized = input.trim().toLowerCase()
162
- const isValidInput = ['y', 'yes', 'n', 'no'].includes(normalized)
163
- expect(isValidInput).toBe(false)
164
- })
165
- })
166
-
167
- test('should recognize valid inputs correctly', () => {
168
- const validInputs = ['y', 'yes', 'n', 'no', 'Y', 'YES', 'N', 'NO', ' y ', ' no ']
169
-
170
- validInputs.forEach((input) => {
171
- const normalized = input.trim().toLowerCase()
172
- const isValidInput = ['y', 'yes', 'n', 'no'].includes(normalized)
173
- expect(isValidInput).toBe(true)
174
- })
175
- })
176
-
177
- test('should handle invalid input and retry successfully', async () => {
178
- // Create a mock that returns invalid input first, then valid input
179
- let callCount = 0
180
- mockRl.question.mockImplementation(() => {
181
- callCount++
182
- if (callCount === 1) {
183
- return Promise.resolve('invalid')
184
- }
185
- return Promise.resolve('y')
186
- })
187
-
188
- // Mock the second readline interface creation for the retry
189
- let interfaceCallCount = 0
190
- const mockRl2 = {
191
- question: jest.fn().mockResolvedValue('y'),
192
- close: jest.fn()
193
- }
194
-
195
- readline.createInterface.mockImplementation(() => {
196
- interfaceCallCount++
197
- if (interfaceCallCount === 1) {
198
- return mockRl
199
- }
200
- return mockRl2
201
- })
202
-
203
- const result = await prompt({
204
- name: 'test',
205
- message: 'Test?'
206
- })
207
-
208
- expect(console.log).toHaveBeenCalledWith('Please answer with y/yes or n/no.')
209
- expect(result).toEqual({ test: true })
210
- expect(mockRl.close).toHaveBeenCalled()
211
- expect(mockRl2.close).toHaveBeenCalled()
212
- })
213
- })
214
-
215
- describe('readline interface management', () => {
216
- test('should create readline interface with correct options', async () => {
217
- mockRl.question.mockResolvedValue('y')
218
-
219
- await prompt({
220
- name: 'test',
221
- message: 'Test?'
222
- })
223
-
224
- expect(readline.createInterface).toHaveBeenCalledWith({
225
- input: process.stdin,
226
- output: process.stdout
227
- })
228
- })
229
-
230
- test('should close readline interface even if error occurs', async () => {
231
- mockRl.question.mockRejectedValue(new Error('Test error'))
232
-
233
- await expect(
234
- prompt({
235
- name: 'test',
236
- message: 'Test?'
237
- })
238
- ).rejects.toThrow('Test error')
239
-
240
- expect(mockRl.close).toHaveBeenCalled()
241
- })
242
- })
243
-
244
- describe('return value format', () => {
245
- test('should return object with specified property name', async () => {
246
- mockRl.question.mockResolvedValue('y')
247
-
248
- const result = await prompt({
249
- name: 'customProperty',
250
- message: 'Test?'
251
- })
252
-
253
- expect(result).toHaveProperty('customProperty')
254
- expect(result.customProperty).toBe(true)
255
- expect(Object.keys(result)).toHaveLength(1)
256
- })
257
-
258
- test('should handle different property names correctly', async () => {
259
- mockRl.question.mockResolvedValue('n')
260
-
261
- const result = await prompt({
262
- name: 'shouldProceed',
263
- message: 'Proceed?'
264
- })
265
-
266
- expect(result).toEqual({ shouldProceed: false })
267
- })
268
- })
269
-
270
- describe('autoContinue', () => {
271
- const { autoContinue } = require('../lib/helpers/cliPrompt')
272
- let mockWrite, originalWrite
273
-
274
- beforeEach(() => {
275
- // Mock process.stdout.write to capture output
276
- originalWrite = process.stdout.write
277
- mockWrite = jest.fn()
278
- process.stdout.write = mockWrite
279
-
280
- // Mock setTimeout to speed up tests
281
- jest.useFakeTimers()
282
- })
283
-
284
- afterEach(() => {
285
- // Restore original stdout.write
286
- process.stdout.write = originalWrite
287
- jest.useRealTimers()
288
- })
289
-
290
- test('should return object with specified property name', async () => {
291
- const promise = autoContinue({
292
- name: 'install',
293
- message: 'Auto-installing in ',
294
- timeInSeconds: 2
295
- })
296
-
297
- // Fast-forward all timers
298
- jest.runAllTimers()
299
-
300
- const result = await promise
301
- expect(result).toEqual({ install: true })
302
- })
303
-
304
- test('should handle custom timeInSeconds', async () => {
305
- const promise = autoContinue({
306
- name: 'proceed',
307
- message: 'Proceeding in ',
308
- timeInSeconds: 3
309
- })
310
-
311
- // Fast-forward all timers
312
- jest.runAllTimers()
313
-
314
- const result = await promise
315
- expect(result).toEqual({ proceed: true })
316
- })
317
-
318
- test('should use default timeInSeconds of 5 when not specified', async () => {
319
- const promise = autoContinue({
320
- name: 'default',
321
- message: 'Starting in '
322
- })
323
-
324
- // Fast-forward all timers
325
- jest.runAllTimers()
326
-
327
- const result = await promise
328
- expect(result).toEqual({ default: true })
329
- }, 10000) // 10 second timeout
330
-
331
- test('should write countdown to stdout', async () => {
332
- const promise = autoContinue({
333
- name: 'test',
334
- message: 'Testing in ',
335
- timeInSeconds: 3
336
- })
337
-
338
- // Fast-forward all timers
339
- jest.runAllTimers()
340
-
341
- await promise
342
-
343
- // Should write initial message with starting number
344
- expect(mockWrite).toHaveBeenCalledWith('Testing in 3')
345
- })
346
-
347
- test('should handle single digit countdown correctly', async () => {
348
- const promise = autoContinue({
349
- name: 'test',
350
- message: 'Starting in ',
351
- timeInSeconds: 2
352
- })
353
-
354
- // Fast-forward all timers
355
- jest.runAllTimers()
356
-
357
- await promise
358
-
359
- // Should write initial message
360
- expect(mockWrite).toHaveBeenCalledWith('Starting in 2')
361
- })
362
-
363
- test('should handle double digit to single digit countdown with padding', async () => {
364
- const promise = autoContinue({
365
- name: 'test',
366
- message: 'Waiting ',
367
- timeInSeconds: 10
368
- })
369
-
370
- // Fast-forward all timers
371
- jest.runAllTimers()
372
-
373
- await promise
374
-
375
- // Should write initial message with double digit
376
- expect(mockWrite).toHaveBeenCalledWith('Waiting 10')
377
- }, 15000) // 15 second timeout
378
-
379
- test('should call console.log at the end', async () => {
380
- const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {})
381
-
382
- const promise = autoContinue({
383
- name: 'test',
384
- message: 'Done in ',
385
- timeInSeconds: 1
386
- })
387
-
388
- // Fast-forward all timers
389
- jest.runAllTimers()
390
-
391
- await promise
392
-
393
- expect(mockConsoleLog).toHaveBeenCalledWith()
394
- mockConsoleLog.mockRestore()
395
- })
396
-
397
- test('should handle default parameters', async () => {
398
- const promise = autoContinue()
399
-
400
- // Fast-forward all timers
401
- jest.runAllTimers()
402
-
403
- const result = await promise
404
-
405
- // Should handle undefined name gracefully
406
- expect(result).toEqual({ undefined: true })
407
- }, 10000) // 10 second timeout
408
- })
409
- })
@@ -1,109 +0,0 @@
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, 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
- })