Skip to content

Commit 6d1a287

Browse files
authored
perf: remove find-up dependency, upgrade file-type dependency (#8195)
Fixes #8111 and #8113 Before: 132 dependencies After: 123 dependencies This PR also contains a small performance optimization during telemetry startup: By using the async `fs.promises.readFile` instead of `readFileSync` we're not blocking the entire thread anymore and are allowing other stuff to happen while the file is being read. Also, in our dependency checker, this moves some variables out of loops, to the module scope, as they only need to be calculated once. We have to pin file-type to 19.3.0 and cannot upgrade it further (latest is 19.5.0). See reasoning in #8111 (comment)
1 parent bb2dd5f commit 6d1a287

File tree

11 files changed

+263
-126
lines changed

11 files changed

+263
-126
lines changed

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ const esModules = [
55
'readable-web-to-node-stream',
66
'token-types',
77
'peek-readable',
8-
'find-up',
98
'locate-path',
109
'p-locate',
1110
'p-limit',
1211
'yocto-queue',
1312
'unicorn-magic',
1413
'path-exists',
1514
'qs-esm',
15+
'uint8array-extras',
1616
].join('|')
1717

1818
/** @type {import('jest').Config} */

packages/next/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
"@payloadcms/translations": "workspace:*",
7171
"@payloadcms/ui": "workspace:*",
7272
"busboy": "^1.6.0",
73-
"file-type": "17.1.6",
73+
"file-type": "19.3.0",
7474
"graphql-http": "^1.22.0",
7575
"graphql-playground-html": "1.6.30",
7676
"http-status": "1.6.2",

packages/payload/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,7 @@
9494
"console-table-printer": "2.11.2",
9595
"dataloader": "2.2.2",
9696
"deepmerge": "4.3.1",
97-
"file-type": "17.1.6",
98-
"find-up": "7.0.0",
97+
"file-type": "19.3.0",
9998
"get-tsconfig": "^4.7.2",
10099
"http-status": "1.6.2",
101100
"image-size": "^1.1.1",

packages/payload/src/bin/loadEnv.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import nextEnvImport from '@next/env'
2+
3+
import { findUpSync } from '../utilities/findUp.js'
24
const { loadEnvConfig } = nextEnvImport
3-
import { findUpStop, findUpSync } from 'find-up'
45

56
/**
67
* Try to find user's env files and load it. Uses the same algorithm next.js uses to parse env files, meaning this also supports .env.local, .env.development, .env.production, etc.
@@ -15,11 +16,14 @@ export function loadEnv(path?: string) {
1516

1617
if (!loadedEnvFiles?.length) {
1718
// use findUp to find the env file. So, run loadEnvConfig for every directory upwards
18-
findUpSync((dir) => {
19-
const { loadedEnvFiles } = loadEnvConfig(dir, true)
20-
if (loadedEnvFiles?.length) {
21-
return findUpStop
22-
}
19+
findUpSync({
20+
condition: (dir) => {
21+
const { loadedEnvFiles } = loadEnvConfig(dir, true)
22+
if (loadedEnvFiles?.length) {
23+
return true
24+
}
25+
},
26+
dir: process.cwd(),
2327
})
2428
}
2529
}

packages/payload/src/config/find.ts

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { findUpSync, pathExistsSync } from 'find-up'
21
import { getTsconfig } from 'get-tsconfig'
32
import path from 'path'
43

4+
import { findUpSync } from '../utilities/findUp.js'
5+
6+
/**
7+
* List of all filenames to detect as a Payload configuration file.
8+
*/
9+
export const payloadConfigFileNames = ['payload.config.js', 'payload.config.ts']
10+
511
/**
612
* Returns the source and output paths from the nearest tsconfig.json file.
713
* If no tsconfig.json file is found, returns the current working directory.
@@ -75,26 +81,10 @@ export const findConfig = (): string => {
7581
continue
7682
}
7783

78-
const configPath = findUpSync(
79-
(dir) => {
80-
const tsPath = path.join(dir, 'payload.config.ts')
81-
const hasTS = pathExistsSync(tsPath)
82-
83-
if (hasTS) {
84-
return tsPath
85-
}
86-
87-
const jsPath = path.join(dir, 'payload.config.js')
88-
const hasJS = pathExistsSync(jsPath)
89-
90-
if (hasJS) {
91-
return jsPath
92-
}
93-
94-
return undefined
95-
},
96-
{ cwd: searchPath },
97-
)
84+
const configPath = findUpSync({
85+
dir: searchPath,
86+
fileNames: payloadConfigFileNames,
87+
})
9888

9989
if (configPath) {
10090
return configPath
@@ -104,16 +94,18 @@ export const findConfig = (): string => {
10494
// If no config file is found in the directories defined by tsconfig.json,
10595
// try searching in the 'src' and 'dist' directory as a last resort, as they are most commonly used
10696
if (process.env.NODE_ENV === 'production') {
107-
const distConfigPath = findUpSync(['payload.config.js', 'payload.config.ts'], {
108-
cwd: path.resolve(process.cwd(), 'dist'),
97+
const distConfigPath = findUpSync({
98+
dir: path.resolve(process.cwd(), 'dist'),
99+
fileNames: ['payload.config.js'],
109100
})
110101

111102
if (distConfigPath) {
112103
return distConfigPath
113104
}
114105
} else {
115-
const srcConfigPath = findUpSync(['payload.config.js', 'payload.config.ts'], {
116-
cwd: path.resolve(process.cwd(), 'src'),
106+
const srcConfigPath = findUpSync({
107+
dir: path.resolve(process.cwd(), 'src'),
108+
fileNames: payloadConfigFileNames,
117109
})
118110

119111
if (srcConfigPath) {

packages/payload/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,12 @@ export {
10051005
deepMergeWithSourceArrays,
10061006
} from './utilities/deepMerge.js'
10071007
export { getDependencies } from './utilities/dependencies/getDependencies.js'
1008+
export {
1009+
findUp,
1010+
findUpSync,
1011+
pathExistsAndIsAccessible,
1012+
pathExistsAndIsAccessibleSync,
1013+
} from './utilities/findUp.js'
10081014
export { default as flattenTopLevelFields } from './utilities/flattenTopLevelFields.js'
10091015
export { formatLabels, formatNames, toWords } from './utilities/formatLabels.js'
10101016
export { getCollectionIDFieldTypes } from './utilities/getCollectionIDFieldTypes.js'

packages/payload/src/utilities/dependencies/getDependencies.ts

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,23 @@
1414
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1515
*/
1616

17-
import { findUp } from 'find-up'
18-
import { existsSync, promises as fs } from 'fs'
17+
import { promises as fs } from 'fs'
1918
import path from 'path'
2019
import { fileURLToPath } from 'url'
2120

21+
import { findUp } from '../findUp.js'
2222
import { resolveFrom } from './resolveFrom.js'
2323

2424
const filename = fileURLToPath(import.meta.url)
2525
const dirname = path.dirname(filename)
2626

27+
const payloadPkgDirname = path.resolve(dirname, '../../../') // pkg dir (outside src)
28+
// if node_modules is in payloadPkgDirname, go to parent dir which contains node_modules
29+
if (payloadPkgDirname.includes('node_modules')) {
30+
payloadPkgDirname.split('node_modules').slice(0, -1)
31+
}
32+
const resolvedCwd = path.resolve(process.cwd())
33+
2734
export type NecessaryDependencies = {
2835
missing: string[]
2936
resolved: Map<
@@ -52,35 +59,29 @@ export async function getDependencies(
5259
requiredPackages.map(async (pkg) => {
5360
try {
5461
const pkgPath = await fs.realpath(resolveFrom(baseDir, pkg))
55-
5662
const pkgDir = path.dirname(pkgPath)
5763

5864
let packageJsonFilePath = null
5965

60-
const processCwd = process.cwd()
61-
const payloadPkgDirname = path.resolve(dirname, '../../../') // pkg dir (outside src)
66+
const foundPackageJsonDir = await findUp({
67+
dir: pkgDir,
68+
fileNames: ['package.json'],
69+
})
6270

63-
// if node_modules is in payloadPkgDirname, go to parent dir which contains node_modules
64-
if (payloadPkgDirname.includes('node_modules')) {
65-
payloadPkgDirname.split('node_modules').slice(0, -1)
66-
}
71+
if (foundPackageJsonDir) {
72+
const resolvedFoundPath = path.resolve(foundPackageJsonDir)
6773

68-
await findUp('package.json', { type: 'file', cwd: pkgDir }).then((foundPath) => {
69-
if (foundPath) {
70-
const resolvedFoundPath = path.resolve(foundPath)
71-
const resolvedCwd = path.resolve(processCwd)
72-
73-
if (
74-
resolvedFoundPath.startsWith(resolvedCwd) ||
75-
resolvedFoundPath.startsWith(payloadPkgDirname)
76-
) {
77-
// We don't want to match node modules outside the user's project. Checking for both process.cwd and dirname is a reliable way to do this.
78-
packageJsonFilePath = foundPath
79-
}
74+
if (
75+
resolvedFoundPath.startsWith(resolvedCwd) ||
76+
resolvedFoundPath.startsWith(payloadPkgDirname)
77+
) {
78+
// We don't want to match node modules outside the user's project. Checking for both process.cwd and dirname is a reliable way to do this.
79+
packageJsonFilePath = resolvedFoundPath
8080
}
81-
})
81+
}
8282

83-
if (packageJsonFilePath && existsSync(packageJsonFilePath)) {
83+
// No need to check if packageJsonFilePath exists - findUp checks that for us
84+
if (packageJsonFilePath) {
8485
// parse version
8586
const packageJson = JSON.parse(await fs.readFile(packageJsonFilePath, 'utf8'))
8687
const version = packageJson.version
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
4+
/**
5+
* Synchronously walks up parent directories until a condition is met and/or one of the file names within the fileNames array is found.
6+
*/
7+
export function findUpSync({
8+
condition,
9+
dir,
10+
fileNames,
11+
}: {
12+
condition?: (dir: string) => boolean | Promise<boolean | string> | string
13+
dir: string
14+
fileNames?: string[]
15+
}): null | string {
16+
const { root } = path.parse(dir)
17+
18+
while (true) {
19+
if (fileNames?.length) {
20+
let found = false
21+
for (const fileName of fileNames) {
22+
const filePath = path.join(dir, fileName)
23+
const exists = pathExistsAndIsAccessibleSync(filePath)
24+
if (exists) {
25+
if (!condition) {
26+
return filePath
27+
}
28+
found = true
29+
break
30+
}
31+
}
32+
if (!found) {
33+
dir = path.dirname(dir) // Move up one directory level.
34+
continue
35+
}
36+
}
37+
const result = condition(dir)
38+
if (result === true) {
39+
return dir
40+
}
41+
if (typeof result === 'string' && result?.length) {
42+
return result
43+
}
44+
if (dir === root) {
45+
return null // Reached the root directory without a match.
46+
}
47+
dir = path.dirname(dir) // Move up one directory level.
48+
}
49+
}
50+
51+
/**
52+
* Asynchronously walks up parent directories until a condition is met and/or one of the file names within the fileNames array is found.
53+
*/
54+
export async function findUp({
55+
condition,
56+
dir,
57+
fileNames,
58+
}: {
59+
condition?: (dir: string) => boolean | Promise<boolean | string> | string
60+
dir: string
61+
fileNames?: string[]
62+
}): Promise<null | string> {
63+
const { root } = path.parse(dir)
64+
65+
while (true) {
66+
if (fileNames?.length) {
67+
let found = false
68+
for (const fileName of fileNames) {
69+
const filePath = path.resolve(dir, fileName)
70+
const exists = await pathExistsAndIsAccessible(filePath)
71+
if (exists) {
72+
if (!condition) {
73+
return filePath
74+
}
75+
found = true
76+
break
77+
}
78+
}
79+
if (!found) {
80+
dir = path.dirname(dir) // Move up one directory level.
81+
continue
82+
}
83+
}
84+
const result = await condition(dir)
85+
if (result === true) {
86+
return dir
87+
}
88+
if (typeof result === 'string' && result?.length) {
89+
return result
90+
}
91+
if (dir === root) {
92+
return null // Reached the root directory without a match.
93+
}
94+
dir = path.dirname(dir) // Move up one directory level.
95+
}
96+
}
97+
98+
// From https://github.com/sindresorhus/path-exists/blob/main/index.js
99+
// fs.accessSync is preferred over fs.existsSync as it's usually a good idea
100+
// to check if the process has permission to read/write to a file before doing so.
101+
// Also see https://github.com/nodejs/node/issues/39960
102+
export function pathExistsAndIsAccessibleSync(path: string) {
103+
try {
104+
fs.accessSync(path)
105+
return true
106+
} catch {
107+
return false
108+
}
109+
}
110+
111+
export async function pathExistsAndIsAccessible(path: string) {
112+
try {
113+
await fs.promises.access(path)
114+
return true
115+
} catch {
116+
return false
117+
}
118+
}

packages/payload/src/utilities/telemetry/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { execSync } from 'child_process'
22
import ciInfo from 'ci-info'
33
import { randomBytes } from 'crypto'
4-
import { findUp } from 'find-up'
54
import fs from 'fs'
65
import path from 'path'
76
import { fileURLToPath } from 'url'
@@ -10,6 +9,7 @@ import type { Payload } from '../../types/index.js'
109
import type { AdminInitEvent } from './events/adminInit.js'
1110
import type { ServerInitEvent } from './events/serverInit.js'
1211

12+
import { findUp } from '../findUp.js'
1313
import { Conf } from './conf/index.js'
1414
import { oneWayHash } from './oneWayHash.js'
1515

@@ -136,13 +136,15 @@ const getPackageJSON = async (): Promise<{
136136
// Old logic
137137
const filename = fileURLToPath(import.meta.url)
138138
const dirname = path.dirname(filename)
139-
packageJSONPath = await findUp('package.json', { cwd: dirname })
140-
const jsonContent: PackageJSON = JSON.parse(fs.readFileSync(packageJSONPath, 'utf-8'))
141-
return { packageJSON: jsonContent, packageJSONPath }
139+
packageJSONPath = await findUp({
140+
dir: dirname,
141+
fileNames: ['package.json'],
142+
})
142143
}
143144

144-
const packageJSON: PackageJSON = JSON.parse(fs.readFileSync(packageJSONPath, 'utf-8'))
145-
return { packageJSON, packageJSONPath }
145+
const jsonContentString = await fs.promises.readFile(packageJSONPath, 'utf-8')
146+
const jsonContent: PackageJSON = JSON.parse(jsonContentString)
147+
return { packageJSON: jsonContent, packageJSONPath }
146148
}
147149

148150
const getPackageJSONID = (payload: Payload, packageJSON: PackageJSON): string => {

0 commit comments

Comments
 (0)