-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for globbed input and ignores
- Loading branch information
Pelle Wessman
committed
Dec 12, 2022
1 parent
11bf8d0
commit c9b03ea
Showing
9 changed files
with
489 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,117 +1,165 @@ | ||
import { stat } from 'node:fs/promises' | ||
import path from 'node:path' | ||
|
||
import { globby } from 'globby' | ||
import ignore from 'ignore' | ||
// @ts-ignore This package provides no types | ||
import { directories } from 'ignore-by-default' | ||
import { ErrorWithCause } from 'pony-cause' | ||
|
||
import { InputError } from './errors.js' | ||
import { isErrnoException } from './type-helpers.js' | ||
|
||
// TODO: Add globbing support with support for ignoring, as a "./**/package.json" in a project also traverses eg. node_modules | ||
/** @type {readonly string[]} */ | ||
const SUPPORTED_LOCKFILES = [ | ||
'package-lock.json', | ||
'yarn.lock', | ||
] | ||
|
||
/** | ||
* Takes paths to folders and/or package.json / package-lock.json files and resolves to package.json + package-lock.json pairs (where feasible) | ||
* There are a lot of possible folders that we should not be looking in and "ignore-by-default" helps us with defining those | ||
* | ||
* @param {string} cwd | ||
* @param {string[]} inputPaths | ||
* @type {readonly string[]} | ||
*/ | ||
const ignoreByDefault = directories() | ||
|
||
/** @type {readonly string[]} */ | ||
const GLOB_IGNORE = [ | ||
...ignoreByDefault.map(item => '**/' + item) | ||
] | ||
|
||
/** | ||
* Resolves package.json and lockfiles from (globbed) input paths, applying relevant ignores | ||
* | ||
* @param {string} cwd The working directory to use when resolving paths | ||
* @param {string[]} inputPaths A list of paths to folders, package.json files and/or recognized lockfiles. Supports globs. | ||
* @param {import('./socket-config.js').SocketYml|undefined} config | ||
* @param {typeof console.error} debugLog | ||
* @returns {Promise<string[]>} | ||
* @throws {InputError} | ||
*/ | ||
export async function resolvePackagePaths (cwd, inputPaths) { | ||
const packagePathLookups = inputPaths.map(async (filePath) => { | ||
const packagePath = await resolvePackagePath(cwd, filePath) | ||
return findComplementaryPackageFile(packagePath) | ||
export async function getPackageFiles (cwd, inputPaths, config, debugLog) { | ||
let hasPlainDot = false | ||
|
||
// TODO [globby@>13.1.2]: The bug that requires this workaround has probably been fixed now: https://github.com/sindresorhus/globby/pull/242 | ||
const filteredInputPaths = inputPaths.filter(item => { | ||
if (item === '.') { | ||
hasPlainDot = true | ||
return false | ||
} | ||
return true | ||
}) | ||
|
||
const packagePaths = await Promise.all(packagePathLookups) | ||
const entries = [ | ||
...(hasPlainDot ? [cwd + '/'] : []), | ||
...(await globby(filteredInputPaths, { | ||
absolute: true, | ||
cwd, | ||
expandDirectories: false, | ||
gitignore: true, | ||
ignore: [...GLOB_IGNORE], | ||
markDirectories: true, | ||
onlyFiles: false, | ||
unique: true, | ||
})) | ||
] | ||
|
||
debugLog(`Globbed resolved ${inputPaths.length} paths to ${entries.length} paths:`, entries) | ||
|
||
const packageFiles = await mapGlobResultToFiles(entries) | ||
|
||
debugLog(`Mapped ${entries.length} entries to ${packageFiles.length} files:`, packageFiles) | ||
|
||
const includedPackageFiles = config?.projectIgnorePaths | ||
? ignore() | ||
.add(config.projectIgnorePaths) | ||
.filter(packageFiles) | ||
: packageFiles | ||
|
||
return includedPackageFiles | ||
} | ||
|
||
/** | ||
* Takes paths to folders, package.json and/or recognized lock files and resolves them to package.json + lockfile pairs (where possible) | ||
* | ||
* @param {string[]} entries | ||
* @returns {Promise<string[]>} | ||
* @throws {InputError} | ||
*/ | ||
export async function mapGlobResultToFiles (entries) { | ||
const packageFiles = await Promise.all(entries.map(mapGlobEntryToFiles)) | ||
|
||
const uniquePackagePaths = new Set(packagePaths.flat()) | ||
const uniquePackageFiles = [...new Set(packageFiles.flat())] | ||
|
||
return [...uniquePackagePaths] | ||
return uniquePackageFiles | ||
} | ||
|
||
/** | ||
* Resolves a package.json / package-lock.json path from a relative folder / file path | ||
* Takes a single path to a folder, package.json or a recognized lock file and resolves to a package.json + lockfile pair (where possible) | ||
* | ||
* @param {string} cwd | ||
* @param {string} inputPath | ||
* @returns {Promise<string>} | ||
* @param {string} entry | ||
* @returns {Promise<string[]>} | ||
* @throws {InputError} | ||
*/ | ||
async function resolvePackagePath (cwd, inputPath) { | ||
const filePath = path.resolve(cwd, inputPath) | ||
export async function mapGlobEntryToFiles (entry) { | ||
/** @type {string|undefined} */ | ||
let filePathAppended | ||
|
||
try { | ||
const fileStat = await stat(filePath) | ||
|
||
if (fileStat.isDirectory()) { | ||
filePathAppended = path.resolve(filePath, 'package.json') | ||
} | ||
} catch (err) { | ||
if (isErrnoException(err) && err.code === 'ENOENT') { | ||
throw new InputError(`Expected '${inputPath}' to point to an existing file or directory`) | ||
} | ||
throw new ErrorWithCause('Failed to resolve path to package.json', { cause: err }) | ||
let pkgFile | ||
/** @type {string|undefined} */ | ||
let lockFile | ||
|
||
if (entry.endsWith('/')) { | ||
// If the match is a folder and that folder contains a package.json file, then include it | ||
const filePath = path.resolve(entry, 'package.json') | ||
pkgFile = await fileExists(filePath) ? filePath : undefined | ||
} else if (path.basename(entry) === 'package.json') { | ||
// If the match is a package.json file, then include it | ||
pkgFile = entry | ||
} else if (SUPPORTED_LOCKFILES.includes(path.basename(entry))) { | ||
// If the match is a lock file, include both it and the corresponding package.json file | ||
lockFile = entry | ||
pkgFile = path.resolve(path.dirname(entry), 'package.json') | ||
} | ||
|
||
if (filePathAppended) { | ||
/** @type {import('node:fs').Stats} */ | ||
let filePathAppendedStat | ||
// If we will include a package.json file but don't already have a corresponding lockfile, then look for one | ||
if (!lockFile && pkgFile) { | ||
const pkgDir = path.dirname(pkgFile) | ||
|
||
try { | ||
filePathAppendedStat = await stat(filePathAppended) | ||
} catch (err) { | ||
if (isErrnoException(err) && err.code === 'ENOENT') { | ||
throw new InputError(`Expected directory '${inputPath}' to contain a package.json file`) | ||
for (const name of SUPPORTED_LOCKFILES) { | ||
const lockFileAlternative = path.resolve(pkgDir, name) | ||
if (await fileExists(lockFileAlternative)) { | ||
lockFile = lockFileAlternative | ||
break | ||
} | ||
throw new ErrorWithCause('Failed to resolve package.json in directory', { cause: err }) | ||
} | ||
|
||
if (!filePathAppendedStat.isFile()) { | ||
throw new InputError(`Expected '${filePathAppended}' to be a file`) | ||
} | ||
} | ||
|
||
return filePathAppended | ||
if (pkgFile && lockFile) { | ||
return [pkgFile, lockFile] | ||
} | ||
|
||
return filePath | ||
return pkgFile ? [pkgFile] : [] | ||
} | ||
|
||
/** | ||
* Finds any complementary file to a package.json or package-lock.json | ||
* | ||
* @param {string} packagePath | ||
* @returns {Promise<string[]>} | ||
* @throws {InputError} | ||
* @param {string} filePath | ||
* @returns {Promise<boolean>} | ||
*/ | ||
async function findComplementaryPackageFile (packagePath) { | ||
const basename = path.basename(packagePath) | ||
const dirname = path.dirname(packagePath) | ||
|
||
if (basename === 'package-lock.json') { | ||
// We need the package file as well | ||
return [ | ||
packagePath, | ||
path.resolve(dirname, 'package.json') | ||
] | ||
} | ||
export async function fileExists (filePath) { | ||
/** @type {import('node:fs').Stats} */ | ||
let pathStat | ||
|
||
if (basename === 'package.json') { | ||
const lockfilePath = path.resolve(dirname, 'package-lock.json') | ||
try { | ||
const lockfileStat = await stat(lockfilePath) | ||
if (lockfileStat.isFile()) { | ||
return [packagePath, lockfilePath] | ||
} | ||
} catch (err) { | ||
if (isErrnoException(err) && err.code === 'ENOENT') { | ||
return [packagePath] | ||
} | ||
throw new ErrorWithCause(`Unexpected error when finding a lockfile for '${packagePath}'`, { cause: err }) | ||
try { | ||
pathStat = await stat(filePath) | ||
} catch (err) { | ||
if (isErrnoException(err) && err.code === 'ENOENT') { | ||
return false | ||
} | ||
throw new ErrorWithCause('Error while checking if file exists', { cause: err }) | ||
} | ||
|
||
throw new InputError(`Encountered a non-file at lockfile path '${lockfilePath}'`) | ||
if (!pathStat.isFile()) { | ||
throw new InputError(`Expected '${filePath}' to be a file`) | ||
} | ||
|
||
throw new InputError(`Expected '${packagePath}' to point to a package.json or package-lock.json or to a folder containing a package.json`) | ||
return true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { readFile } from 'node:fs/promises' | ||
|
||
import Ajv from 'ajv' | ||
import { ErrorWithCause } from 'pony-cause' | ||
import { parse as yamlParse } from 'yaml' | ||
|
||
import { isErrnoException } from './type-helpers.js' | ||
|
||
/** | ||
* @typedef SocketYml | ||
* @property {2} version | ||
* @property {string[]} [projectIgnorePaths] | ||
* @property {{ [issueName: string]: boolean }} [issueRules] | ||
*/ | ||
|
||
/** @type {import('ajv').JSONSchemaType<SocketYml>} */ | ||
const socketYmlSchema = { | ||
$schema: 'http://json-schema.org/draft-07/schema#', | ||
type: 'object', | ||
properties: { | ||
version: { type: 'integer' }, | ||
projectIgnorePaths: { | ||
type: 'array', | ||
items: { type: 'string' }, | ||
nullable: true, | ||
}, | ||
issueRules: { | ||
type: 'object', | ||
additionalProperties: { type: 'boolean' }, | ||
nullable: true, | ||
required: [], | ||
}, | ||
}, | ||
required: ['version'], | ||
additionalProperties: true, | ||
} | ||
|
||
/** | ||
* @param {string} filePath | ||
* @returns {Promise<SocketYml|undefined>} | ||
*/ | ||
export async function readSocketConfig (filePath) { | ||
/** @type {string} */ | ||
let fileContent | ||
|
||
try { | ||
fileContent = await readFile(filePath, 'utf8') | ||
} catch (err) { | ||
if (isErrnoException(err) && err.code === 'ENOENT') { | ||
return | ||
} | ||
throw new ErrorWithCause('Error when reading socket.yml config file', { cause: err }) | ||
} | ||
|
||
/** @type {unknown} */ | ||
let parsedContent | ||
|
||
try { | ||
parsedContent = yamlParse(fileContent) | ||
} catch (err) { | ||
throw new ErrorWithCause('Error when parsing socket.yml config', { cause: err }) | ||
} | ||
if ((new Ajv()).validate(socketYmlSchema, parsedContent)) { | ||
return parsedContent | ||
} | ||
} |
Oops, something went wrong.