Skip to content

Commit 4fd9257

Browse files
Merge pull request #7498 from Shopify/bolt/optimize-startup-and-discovery-15894214764489785463
[Performance] Optimize CLI startup and project discovery
2 parents 60af2e1 + f427e60 commit 4fd9257

3 files changed

Lines changed: 53 additions & 7 deletions

File tree

packages/cli-kit/src/public/node/fs.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
generateRandomNameForSubdirectory,
1919
readFileSync,
2020
glob,
21+
globSync,
2122
detectEOL,
2223
copyDirectoryContents,
2324
symlink,
@@ -319,6 +320,40 @@ describe('glob', () => {
319320
})
320321
})
321322

323+
describe('globSync', () => {
324+
test('returns matches for the given pattern, including dotfiles by default (dot:true)', async () => {
325+
await inTemporaryDirectory(async (tmpDir) => {
326+
// Given
327+
const visiblePath = joinPath(tmpDir, 'visible.toml')
328+
const hiddenPath = joinPath(tmpDir, '.hidden.toml')
329+
await writeFile(visiblePath, '')
330+
await writeFile(hiddenPath, '')
331+
332+
// When
333+
const matches = globSync(joinPath(tmpDir, '*.toml')).map(normalizePath).sort()
334+
335+
// Then
336+
expect(matches).toEqual([normalizePath(hiddenPath), normalizePath(visiblePath)].sort())
337+
})
338+
})
339+
340+
test('honors an explicit dot:false option, excluding dotfiles', async () => {
341+
await inTemporaryDirectory(async (tmpDir) => {
342+
// Given
343+
const visiblePath = joinPath(tmpDir, 'visible.toml')
344+
const hiddenPath = joinPath(tmpDir, '.hidden.toml')
345+
await writeFile(visiblePath, '')
346+
await writeFile(hiddenPath, '')
347+
348+
// When
349+
const matches = globSync(joinPath(tmpDir, '*.toml'), {dot: false}).map(normalizePath)
350+
351+
// Then
352+
expect(matches).toEqual([normalizePath(visiblePath)])
353+
})
354+
})
355+
})
356+
322357
describe('detectEOL', () => {
323358
test('detects the EOL of a file', async () => {
324359
// Given

packages/cli-kit/src/public/node/fs.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import {sep, join} from 'pathe'
1818
import {findUp as internalFindUp, findUpSync as internalFindUpSync} from 'find-up'
1919
import {minimatch} from 'minimatch'
20-
import fastGlobLib from 'fast-glob'
20+
import {createRequire} from 'module'
2121
import {
2222
mkdirSync as fsMkdirSync,
2323
readFileSync as fsReadFileSync,
@@ -33,7 +33,6 @@ import {
3333
accessSync,
3434
ReadStream,
3535
WriteStream,
36-
statSync,
3736
} from 'fs'
3837

3938
import {
@@ -58,6 +57,8 @@ import * as os from 'os'
5857

5958
import type {Pattern, Options as GlobOptions} from 'fast-glob'
6059

60+
const require = createRequire(import.meta.url)
61+
6162
/**
6263
* Strip the first `strip` parts of the path.
6364
*
@@ -510,7 +511,7 @@ export function unixFileIsOwnedByCurrentUser(path: string): boolean | undefined
510511
if (!fileExistsSync(path)) return false
511512

512513
try {
513-
const stats = statSync(path)
514+
const stats = fsStatSync(path)
514515
const currentUid = process.getuid()
515516

516517
return stats.uid === currentUid
@@ -594,11 +595,13 @@ export async function glob(pattern: Pattern | Pattern[], options?: GlobOptions):
594595
* @returns An array of pathnames that match the given pattern.
595596
*/
596597
export function globSync(pattern: Pattern | Pattern[], options?: GlobOptions): string[] {
598+
// Performance: fast-glob is a heavy dependency. We lazy-load it here to avoid
599+
// overhead during CLI startup for commands that don't need globbing.
597600
let overridenOptions = options
598601
if (options?.dot == null) {
599602
overridenOptions = {...options, dot: true}
600603
}
601-
return fastGlobLib.sync(pattern, overridenOptions)
604+
return (require('fast-glob') as typeof import('fast-glob')).sync(pattern, overridenOptions)
602605
}
603606

604607
/**

packages/cli-kit/src/public/node/is-global.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {cwd, dirname, isSubpath, joinPath, sniffForPath} from './path.js'
22
import {isUnitTest} from './context/local.js'
3-
import {findPathUpSync, globSync} from './fs.js'
3+
import {findPathUpSync, globSync, fileExistsSync} from './fs.js'
44
import {realpathSync} from 'fs'
55
import type {PackageManager} from './node-package-manager.js'
66

@@ -140,9 +140,17 @@ export function inferPackageManagerForGlobalCLI(argv = process.argv, env = proce
140140
* @returns The project root directory, or undefined if not found.
141141
*/
142142
export function getProjectDir(directory: string): string | undefined {
143-
const configFiles = ['shopify.app{,.*}.toml', 'hydrogen.config.js', 'hydrogen.config.ts']
143+
// Performance: Check for the most common config files first using fileExistsSync (fast-path)
144+
// to avoid the overhead of globbing/directory scanning in the common case.
145+
const configFiles = ['shopify.app.toml', 'hydrogen.config.js', 'hydrogen.config.ts']
144146
const existsConfigFile = (directory: string) => {
145-
const configPaths = globSync(configFiles.map((file) => joinPath(directory, file)))
147+
for (const file of configFiles) {
148+
const configPath = joinPath(directory, file)
149+
if (fileExistsSync(configPath)) return configPath
150+
}
151+
152+
// Fallback to glob for custom app config files or if fast-path files were not found.
153+
const configPaths = globSync(joinPath(directory, 'shopify.app.*.toml'))
146154
return configPaths.length > 0 ? configPaths[0] : undefined
147155
}
148156
try {

0 commit comments

Comments
 (0)