Skip to content

Commit c9b03ea

Browse files
author
Pelle Wessman
committed
Add support for globbed input and ignores
1 parent 11bf8d0 commit c9b03ea

File tree

9 files changed

+489
-97
lines changed

9 files changed

+489
-97
lines changed

.eslintrc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"root": true,
3-
"plugins": ["jsdoc"],
3+
"plugins": [
4+
"jsdoc",
5+
"unicorn"
6+
],
47
"extends": [
58
"@socketsecurity",
69
"plugin:jsdoc/recommended"
@@ -24,6 +27,8 @@
2427
"jsdoc/require-property-description": "off",
2528
"jsdoc/require-returns-description": "off",
2629
"jsdoc/require-yields": "off",
27-
"jsdoc/valid-types": "off"
30+
"jsdoc/valid-types": "off",
31+
32+
"unicorn/expiring-todo-comments": "warn"
2833
}
2934
}

lib/commands/report/create.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
/* eslint-disable no-console */
22

3+
import path from 'node:path'
4+
35
import meow from 'meow'
46
import ora from 'ora'
57

68
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
79
import { ChalkOrMarkdown, logSymbols } from '../../utils/chalk-markdown.js'
810
import { printFlagList } from '../../utils/formatting.js'
911
import { createDebugLogger } from '../../utils/misc.js'
10-
import { resolvePackagePaths } from '../../utils/path-resolve.js'
12+
import { getPackageFiles } from '../../utils/path-resolve.js'
1113
import { setupSdk } from '../../utils/sdk.js'
14+
import { readSocketConfig } from '../../utils/socket-config.js'
1215
import { fetchReportData, formatReportDataOutput } from './view.js'
1316

