Skip to content

Commit

Permalink
feat(cli): support path auto-completion
Browse files Browse the repository at this point in the history
  • Loading branch information
pionxzh committed Dec 28, 2023
1 parent e3da389 commit ce92fea
Show file tree
Hide file tree
Showing 15 changed files with 562 additions and 43 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"pnpm": {
"patchedDependencies": {
"[email protected]": "patches/[email protected]",
"@clack/[email protected]": "patches/@[email protected]"
"@clack/[email protected]": "patches/@[email protected]",
"@clack/[email protected]": "patches/@[email protected]"
}
},
"resolutions": {
Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
50 changes: 50 additions & 0 deletions packages/cli/__tests__/path.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, it } from 'vitest'
import { pathCompletion } from '../src/path'

const baseDir = __dirname

interface CustomMatchers<R = unknown> {
toBeSamePath(expected: R): R
}

declare module 'vitest' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}

expect.extend({
// Check if the path is the same with automatic path separator conversion
toBeSamePath: (received, expected) => {
const receivedPath = received.replace(/\\/g, '/')
const expectedPath = expected.replace(/\\/g, '/')
const pass = receivedPath === expectedPath
return {
pass,
message: () => `Expected ${receivedPath} ${pass ? 'not ' : ''}to be same path as ${expectedPath}`,
actual: receivedPath,
expected: expectedPath,
}
},
})

it('pathCompletion', () => {
// expect(pathCompletion({ input: '', baseDir })).toBe('./folder/')
expect(pathCompletion({ input: 'f', baseDir })).toBeSamePath('./folder/')
expect(pathCompletion({ input: 'fold', baseDir })).toBeSamePath('./folder/')
expect(pathCompletion({ input: 'folder', baseDir })).toBeSamePath('./folder/')
expect(pathCompletion({ input: 'folder/', baseDir })).toBeSamePath('./folder/')
expect(pathCompletion({ input: './folder', baseDir })).toBeSamePath('./folder/')
expect(pathCompletion({ input: './folder/', baseDir })).toBeSamePath('./folder/')

expect(pathCompletion({ input: 'folder/f', baseDir })).toBeSamePath('./folder/file1.js')
expect(pathCompletion({ input: 'folder/file1', baseDir })).toBeSamePath('./folder/file1.js')
expect(pathCompletion({ input: 'folder/file1.js', baseDir })).toBeSamePath('./folder/file1.js')
expect(pathCompletion({ input: './folder/file1', baseDir })).toBeSamePath('./folder/file1.js')

expect(pathCompletion({ input: 'folder/file12', baseDir })).toBeSamePath('./folder/file12.js')
expect(pathCompletion({ input: 'folder/file12.js', baseDir })).toBeSamePath('./folder/file12.js')

expect(pathCompletion({ input: 'folder/file123', baseDir })).toBeSamePath('./folder/file123.js')

expect(pathCompletion({ input: 'folder/nested', baseDir })).toBeSamePath('./folder/nested/')
})
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"lint:fix": "eslint src --fix"
},
"dependencies": {
"@clack/prompts": "^0.7.0",
"fs-extra": "^11.1.1",
"globby": "^11.1.0",
"picocolors": "^1.0.0",
Expand All @@ -33,6 +32,7 @@
},
"devDependencies": {
"@clack/core": "^0.3.3",
"@clack/prompts": "^0.7.0",
"@types/fs-extra": "^11.0.4",
"@types/jscodeshift": "^0.11.11",
"@types/yargs": "^17.0.32",
Expand Down
22 changes: 21 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { FixedThreadPool } from 'poolifier'
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import { version } from '../package.json'
import { findCommonBaseDir, getRelativePath, isPathInside, resolveGlob } from './path'
import { findCommonBaseDir, getRelativePath, isPathInside, pathCompletion, resolveGlob } from './path'
import { Timing } from './timing'
import { unpacker } from './unpacker'
import type { Measurement } from './timing'
Expand Down Expand Up @@ -205,6 +205,11 @@ async function interactive({
const rawInputPath = await text({
message: `Input file path ${c.dim('(Supports glob patterns)')}`,
placeholder: './input.js',
autocomplete(value) {
if (typeof value !== 'string') return

return pathCompletion({ input: value, baseDir: cwd })
},
validate(value) {
if (!value) return 'Please enter a file path'

Expand Down Expand Up @@ -232,6 +237,11 @@ async function interactive({
const rawOutputBase = await text({
message: `Output directory path ${c.dim('(<enter> to accept default)')}`,
placeholder: defaultOutputBase,
autocomplete(value) {
if (typeof value !== 'string') return

return pathCompletion({ input: value, baseDir: cwd, directoryOnly: true })
},
validate(value) {
if (!value) return undefined // default value

Expand Down Expand Up @@ -309,6 +319,11 @@ async function interactive({
const rawInputPath = await text({
message: `Input file path ${c.dim('(Supports glob patterns)')}`,
placeholder: './*.js',
autocomplete(value) {
if (typeof value !== 'string') return

return pathCompletion({ input: value, baseDir: cwd })
},
validate(value) {
if (!value) return 'Please enter a file path'

Expand Down Expand Up @@ -341,6 +356,11 @@ async function interactive({
const rawOutputBase = await text({
message: `Output directory path ${c.dim('(<enter> to accept default)')}`,
placeholder: defaultOutputBase,
autocomplete(value) {
if (typeof value !== 'string') return

return pathCompletion({ input: value, baseDir: cwd, directoryOnly: true })
},
validate(value) {
if (!value) return undefined // default value

Expand Down
47 changes: 47 additions & 0 deletions packages/cli/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,50 @@ export function resolveGlob(glob: string) {
ignore: [path.join(cwd, '**/node_modules/**')],
})
}

export function pathCompletion({
// can be a path or a file path or incomplete string
input,
baseDir = process.cwd(),
directoryOnly = false,
}: {
input: string
baseDir?: string
directoryOnly?: boolean
}): string {
// Determine if the input is an absolute path
const fullPath = path.isAbsolute(input) ? input : path.resolve(baseDir, input)

// Get the directory part and the part of the path to be completed
const dir = path.dirname(fullPath)
const toComplete = path.basename(fullPath)

// Check if the directory exists
if (!fsa.existsSync(dir)) {
return input
}

// Read the directory content
const files = fsa.readdirSync(dir)

// Find the first matching file or directory
const match = files
.filter((file) => {
if (directoryOnly && !fsa.statSync(path.join(dir, file)).isDirectory()) {
return false
}
return true
})
.find((file) => {
return file.startsWith(toComplete)
})

if (match) {
const matchedPath = path.join(dir, match)
const isDir = fsa.statSync(matchedPath).isDirectory()
// Check if the matched path is a directory and append a slash if it is
return getRelativePath(baseDir, matchedPath) + (isDir ? path.sep : '')
}

return input
}
3 changes: 2 additions & 1 deletion packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"skipLibCheck": true
},
"include": [
"src"
"src",
"__tests__"
],
"exclude": [
"node_modules",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default defineConfig({
'jscodeshift',
'ast-types',
'@clack/core', // patched
'@clack/prompts', // patched
/@wakaru\/.+/,
],
})
69 changes: 39 additions & 30 deletions patches/@[email protected]

Large diffs are not rendered by default.

Loading

0 comments on commit ce92fea

Please sign in to comment.