|
1 | 1 | import { stat } from 'node:fs/promises'
|
2 | 2 | import path from 'node:path'
|
3 | 3 |
|
| 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' |
4 | 8 | import { ErrorWithCause } from 'pony-cause'
|
5 | 9 |
|
6 | 10 | import { InputError } from './errors.js'
|
7 | 11 | import { isErrnoException } from './type-helpers.js'
|
8 | 12 |
|
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 | + |
10 | 19 | /**
|
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 |
12 | 21 | *
|
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 |
15 | 38 | * @returns {Promise<string[]>}
|
16 | 39 | * @throws {InputError}
|
17 | 40 | */
|
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 |
22 | 51 | })
|
23 | 52 |
|
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)) |
25 | 91 |
|
26 |
| - const uniquePackagePaths = new Set(packagePaths.flat()) |
| 92 | + const uniquePackageFiles = [...new Set(packageFiles.flat())] |
27 | 93 |
|
28 |
| - return [...uniquePackagePaths] |
| 94 | + return uniquePackageFiles |
29 | 95 | }
|
30 | 96 |
|
31 | 97 | /**
|
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) |
33 | 99 | *
|
34 |
| - * @param {string} cwd |
35 |
| - * @param {string} inputPath |
36 |
| - * @returns {Promise<string>} |
| 100 | + * @param {string} entry |
| 101 | + * @returns {Promise<string[]>} |
37 | 102 | * @throws {InputError}
|
38 | 103 | */
|
39 |
| -async function resolvePackagePath (cwd, inputPath) { |
40 |
| - const filePath = path.resolve(cwd, inputPath) |
| 104 | +export async function mapGlobEntryToFiles (entry) { |
41 | 105 | /** @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') |
55 | 121 | }
|
56 | 122 |
|
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) |
60 | 126 |
|
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 |
66 | 132 | }
|
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`) |
72 | 133 | }
|
| 134 | + } |
73 | 135 |
|
74 |
| - return filePathAppended |
| 136 | + if (pkgFile && lockFile) { |
| 137 | + return [pkgFile, lockFile] |
75 | 138 | }
|
76 | 139 |
|
77 |
| - return filePath |
| 140 | + return pkgFile ? [pkgFile] : [] |
78 | 141 | }
|
79 | 142 |
|
80 | 143 | /**
|
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>} |
86 | 146 | */
|
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 |
98 | 150 |
|
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 |
111 | 156 | }
|
| 157 | + throw new ErrorWithCause('Error while checking if file exists', { cause: err }) |
| 158 | + } |
112 | 159 |
|
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`) |
114 | 162 | }
|
115 | 163 |
|
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 |
117 | 165 | }
|
0 commit comments