-
Notifications
You must be signed in to change notification settings - Fork 106
feat(init): add fuzzy search for module selection #1180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
531c5e2
ebcafc2
49bdf07
3f01b42
0bfd5c1
8f1e69e
a517c0e
1d25796
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,7 @@ | |
| "defu", | ||
| "exsolve", | ||
| "fuse.js", | ||
| "fzf", | ||
| "giget", | ||
| "h3-next", | ||
| "jiti", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import type { Option } from '@clack/prompts' | ||
| import type { NuxtModule } from './_utils' | ||
|
|
||
| import { autocompleteMultiselect, isCancel } from '@clack/prompts' | ||
| import { byLengthAsc, Fzf } from 'fzf' | ||
| import { hasTTY } from 'std-env' | ||
|
|
||
| import { logger } from '../../utils/logger' | ||
|
|
||
| export interface AutocompleteOptions { | ||
| modules: NuxtModule[] | ||
| message?: string | ||
| } | ||
|
|
||
| export interface AutocompleteResult { | ||
| selected: string[] | ||
| cancelled: boolean | ||
| } | ||
|
|
||
| /** | ||
| * Interactive fuzzy search for selecting Nuxt modules | ||
| * Returns object with selected module npm package names and cancellation status | ||
| */ | ||
| export async function selectModulesAutocomplete(options: AutocompleteOptions): Promise<AutocompleteResult> { | ||
| const { modules, message = 'Search and select modules:' } = options | ||
|
|
||
| if (!hasTTY) { | ||
| logger.warn('Interactive module selection requires a TTY. Skipping.') | ||
| return { selected: [], cancelled: false } | ||
| } | ||
|
|
||
| // Sort: official modules first, then alphabetically | ||
| const sortedModules = [...modules].sort((a, b) => { | ||
| if (a.type === 'official' && b.type !== 'official') | ||
| return -1 | ||
| if (a.type !== 'official' && b.type === 'official') | ||
| return 1 | ||
| return a.npm.localeCompare(b.npm) | ||
| }) | ||
|
|
||
| // Setup fzf for fast fuzzy search | ||
| const fzf = new Fzf(sortedModules, { | ||
| selector: m => `${m.npm} ${m.name} ${m.category}`, | ||
| casing: 'case-insensitive', | ||
| tiebreakers: [byLengthAsc], | ||
| }) | ||
|
|
||
| // Build options for clack multiselect | ||
| const clackOptions: Option<string>[] = sortedModules.map(m => ({ | ||
| value: m.npm, | ||
| label: m.npm, | ||
| hint: m.description.replace(/\.$/, ''), | ||
| })) | ||
|
|
||
| // Custom filter function using fzf for fuzzy matching | ||
| const filter = (search: string, option: Option<string>): boolean => { | ||
| if (!search) | ||
| return true | ||
| const results = fzf.find(search) | ||
| return results.some(r => r.item.npm === option.value) | ||
| } | ||
|
|
||
| const result = await autocompleteMultiselect({ | ||
| message, | ||
| options: clackOptions, | ||
| filter, | ||
| required: false, | ||
| }) | ||
|
|
||
| if (isCancel(result)) { | ||
| return { selected: [], cancelled: true } | ||
| } | ||
|
|
||
| return { selected: result, cancelled: false } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,7 +8,7 @@ import { homedir } from 'node:os' | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { join } from 'node:path' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import process from 'node:process' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cancel, confirm, isCancel, select } from '@clack/prompts' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cancel, confirm, isCancel, select, spinner } from '@clack/prompts' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { updateConfig } from 'c12/update' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { defineCommand } from 'citty' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { colors } from 'consola/utils' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -25,6 +25,7 @@ import { relativeToProcess } from '../../utils/paths' | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getNuxtVersion } from '../../utils/versions' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cwdArgs, logLevelArgs } from '../_shared' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import prepareCommand from '../prepare' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { selectModulesAutocomplete } from './_autocomplete' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { checkNuxtCompatibility, fetchModules, getRegistryFromContent } from './_utils' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface RegistryMeta { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -68,7 +69,7 @@ export default defineCommand({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async setup(ctx) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cwd = resolve(ctx.args.cwd) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const modules = ctx.args._.map(e => e.trim()).filter(Boolean) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let modules = ctx.args._.map(e => e.trim()).filter(Boolean) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const projectPkg = await readPackageJSON(cwd).catch(() => ({} as PackageJson)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!projectPkg.dependencies?.nuxt && !projectPkg.devDependencies?.nuxt) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -84,6 +85,35 @@ export default defineCommand({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If no modules specified, show interactive search | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (modules.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const modulesSpinner = spinner() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modulesSpinner.start('Fetching available modules') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [allModules, nuxtVersion] = await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchModules(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getNuxtVersion(cwd), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const compatibleModules = allModules.filter(m => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modulesSpinner.stop('Modules loaded') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await selectModulesAutocomplete({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modules: compatibleModules, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: 'Search modules to add (Esc to finish):', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result.selected.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cancel('No modules selected.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modules = result.selected | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+88
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle module fetch failures and explicit cancellation in the interactive flow. A failed fetch will currently throw and leave the spinner running; also explicit cancellation isn’t handled separately from empty selection. Consider adding a guarded fetch and a 🐛 Suggested hardening if (modules.length === 0) {
const modulesSpinner = spinner()
modulesSpinner.start('Fetching available modules')
- const [allModules, nuxtVersion] = await Promise.all([
- fetchModules(),
- getNuxtVersion(cwd),
- ])
+ let allModules: NuxtModule[]
+ let nuxtVersion: string
+ try {
+ [allModules, nuxtVersion] = await Promise.all([
+ fetchModules(),
+ getNuxtVersion(cwd),
+ ])
+ }
+ catch (err) {
+ modulesSpinner.stop('Failed to load modules')
+ logger.error(err instanceof Error ? err.message : String(err))
+ process.exit(1)
+ }
const compatibleModules = allModules.filter(m =>
!m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion),
)
modulesSpinner.stop('Modules loaded')
const result = await selectModulesAutocomplete({
modules: compatibleModules,
message: 'Search modules to add (Esc to finish):',
})
+ if (result.cancelled) {
+ cancel('Operation cancelled.')
+ process.exit(1)
+ }
if (result.selected.length === 0) {
cancel('No modules selected.')
process.exit(0)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resolvedModules: ResolvedModule[] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const moduleName of modules) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resolvedModule = await resolveModule(moduleName, cwd) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: nuxt/cli
Length of output: 1582
🏁 Script executed:
Repository: nuxt/cli
Length of output: 2002
🏁 Script executed:
Repository: nuxt/cli
Length of output: 515
🏁 Script executed:
Repository: nuxt/cli
Length of output: 413
🏁 Script executed:
Repository: nuxt/cli
Length of output: 513
🏁 Script executed:
Repository: nuxt/cli
Length of output: 34
🏁 Script executed:
Repository: nuxt/cli
Length of output: 34
🏁 Script executed:
Repository: nuxt/cli
Length of output: 1693
🏁 Script executed:
# Read the tsdown configuration cat -n packages/nuxi/tsdown.config.tsRepository: nuxt/cli
Length of output: 892
🏁 Script executed:
Repository: nuxt/cli
Length of output: 34
Move
fzffrom devDependencies to dependencies.fzfis imported and used at runtime in the CLI (packages/nuxi/src/commands/module/_autocomplete.ts). The tsdown build configuration does not bundle dependencies by default, meaning consumers who install the package will not receivefzfsince it's currently only a devDependency. This will cause the CLI to fail at runtime with a "module not found" error when the autocomplete functionality is invoked.Proposed fix
{ "name": "nuxi", "type": "module", "version": "3.32.0", "description": "Nuxt CLI", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/nuxt/cli.git", "directory": "packages/nuxi" }, "exports": { ".": "./dist/index.mjs", "./cli": "./bin/nuxi.mjs" }, "types": "./dist/index.d.ts", "bin": { "nuxi": "bin/nuxi.mjs", "nuxi-ng": "bin/nuxi.mjs", "nuxt": "bin/nuxi.mjs", "nuxt-cli": "bin/nuxi.mjs" }, "files": [ "bin", "dist" ], "engines": { "node": "^16.10.0 || >=18.0.0" }, "scripts": { "build": "tsdown", "prepack": "pnpm build", "test:dist": "node ./bin/nuxi.mjs info ../../playground" }, + "dependencies": { + "fzf": "^0.5.2" + }, "devDependencies": { "@bomb.sh/tab": "^0.0.12", "@clack/prompts": "1.0.0", "@nuxt/kit": "^4.3.0", "@nuxt/schema": "^4.3.0", "@nuxt/test-utils": "^3.23.0", "@types/copy-paste": "^2.1.0", "@types/debug": "^4.1.12", "@types/node": "^24.10.10", "@types/semver": "^7.7.1", "c12": "^3.3.3", "citty": "^0.2.0", "confbox": "^0.2.2", "consola": "^3.4.2", "copy-paste": "^2.2.0", "debug": "^4.4.3", "defu": "^6.1.4", "exsolve": "^1.0.8", "fuse.js": "^7.1.0", - "fzf": "^0.5.2", "giget": "^3.1.1",📝 Committable suggestion
🤖 Prompt for AI Agents