npq 3.8.1 → 3.9.1

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 (48) hide show
  1. package/.github/npq.mov +0 -0
  2. package/.github/npq.mp4 +0 -0
  3. package/.github/npq.png +0 -0
  4. package/.github/workflows/main.yml +2 -2
  5. package/README.md +18 -9
  6. package/__tests__/__fixtures__/test.marshall.js +5 -6
  7. package/__tests__/cli.test.js +110 -0
  8. package/__tests__/cliPrompt.test.js +409 -0
  9. package/__tests__/marshalls.base.test.js +1 -1
  10. package/__tests__/marshalls.classMethods.test.js +11 -14
  11. package/__tests__/marshalls.expiredDomains.test.js +2 -2
  12. package/__tests__/marshalls.provenance.test.js +181 -4
  13. package/__tests__/marshalls.signatures.test.js +0 -4
  14. package/__tests__/marshalls.snyk.test.js +294 -0
  15. package/__tests__/marshalls.tasks.test.js +48 -24
  16. package/__tests__/marshalls.typosquatting.test.js +16 -9
  17. package/__tests__/reportResults.test.js +772 -0
  18. package/__tests__/scripts.test.js +2 -2
  19. package/bin/npq-hero.js +73 -20
  20. package/bin/npq.js +113 -21
  21. package/lib/cli.js +140 -24
  22. package/lib/helpers/cliPrompt.js +97 -0
  23. package/lib/helpers/cliSpinner.js +104 -0
  24. package/lib/helpers/cliSupportHandler.js +50 -7
  25. package/lib/helpers/packageRepoUtils.js +5 -0
  26. package/lib/helpers/promiseThrottler.js +96 -0
  27. package/lib/helpers/reportResults.js +304 -0
  28. package/lib/helpers/sourcePackages.js +36 -0
  29. package/lib/marshall.js +42 -65
  30. package/lib/marshalls/age.marshall.js +3 -2
  31. package/lib/marshalls/author.marshall.js +36 -7
  32. package/lib/marshalls/baseMarshall.js +9 -17
  33. package/lib/marshalls/deprecation.marshall.js +43 -0
  34. package/lib/marshalls/downloads.marshall.js +1 -1
  35. package/lib/marshalls/expiredDomains.marshall.js +7 -5
  36. package/lib/marshalls/index.js +110 -80
  37. package/lib/marshalls/license.marshall.js +1 -1
  38. package/lib/marshalls/newbin.marshall.js +1 -2
  39. package/lib/marshalls/provenance.marshall.js +152 -33
  40. package/lib/marshalls/repo.marshall.js +6 -5
  41. package/lib/marshalls/scripts.marshall.js +1 -1
  42. package/lib/marshalls/signatures.marshall.js +26 -2
  43. package/lib/marshalls/snyk.marshall.js +61 -15
  44. package/lib/marshalls/typosquatting.marshall.js +8 -5
  45. package/lib/marshalls/version-maturity.marshall.js +1 -1
  46. package/package.json +16 -19
  47. package/scripts/postinstall.js +10 -11
  48. package/lib/cliCommons.js +0 -80
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const color = require('kleur')
3
+ const { styleText } = require('node:util')
4
+
4
5
  // eslint-disable-next-line security/detect-child-process
5
6
  const childProcess = require('child_process')
6
7
  const semver = require('semver')
@@ -9,17 +10,22 @@ const DEFAULT_PKGMGR = process.env.NPQ_PKG_MGR || 'npm'
9
10
 
10
11
  const nodeVersion = process.versions.node
11
12
 