1417
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
@@ -147,8 +150,12 @@ async function setupCommand (name, description, argv, importMeta) {
147150

148151
const debugLog = createDebugLogger(dryRun || cli.flags.debug)
149152

153+
// TODO: Allow setting a custom cwd and/or configFile path?
150154
const cwd = process.cwd()
151-
const packagePaths = await resolvePackagePaths(cwd, cli.input)
155+
const absoluteConfigPath = path.join(cwd, 'socket.yml')
156+
157+
const config = await readSocketConfig(absoluteConfigPath)
158+
const packagePaths = await getPackageFiles(cwd, cli.input, config, debugLog)
152159

153160
return {
154161
cwd,
@@ -169,7 +176,7 @@ async function setupCommand (name, description, argv, importMeta) {
169176
* @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'createReport'>>}
170177
*/
171178
async function createReport (packagePaths, { cwd, debugLog, dryRun }) {
172-
debugLog(`${logSymbols.info} Uploading:`, packagePaths.join(`\n${logSymbols.info} Uploading:`))
179+
debugLog('Uploading:', packagePaths.join(`\n${logSymbols.info} Uploading: `))
173180

174181
if (dryRun) {
175182
return

lib/utils/misc.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
import { logSymbols } from './chalk-markdown.js'
2+
13
/**
24
* @param {boolean|undefined} printDebugLogs
35
* @returns {typeof console.error}
46
*/
57
export function createDebugLogger (printDebugLogs) {
6-
if (printDebugLogs) {
8+
return printDebugLogs
79
// eslint-disable-next-line no-console
8-
return console.error.bind(console)
9-
}
10-
return () => {}
10+
? (...params) => console.error(logSymbols.info, ...params)
11+
: () => {}
1112
}
1213

1314
/**

lib/utils/path-resolve.js

Lines changed: 123 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,165 @@
11
import { stat } from 'node:fs/promises'
22
import path from 'node:path'
33

4+
import { globby } from 'globby'
5+
import ignore from 'ignore'
6+
// @ts-ignore This package provides no types
7+
import { directories } from 'ignore-by-default'
48
import { ErrorWithCause } from 'pony-cause'
59

610
import { InputError } from './errors.js'
711
import { isErrnoException } from './type-helpers.js'
812

9-
// TODO: Add globbing support with support for ignoring, as a "./**/package.json" in a project also traverses eg. node_modules
13+
/** @type {readonly string[]} */
14+
const SUPPORTED_LOCKFILES = [
15+
'package-lock.json',
16+
'yarn.lock',
17+
]
18+
1019
/**
11-
* Takes paths to folders and/or package.json / package-lock.json files and resolves to package.json + package-lock.json pairs (where feasible)
20+
* There are a lot of possible folders that we should not be looking in and "ignore-by-default" helps us with defining those
1221
*
13-
* @param {string} cwd
14-
* @param {string[]} inputPaths
22+
* @type {readonly string[]}
23+
*/
24+
const ignoreByDefault = directories()
25+
26+
/** @type {readonly string[]} */
27+
const GLOB_IGNORE = [
28+
...ignoreByDefault.map(item => '**/' + item)
29+
]
30+
31+
/**
32+
* Resolves package.json and lockfiles from (globbed) input paths, applying relevant ignores
33+
*
34+
* @param {string} cwd The working directory to use when resolving paths
35+
* @param {string[]} inputPaths A list of paths to folders, package.json files and/or recognized lockfiles. Supports globs.
36+
* @param {import('./socket-config.js').SocketYml|undefined} config
37+
* @param {typeof console.error} debugLog
1538
* @returns {Promise<string[]>}
1639
* @throws {InputError}
1740
*/
18-
export async function resolvePackagePaths (cwd, inputPaths) {
19-
const packagePathLookups = inputPaths.map(async (filePath) => {
20-
const packagePath = await resolvePackagePath(cwd, filePath)
21-
return findComplementaryPackageFile(packagePath)
41+
export async function getPackageFiles (cwd, inputPaths, config, debugLog) {
42+
let hasPlainDot = false
43+
44+
// TODO [globby@>13.1.2]: The bug that requires this workaround has probably been fixed now: https://github.com/sindresorhus/globby/pull/242
45+
const filteredInputPaths = inputPaths.filter(item => {
46+
if (item === '.') {
47+
hasPlainDot = true
48+
return false
49+
}
50+
return true
2251
})
2352

24-
const packagePaths = await Promise.all(packagePathLookups)
53+
const entries = [
54+
...(hasPlainDot ? [cwd + '/'] : []),
55+
...(await globby(filteredInputPaths, {
56+
absolute: true,
57+
cwd,
58+
expandDirectories: false,
59+
gitignore: true,
60+
ignore: [...GLOB_IGNORE],
61+
markDirectories: true,
62+
onlyFiles: false,
63+
unique: true,
64+
}))
65+
]
66+
67+
debugLog(`Globbed resolved ${inputPaths.length} paths to ${entries.length} paths:`, entries)
68+
69+
const packageFiles = await mapGlobResultToFiles(entries)
70+
71+
debugLog(`Mapped ${entries.length} entries to ${packageFiles.length} files:`, packageFiles)
72+
73+
const includedPackageFiles = config?.projectIgnorePaths
74+
? ignore()
75+
.add(config.projectIgnorePaths)
76+
.filter(packageFiles)
77+
: packageFiles
78+
79+
return includedPackageFiles
80+
}
81+
82+
/**
83+
* Takes paths to folders, package.json and/or recognized lock files and resolves them to package.json + lockfile pairs (where possible)
84+
*
85+
* @param {string[]} entries
86+
* @returns {Promise<string[]>}
87+
* @throws {InputError}
88+
*/
89+
export async function mapGlobResultToFiles (entries) {
90+
const packageFiles = await Promise.all(entries.map(mapGlobEntryToFiles))
2591

26-
const uniquePackagePaths = new Set(packagePaths.flat())
92+
const uniquePackageFiles = [...new Set(packageFiles.flat())]
2793

28-
return [...uniquePackagePaths]
94+
return uniquePackageFiles
2995
}
3096

3197
/**
32-
* Resolves a package.json / package-lock.json path from a relative folder / file path
98+
* Takes a single path to a folder, package.json or a recognized lock file and resolves to a package.json + lockfile pair (where possible)
3399
*
34-
* @param {string} cwd
35-
* @param {string} inputPath
36-
* @returns {Promise<string>}
100+
* @param {string} entry
101+
* @returns {Promise<string[]>}
37102
* @throws {InputError}
38103
*/
39-
async function resolvePackagePath (cwd, inputPath) {
40-
const filePath = path.resolve(cwd, inputPath)
104+
export async function mapGlobEntryToFiles (entry) {
41105
/** @type {string|undefined} */
42-
let filePathAppended
43-
44-
try {
45-
const fileStat = await stat(filePath)
46-
47-
if (fileStat.isDirectory()) {
48-
filePathAppended = path.resolve(filePath, 'package.json')
49-
}
50-
} catch (err) {
51-
if (isErrnoException(err) && err.code === 'ENOENT') {
52-
throw new InputError(`Expected '${inputPath}' to point to an existing file or directory`)
53-
}
54-
throw new ErrorWithCause('Failed to resolve path to package.json', { cause: err })
106+
let pkgFile
107+
/** @type {string|undefined} */
108+
let lockFile
109+
110+
if (entry.endsWith('/')) {
111+
// If the match is a folder and that folder contains a package.json file, then include it
112+
const filePath = path.resolve(entry, 'package.json')
113+
pkgFile = await fileExists(filePath) ? filePath : undefined
114+
} else if (path.basename(entry) === 'package.json') {
115+
// If the match is a package.json file, then include it
116+
pkgFile = entry
117+
} else if (SUPPORTED_LOCKFILES.includes(path.basename(entry))) {
118+
// If the match is a lock file, include both it and the corresponding package.json file
119+
lockFile = entry
120+
pkgFile = path.resolve(path.dirname(entry), 'package.json')
55121
}
56122

57-
if (filePathAppended) {
58-
/** @type {import('node:fs').Stats} */
59-
let filePathAppendedStat
123+
// If we will include a package.json file but don't already have a corresponding lockfile, then look for one
124+
if (!lockFile && pkgFile) {
125+
const pkgDir = path.dirname(pkgFile)
60126

61-
try {
62-
filePathAppendedStat = await stat(filePathAppended)
63-
} catch (err) {
64-
if (isErrnoException(err) && err.code === 'ENOENT') {
65-
throw new InputError(`Expected directory '${inputPath}' to contain a package.json file`)
127+
for (const name of SUPPORTED_LOCKFILES) {
128+
const lockFileAlternative = path.resolve(pkgDir, name)
129+
if (await fileExists(lockFileAlternative)) {
130+
lockFile = lockFileAlternative
131+
break
66132
}
67-
throw new ErrorWithCause('Failed to resolve package.json in directory', { cause: err })
68-
}
69-
70-
if (!filePathAppendedStat.isFile()) {
71-
throw new InputError(`Expected '${filePathAppended}' to be a file`)
72133
}
134+
}
73135

74-
return filePathAppended
136+
if (pkgFile && lockFile) {
137+
return [pkgFile, lockFile]
75138
}
76139

77-
return filePath
140+
return pkgFile ? [pkgFile] : []
78141
}
79142

80143
/**
81-
* Finds any complementary file to a package.json or package-lock.json
82-
*
83-
* @param {string} packagePath
84-
* @returns {Promise<string[]>}
85-
* @throws {InputError}
144+
* @param {string} filePath
145+
* @returns {Promise<boolean>}
86146
*/
87-
async function findComplementaryPackageFile (packagePath) {
88-
const basename = path.basename(packagePath)
89-
const dirname = path.dirname(packagePath)
90-
91-
if (basename === 'package-lock.json') {
92-
// We need the package file as well
93-
return [
94-
packagePath,
95-
path.resolve(dirname, 'package.json')
96-
]
97-
}
147+
export async function fileExists (filePath) {
148+
/** @type {import('node:fs').Stats} */
149+
let pathStat
98150

99-
if (basename === 'package.json') {
100-
const lockfilePath = path.resolve(dirname, 'package-lock.json')
101-
try {
102-
const lockfileStat = await stat(lockfilePath)
103-
if (lockfileStat.isFile()) {
104-
return [packagePath, lockfilePath]
105-
}
106-
} catch (err) {
107-
if (isErrnoException(err) && err.code === 'ENOENT') {
108-
return [packagePath]
109-
}
110-
throw new ErrorWithCause(`Unexpected error when finding a lockfile for '${packagePath}'`, { cause: err })
151+
try {
152+
pathStat = await stat(filePath)
153+
} catch (err) {
154+
if (isErrnoException(err) && err.code === 'ENOENT') {
155+
return false
111156
}
157+
throw new ErrorWithCause('Error while checking if file exists', { cause: err })
158+
}
112159

113-
throw new InputError(`Encountered a non-file at lockfile path '${lockfilePath}'`)
160+
if (!pathStat.isFile()) {
161+
throw new InputError(`Expected '${filePath}' to be a file`)
114162
}
115163

116-
throw new InputError(`Expected '${packagePath}' to point to a package.json or package-lock.json or to a folder containing a package.json`)
164+
return true
117165
}

lib/utils/socket-config.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { readFile } from 'node:fs/promises'
2+
3+
import Ajv from 'ajv'
4+
import { ErrorWithCause } from 'pony-cause'
5+
import { parse as yamlParse } from 'yaml'
6+
7+
import { isErrnoException } from './type-helpers.js'
8+
9+
/**
10+
* @typedef SocketYml
11+
* @property {2} version
12+
* @property {string[]} [projectIgnorePaths]
13+
* @property {{ [issueName: string]: boolean }} [issueRules]
14+
*/
15+
16+
/** @type {import('ajv').JSONSchemaType<SocketYml>} */
17+
const socketYmlSchema = {
18+
$schema: 'http://json-schema.org/draft-07/schema#',
19+
type: 'object',
20+
properties: {
21+
version: { type: 'integer' },
22+
projectIgnorePaths: {
23+
type: 'array',
24+
items: { type: 'string' },
25+
nullable: true,
26+
},
27+
issueRules: {
28+
type: 'object',
29+
additionalProperties: { type: 'boolean' },
30+
nullable: true,
31+
required: [],
32+
},
33+
},
34+
required: ['version'],
35+
additionalProperties: true,
36+
}
37+
38+
/**
39+
* @param {string} filePath
40+
* @returns {Promise<SocketYml|undefined>}
41+
*/
42+
export async function readSocketConfig (filePath) {
43+
/** @type {string} */
44+
let fileContent
45+
46+
try {
47+
fileContent = await readFile(filePath, 'utf8')
48+
} catch (err) {
49+
if (isErrnoException(err) && err.code === 'ENOENT') {
50+
return
51+
}
52+
throw new ErrorWithCause('Error when reading socket.yml config file', { cause: err })
53+
}
54+
55+
/** @type {unknown} */
56+
let parsedContent
57+
58+
try {
59+
parsedContent = yamlParse(fileContent)
60+
} catch (err) {
61+
throw new ErrorWithCause('Error when parsing socket.yml config', { cause: err })
62+
}
63+
if ((new Ajv()).validate(socketYmlSchema, parsedContent)) {
64+
return parsedContent
65+
}
66+
}

0 commit comments

Comments
 (0)