12
- module.exports.isEnvSupport = function () {
13
- if (!semver.satisfies(nodeVersion, '>=7.6.0')) {
13
+ function isEnvSupport() {
14
+ if (!semver.satisfies(nodeVersion, '>=20.13.0')) {
14
15
  return false
15
16
  }
16
17
 
17
18
  return true
18
19
  }
19
20
 
20
- module.exports.noSupportError = function (failFast) {
21
- // eslint-disable-next-line no-console
22
- console.error(color.red('error:'), 'npq suppressed due to old node version')
21
+ function noSupportError(failFast) {
22
+ if (isInteractiveTerminal()) {
23
+ // eslint-disable-next-line no-console
24
+ console.error(styleText('red', 'error:'), 'npq suppressed due to old node version')
25
+ } else {
26
+ // eslint-disable-next-line no-console
27
+ console.error('error: npq suppressed due to old node version')
28
+ }
23
29
 
24
30
  if (failFast === true) {
25
31
  process.exit(-1)
@@ -28,7 +34,7 @@ module.exports.noSupportError = function (failFast) {
28
34
  return true
29
35
  }
30
36
 
31
- module.exports.packageManagerPassthrough = function () {
37
+ function packageManagerPassthrough() {
32
38
  const result = childProcess.spawnSync(DEFAULT_PKGMGR, process.argv.slice(2), {
33
39
  stdio: 'inherit',
34
40
  shell: true
@@ -36,3 +42,40 @@ module.exports.packageManagerPassthrough = function () {
36
42
 
37
43
  process.exit(result.status)
38
44
  }
45
+
46
+ /**
47
+ * Check if we're running in an interactive terminal environment
48
+ * @returns {boolean} True if interactive, false if in CI/build environment
49
+ */
50
+ function isInteractiveTerminal() {
51
+ // Check common CI environment variables
52
+ const ciEnvVars = [
53
+ 'CI',
54
+ 'CONTINUOUS_INTEGRATION',
55
+ 'BUILD_NUMBER',
56
+ 'RUN_ID',
57
+ 'GITHUB_ACTIONS',
58
+ 'GITLAB_CI',
59
+ 'TRAVIS',
60
+ 'CIRCLECI',
61
+ 'JENKINS_URL',
62
+ 'TEAMCITY_VERSION',
63
+ 'TF_BUILD'
64
+ ]
65
+
66
+ // If any CI environment variable is set, we're likely in a CI environment
67
+ const isCI = ciEnvVars.some((envVar) => process.env[envVar])
68
+
69
+ // Check if stdout is a TTY (terminal)
70
+ const isTTY = Boolean(process.stdout.isTTY)
71
+
72
+ // We're interactive if we're in a TTY and not in CI
73
+ return isTTY && !isCI
74
+ }
75
+
76
+ module.exports = {
77
+ isEnvSupport,
78
+ noSupportError,
79
+ packageManagerPassthrough,
80
+ isInteractiveTerminal
81
+ }
@@ -52,6 +52,11 @@ class PackageRepoUtils {
52
52
  return semver.coerce(version)
53
53
  }
54
54
 
55
+ async isPackageInAllowList(packageName) {
56
+ const allowList = ['npq', 'ai', 'bun', 'deno']
57
+ return allowList.includes(packageName)
58
+ }
59
+
55
60
  async getSemVer(packageName, packageVersion) {
56
61
  if (semver.valid(packageVersion)) {
57
62
  return packageVersion
@@ -0,0 +1,96 @@
1
+ // Example use-case
2
+ //
3
+ // Using the new throttling mechanism - each audit will be throttled globally
4
+ // const throttledAudits = pkgs.map(pkg =>
5
+ // promiseThrottle(() => auditPackage(pkg), 1, 1000) // max 1 concurrent, 1 second delay
6
+ // )
7
+ // const res = await Promise.all(throttledAudits)
8
+
9
+ /**
10
+ * A function that throttles promises to avoid overwhelming remote APIs
11
+ * rate limits. Uses a global throttling mechanism that works across
12
+ * multiple concurrent calls.
13
+ *
14
+ * @param {Function} promiseFunction - Function that returns a promise
15
+ * @param {number} maxConcurrent - Maximum concurrent promises (global setting)
16
+ * @param {number} delay - Minimum delay between requests in milliseconds
17
+ */
18
+ async function promiseThrottleHelper(promiseFunction, maxConcurrent = 5, delay = 0) {
19
+ const throttler = PromiseThrottler.getInstance()
20
+
21
+ // Configure global throttling settings
22
+ throttler.configure(maxConcurrent, delay)
23
+
24
+ // Use the global throttler
25
+ return throttler.throttle(promiseFunction)
26
+ }
27
+
28
+ /**
29
+ * A throttling class that limits concurrent promise execution across all instances
30
+ * to avoid overwhelming remote APIs with rate limits.
31
+ */
32
+ class PromiseThrottler {
33
+ constructor() {
34
+ if (!PromiseThrottler.instance) {
35
+ this.runningCount = 0
36
+ this.queue = []
37
+ this.maxConcurrent = 5
38
+ this.minDelay = 0
39
+ PromiseThrottler.instance = this
40
+ }
41
+ return PromiseThrottler.instance
42
+ }
43
+
44
+ static getInstance() {
45
+ if (!PromiseThrottler.instance) {
46
+ new PromiseThrottler()
47
+ }
48
+ return PromiseThrottler.instance
49
+ }
50
+
51
+ configure(maxConcurrent = 5, minDelay = 0) {
52
+ this.maxConcurrent = maxConcurrent
53
+ this.minDelay = minDelay
54
+ }
55
+
56
+ async processQueue() {
57
+ while (this.queue.length > 0 && this.runningCount < this.maxConcurrent) {
58
+ const { promiseFunction, resolve, reject } = this.queue.shift()
59
+ this.runningCount++
60
+
61
+ try {
62
+ const startTime = Date.now()
63
+ const result = await promiseFunction()
64
+
65
+ // Ensure minimum delay between requests
66
+ if (this.minDelay > 0) {
67
+ const elapsed = Date.now() - startTime
68
+ const remainingDelay = this.minDelay - elapsed
69
+ if (remainingDelay > 0) {
70
+ await new Promise((r) => setTimeout(r, remainingDelay))
71
+ }
72
+ }
73
+
74
+ resolve(result)
75
+ } catch (error) {
76
+ reject(error)
77
+ } finally {
78
+ this.runningCount--
79
+ // Process next items in queue
80
+ setImmediate(() => this.processQueue())
81
+ }
82
+ }
83
+ }
84
+
85
+ async throttle(promiseFunction) {
86
+ return new Promise((resolve, reject) => {
87
+ this.queue.push({ promiseFunction, resolve, reject })
88
+ this.processQueue()
89
+ })
90
+ }
91
+ }
92
+
93
+ module.exports = {
94
+ promiseThrottleHelper,
95
+ PromiseThrottler
96
+ }
@@ -0,0 +1,304 @@
1
+ const { styleText } = require('node:util')
2
+ const { marshallCategories } = require('../marshalls/constants')
3
+ const { isInteractiveTerminal } = require('./cliSupportHandler')
4
+
5
+ /**
6
+ * Get terminal width, with fallback to a reasonable default
7
+ * @returns {number} Terminal width in columns
8
+ */
9
+ function getTerminalWidth() {
10
+ try {
11
+ if (process.stdout.isTTY && process.stdout.getWindowSize) {
12
+ const [columns] = process.stdout.getWindowSize()
13
+ return columns
14
+ }
15
+ } catch (error) {
16
+ // Fallback if terminal size detection fails
17
+ }
18
+ return 80 // Default fallback width
19
+ }
20
+
21
+ /**
22
+ * Strip ANSI escape codes from a string to get its actual display length
23
+ * @param {string} str - String that may contain ANSI codes
24
+ * @returns {number} Actual display length
25
+ */
26
+ function getDisplayLength(str) {
27
+ // Remove ANSI escape sequences to get actual character count
28
+ return str.replace(/\u001b\[[0-9;]*m/g, '').length
29
+ }
30
+
31
+ /**
32
+ * Wrap text to fit within terminal width while maintaining indentation
33
+ * @param {string} text - Text to wrap
34
+ * @param {number} maxWidth - Maximum width for each line
35
+ * @param {string} indent - Indentation string for continuation lines
36
+ * @returns {string} Wrapped text with proper indentation
37
+ */
38
+ function wrapTextWithIndent(text, maxWidth, indent) {
39
+ if (getDisplayLength(text) <= maxWidth) {
40
+ return text
41
+ }
42
+
43
+ const words = text.split(' ')
44
+ const lines = []
45
+ let currentLine = ''
46
+
47
+ for (const word of words) {
48
+ const testLine = currentLine ? `${currentLine} ${word}` : word
49
+
50
+ if (getDisplayLength(testLine) <= maxWidth) {
51
+ currentLine = testLine
52
+ } else {
53
+ if (currentLine) {
54
+ lines.push(currentLine)
55
+ currentLine = word
56
+ } else {
57
+ // Single word is longer than maxWidth, break it
58
+ lines.push(word)
59
+ currentLine = ''
60
+ }
61
+ }
62
+ }
63
+
64
+ if (currentLine) {
65
+ lines.push(currentLine)
66
+ }
67
+
68
+ // Join lines with proper indentation
69
+ return lines
70
+ .map((line, index) => {
71
+ return index === 0 ? line : `\n${indent}${line}`
72
+ })
73
+ .join('')
74
+ }
75
+
76
+ /**
77
+ * Report scan results to the console with colored formatting
78
+ * @param {Array} marshallResults - Array of package scan results
79
+ * @param {string} results[].pkg - Package name
80
+ * @param {Array<Array>} results[].errors - Array of error arrays, each containing error objects with pkg and message
81
+ * @param {Array<Array>} results[].warnings - Array of warning arrays, each containing warning objects with pkg and message
82
+ */
83
+ function reportResults(marshallResults, { plain = false } = {}) {
84
+ // Convert results to issues per package
85
+ const results = marshallResultsToIssuesPerPackage(marshallResults)
86
+
87
+ let resultsForPrettyPrint = ''
88
+ let resultsForJSON = results
89
+ let resultsForPlainTextPrint = ''
90
+
91
+ let summaryForPrettyPrint = ''
92
+ let summaryForPlainTextPrint = ''
93
+ // @TODO: add support for JSON output
94
+ // let summaryForJSON =
95
+
96
+ if (!Array.isArray(results) || results.length === 0) {
97
+ return
98
+ }
99
+
100
+ // Check if we should use rich formatting
101
+ const useRichFormatting = isInteractiveTerminal() && !plain
102
+
103
+ // Get terminal width for text wrapping
104
+ const terminalWidth = getTerminalWidth()
105
+
106
+ let countErrors = 0
107
+ let countWarnings = 0
108
+ let countPackages = results.length
109
+
110
+ results.forEach((result, index) => {
111
+ // Add spacing between packages (except for the first one)
112
+ if (index > 0) {
113
+ resultsForPrettyPrint += ''
114
+ resultsForPlainTextPrint += '\n'
115
+ }
116
+
117
+ // Rich formatting version
118
+ const packageNameRich = styleText(['bold'], result.pkg)
119
+ const prefixRich = styleText(['gray'], `${'│'}`)
120
+ const promptRich = styleText(['gray', 'dim'], `${'>'} `)
121
+ const separatorRich = styleText(['dim'], `${'·'}`)
122
+
123
+ resultsForPrettyPrint += `\n ${styleText(['dim'], '┌─')}`
124
+ resultsForPrettyPrint += `\n ${prefixRich} ${promptRich} ${packageNameRich}`
125
+ resultsForPrettyPrint += `\n ${prefixRich}`
126
+
127
+ // Plain text version
128
+ resultsForPlainTextPrint += `\n--- Package: ${result.pkg} ---`
129
+
130
+ // TBD later change to actually calc'ing it instead of hardcoding
131
+ const maxTextLength = 'Supply Chain Security'.length
132
+
133
+ // Calculate the prefix length for indentation (without ANSI codes)
134
+ // Format: " │ ✖ Supply Chain Security · "
135
+ const basePrefix = ` │ ✖ `
136
+ const topicPadding = maxTextLength + 0 // +1 for space after topic
137
+ const separatorLength = 3 // ` · `
138
+ const prefixLength = basePrefix.length + topicPadding + separatorLength
139
+ const maxMessageWidth = terminalWidth - prefixLength - 2 // -2 for safety margin
140
+
141
+ // Create indentation string for wrapped lines - include the │ prefix and align with message start
142
+ const indentString = ` ${styleText(['gray'], '│')} ${' '.repeat(prefixLength - 3)}`
143
+
144
+ let isPackageMalicious = null
145
+
146
+ if (result.errors && result.errors.length > 0) {
147
+ for (const errorArray of result.errors) {
148
+ isPackageMalicious = errorArray.find((error) => {
149
+ return error.message.includes('Malicious package found')
150
+ })
151
+
152
+ if (isPackageMalicious) {
153
+ break
154
+ }
155
+ }
156
+
157
+ if (isPackageMalicious) {
158
+ countErrors = 1
159
+
160
+ // Rich formatting version
161
+ const topicRich = styleText(
162
+ ['red', 'bold'],
163
+ isPackageMalicious.categoryFriendlyName.padEnd(maxTextLength, ' ')
164
+ )
165
+ const iconRich = styleText(['red', 'bold'], `✖`)
166
+ const errorLineRich = ` ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${isPackageMalicious.message} `
167
+ resultsForPrettyPrint += `\n${errorLineRich}`
168
+
169
+ // Plain text version
170
+ resultsForPlainTextPrint += `\n ERROR: ${isPackageMalicious.categoryFriendlyName} - ${isPackageMalicious.message}`
171
+ } else {
172
+ for (const errorArray of result.errors) {
173
+ countErrors += errorArray.length
174
+ // Each error is actually an array of error objects
175
+ errorArray.forEach((error) => {
176
+ const messageText = error.message
177
+
178
+ // Rich formatting version
179
+ const topicRich = styleText(
180
+ ['red', 'bold'],
181
+ error.categoryFriendlyName.padEnd(maxTextLength, ' ')
182
+ )
183
+ const iconRich = styleText(['red', 'bold'], `✖`)
184
+ const wrappedMessage = wrapTextWithIndent(messageText, maxMessageWidth, indentString)
185
+ const errorLineRich = ` ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${wrappedMessage} `
186
+ resultsForPrettyPrint += `\n${errorLineRich}`
187
+
188
+ // Plain text version
189
+ resultsForPlainTextPrint += `\n ERROR: ${error.categoryFriendlyName} - ${messageText}`
190
+ })
191
+ }
192
+ }
193
+ }
194
+
195
+ // Display warnings with yellow color and ⚠ symbol
196
+ if (!isPackageMalicious && result.warnings && result.warnings.length > 0) {
197
+ result.warnings.forEach((warningArray) => {
198
+ countWarnings += warningArray.length
199
+ // Each warning is actually an array of warning objects
200
+ warningArray.forEach((warning) => {
201
+ const messageText = warning.message
202
+
203
+ // Rich formatting version
204
+ const topicRich = styleText(
205
+ ['yellow'],
206
+ warning.categoryFriendlyName.padEnd(maxTextLength, ' ')
207
+ )
208
+ const iconRich = styleText(['yellow', 'bold'], `⚠`)
209
+ const wrappedMessage = wrapTextWithIndent(messageText, maxMessageWidth, indentString)
210
+ const warningLineRich = ` ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${wrappedMessage} `
211
+ resultsForPrettyPrint += `\n${warningLineRich}`
212
+
213
+ // Plain text version
214
+ resultsForPlainTextPrint += `\n WARNING: ${warning.categoryFriendlyName} - ${messageText}`
215
+ })
216
+ })
217
+ }
218
+
219
+ // Close rich formatting box
220
+ resultsForPrettyPrint += `\n ${styleText(['dim'], '└─')}`
221
+ })
222
+
223
+ const maxTextLength = 'Total packages:'.length
224
+
225
+ // Rich formatting summary
226
+ summaryForPrettyPrint += `\n\n${styleText(['bold'], 'Summary:')}\n`
227
+ summaryForPrettyPrint += `\n - ${'Total packages:'.padEnd(maxTextLength, ' ')} ${styleText(
228
+ ['bold'],
229
+ String(countPackages)
230
+ )}`
231
+ summaryForPrettyPrint += `\n - ${'Total errors:'.padEnd(maxTextLength, ' ')} ${styleText(
232
+ ['red', 'bold'],
233
+ String(countErrors)
234
+ )}`
235
+ summaryForPrettyPrint += `\n - ${'Total warnings:'.padEnd(maxTextLength, ' ')} ${styleText(
236
+ ['yellow', 'bold'],
237
+ String(countWarnings)
238
+ )}`
239
+ summaryForPrettyPrint += `\n`
240
+
241
+ // Plain text summary
242
+ summaryForPlainTextPrint += `\n\nSummary:\n`
243
+ summaryForPlainTextPrint += `\n - Total packages: ${countPackages}`
244
+ summaryForPlainTextPrint += `\n - Total errors: ${countErrors}`
245
+ summaryForPlainTextPrint += `\n - Total warnings: ${countWarnings}`
246
+ summaryForPlainTextPrint += `\n`
247
+
248
+ return {
249
+ countErrors,
250
+ countWarnings,
251
+ resultsForPrettyPrint,
252
+ resultsForJSON,
253
+ resultsForPlainTextPrint,
254
+ summaryForPrettyPrint,
255
+ summaryForPlainTextPrint,
256
+ useRichFormatting
257
+ }
258
+ }
259
+
260
+ function marshallResultsToIssuesPerPackage(results) {
261
+ const issuesPerPackage = []
262
+ for (const pkg in results) {
263
+ const allMarshallResultsList = results[pkg]
264
+ const issues = {
265
+ pkg,
266
+ errors: [],
267
+ warnings: []
268
+ }
269
+ for (const marshall of allMarshallResultsList) {
270
+ for (const [key, value] of Object.entries(marshall)) {
271
+ if (value.errors && value.errors.length > 0) {
272
+ const marshallErrors = value.errors.map((error) => {
273
+ return {
274
+ marshall: value.marshall,
275
+ categoryId: value.categoryId,
276
+ categoryFriendlyName: marshallCategories[value.categoryId].title,
277
+ ...error
278
+ }
279
+ })
280
+ issues.errors.push(marshallErrors)
281
+ }
282
+ if (value.warnings && value.warnings.length > 0) {
283
+ const marshallWarnings = value.warnings.map((warning) => {
284
+ return {
285
+ marshall: value.marshall,
286
+ categoryId: value.categoryId,
287
+ categoryFriendlyName: marshallCategories[value.categoryId].title,
288
+ ...warning
289
+ }
290
+ })
291
+
292
+ issues.warnings.push(marshallWarnings)
293
+ }
294
+ }
295
+ }
296
+ if (issues.errors.length > 0 || issues.warnings.length > 0) {
297
+ issuesPerPackage.push(issues)
298
+ }
299
+ }
300
+
301
+ return issuesPerPackage
302
+ }
303
+
304
+ module.exports = { reportResults, marshallResultsToIssuesPerPackage }
@@ -0,0 +1,36 @@
1
+ const path = require('node:path')
2
+ const fs = require('node:fs/promises')
3
+
4
+ async function getProjectPackages() {
5
+ const currentDirectory = process.cwd()
6
+
7
+ const packageJSONPath = path.join(currentDirectory, 'package.json')
8
+ try {
9
+ let packages = []
10
+ const packageJSON = await fs.readFile(packageJSONPath, 'utf-8')
11
+ const packageData = JSON.parse(packageJSON)
12
+ if (packageData && packageData.dependencies) {
13
+ packages = Object.keys(packageData.dependencies).map((dep) => {
14
+ return `${dep}@${packageData.dependencies[dep]}`
15
+ })
16
+ }
17
+
18
+ return packages
19
+ } catch (error) {
20
+ let errorMessage = ''
21
+ if (error.code === 'ENOENT') {
22
+ errorMessage = `No package.json found in ${currentDirectory}`
23
+ } else {
24
+ errorMessage = `Error reading package.json: ${error.message}`
25
+ }
26
+
27
+ return {
28
+ error: true,
29
+ message: errorMessage
30
+ }
31
+ }
32
+ }
33
+
34
+ module.exports = {
35
+ getProjectPackages
36
+ }
package/lib/marshall.js CHANGED
@@ -1,36 +1,65 @@
1
1
  /* eslint-disable no-console */
2
2
  'use strict'
3
3
 
4
- const color = require('kleur')
5
-
4
+ // const util = require('node:util')
6
5
  const Marshalls = require('./marshalls')
7
6
  const PackageRepoUtils = require('./helpers/packageRepoUtils')
8
-
9
- const MESSAGE_TYPE = {
10
- ERROR: 0,
11
- WARNING: 1
12
- }
7
+ // const { marshallCategories } = require('./marshalls/constants')
13
8
 
14
9
  class Marshall {
15
10
  constructor(options = {}) {
16
11
  this.pkgs = options ? options.pkgs : null
17
12
  this.packageRepoUtils = new PackageRepoUtils()
13
+ this.progressManager = options.progressManager || null
14
+ this.promiseThrottleHelper = options.promiseThrottleHelper || null
18
15
  }
19
16
 
20
- process() {
17
+ async process() {
21
18
  // nothing to do? move on
22
19
  if (!this.pkgs) {
23
20
  return Promise.resolve()
24
21
  }
25
22
 
26
- const config = {
27
- pkgs: this.createPackageVersionMaps(this.pkgs),
28
- packageRepoUtils: this.packageRepoUtils
23
+ if (this.progressManager) {
24
+ this.progressManager.update('Analyzing...')
29
25
  }
30
26
 
31
- return Marshalls.tasks(config).then((ctx) => {
32
- return this.report(ctx.marshalls)
27
+ const promises = this.pkgs.map((pkg) => {
28
+ if (!this.promiseThrottleHelper) {
29
+ return this.createPackageAuditFunction(pkg, this.packageRepoUtils)
30
+ } else {
31
+ // use the promise throttler to limit concurrency
32
+ return this.promiseThrottleHelper(
33
+ () => {
34
+ return this.createPackageAuditFunction(pkg, this.packageRepoUtils)
35
+ },
36
+ 1,
37
+ 50
38
+ ) // max 1 concurrent, 0.1 second delay
39
+ }
33
40
  })
41
+ const res = await Promise.all(promises)
42
+
43
+ // match pkgs array with results of promises
44
+ const promiseResultsPerPackage = this.pkgs.reduce((acc, pkg, index) => {
45
+ acc[pkg] = res[index]
46
+ return acc
47
+ }, {})
48
+
49
+ if (this.progressManager) {
50
+ this.progressManager.stop()
51
+ }
52
+
53
+ return promiseResultsPerPackage
54
+ }
55
+
56
+ async createPackageAuditFunction(pkg, packageRepoUtils) {
57
+ const allPackages = Array.isArray(pkg) ? pkg : [pkg]
58
+ const config = {
59
+ pkgs: this.createPackageVersionMaps(allPackages),
60
+ packageRepoUtils: packageRepoUtils
61
+ }
62
+ return Marshalls.tasks(config, this.progressManager)
34
63
  }
35
64
 
36
65
  createPackageVersionMaps(packages) {
@@ -52,58 +81,6 @@ class Marshall {
52
81
 
53
82
  return packageVersionMapping
54
83
  }
55
-
56
- report(marshalls) {
57
- const messages = this.collectPackageMessages(marshalls)
58
-
59
- if (!messages || Object.keys(messages).length === 0) {
60
- return { error: false }
61
- }
62
-
63
- console.log()
64
- console.log('Detected possible issues with the following packages:')
65
- for (const packageName in messages) {
66
- const packageMessages = messages[packageName]
67
- console.log(` [${color.red(packageName)}]`)
68
-
69
- packageMessages.forEach((message) => {
70
- if (message.type === MESSAGE_TYPE.ERROR) {
71
- console.log(` - ${color.red(message.text)}`)
72
- } else {
73
- console.log(` - ${color.yellow(message.text)}`)
74
- }
75
- })
76
- }
77
-
78
- return { error: true, data: messages }
79
- }
80
-
81
- collectPackageMessages(marshalls) {
82
- const allPackageMessages = {}
83
- for (const key in marshalls) {
84
- this.prepareMessages(allPackageMessages, marshalls[key].errors)
85
- this.prepareMessages(allPackageMessages, marshalls[key].warnings, true)
86
- }
87
-
88
- return allPackageMessages
89
- }
90
-
91
- prepareMessages(allPackageMessages, messages, isWarning) {
92
- if (Array.isArray(messages) && messages.length > 0) {
93
- messages.forEach((msg) => {
94
- this.appendPackageMessage(allPackageMessages, msg.pkg, {
95
- text: msg.message,
96
- type: isWarning ? MESSAGE_TYPE.WARNING : MESSAGE_TYPE.ERROR
97
- })
98
- })
99
- }
100
- }
101
-
102
- appendPackageMessage(packages, packageName, packageMessage) {
103
- packages[packageName]
104
- ? packages[packageName].push(packageMessage)
105
- : (packages[packageName] = [packageMessage])
106
- }
107
84
  }
108
85
 
109
86
  module.exports = Marshall
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const BaseMarshall = require('./baseMarshall')
4
+ const Warning = require('../helpers/warning')
4
5
  const { marshallCategories } = require('./constants')
5
6
 
6
7
  const MARSHALL_NAME = 'age'
@@ -38,7 +39,7 @@ class Marshall extends BaseMarshall {
38
39
 
39
40
  return pkg
40
41
  } else {
41
- throw new Error('Could not determine package age')
42
+ throw new Warning('Could not determine package age')
42
43
  }
43
44
  })
44
45
  .then((pkg) => {
@@ -59,7 +60,7 @@ class Marshall extends BaseMarshall {
59
60
  }
60
61
 
61
62
  if (versionDateDiffInDays >= PACKAGE_AGE_UNMAINTAINED_RISK) {
62
- throw new Error(`Detected an old package (created ${timeAgoNumber} ${timeAgoText} ago)`)
63
+ throw new Warning(`Detected an old package (created ${timeAgoNumber} ${timeAgoText} ago)`)
63
64
  }
64
65
  })
65
66
  }