diff --git a/src/commands/module/_utils.ts b/src/commands/module/_utils.ts index 2073078f2..f1903afee 100644 --- a/src/commands/module/_utils.ts +++ b/src/commands/module/_utils.ts @@ -27,6 +27,9 @@ export const categories = [ export interface ModuleCompatibility { nuxt: string requires: { bridge?: boolean | 'optional' } + versionMap: { + [nuxtVersion: string]: string + } } export interface MaintainerInfo { diff --git a/src/commands/module/add.ts b/src/commands/module/add.ts index ab74b51ee..18f413feb 100644 --- a/src/commands/module/add.ts +++ b/src/commands/module/add.ts @@ -5,6 +5,13 @@ import { existsSync } from 'node:fs' import { loadFile, writeFile, parseModule, ProxifiedModule } from 'magicast' import consola from 'consola' import { addDependency } from 'nypm' +import { + NuxtModule, + checkNuxtCompatibility, + fetchModules, + getNuxtVersion, +} from './_utils' +import { satisfies } from 'semver' export default defineCommand({ meta: { @@ -29,16 +36,18 @@ export default defineCommand({ async setup(ctx) { const cwd = resolve(ctx.args.cwd || '.') - // TODO: Resolve and validate npm package name first - const npmPackage = ctx.args.moduleName + const r = await resolveModule(ctx.args.moduleName, cwd) + if (r === false) { + return + } // Add npm dependency if (!ctx.args.skipInstall) { - consola.info(`Installing dev dependency \`${npmPackage}\``) - await addDependency(npmPackage, { cwd, dev: true }).catch((err) => { + consola.info(`Installing dev dependency \`${r.pkg}\``) + await addDependency(r.pkg, { cwd, dev: true }).catch((err) => { consola.error(err) consola.error( - `Please manually install \`${npmPackage}\` as a dev dependency`, + `Please manually install \`${r.pkg}\` as a dev dependency`, ) }) } @@ -50,17 +59,17 @@ export default defineCommand({ config.modules = [] } for (let i = 0; i < config.modules.length; i++) { - if (config.modules[i] === npmPackage) { - consola.info(`\`${npmPackage}\` is already in the \`modules\``) + if (config.modules[i] === r.pkgName) { + consola.info(`\`${r.pkgName}\` is already in the \`modules\``) return } } - consola.info(`Adding \`${npmPackage}\` to the \`modules\``) - config.modules.push(npmPackage) + consola.info(`Adding \`${r.pkgName}\` to the \`modules\``) + config.modules.push(r.pkgName) }).catch((err) => { consola.error(err) consola.error( - `Please manually add \`${npmPackage}\` to the \`modules\` in \`nuxt.config.ts\``, + `Please manually add \`${r.pkgName}\` to the \`modules\` in \`nuxt.config.ts\``, ) }) } @@ -102,3 +111,97 @@ export default defineNuxtConfig({ modules: [] })` } + +// Based on https://github.com/dword-design/package-name-regex +const packageRegex = + /^(@[a-z0-9-~][a-z0-9-._~]*\/)?([a-z0-9-~][a-z0-9-._~]*)(@[^@]+)?$/ + +async function resolveModule( + moduleName: string, + cwd: string, +): Promise< + | false + | { + nuxtModule?: NuxtModule + pkg: string + pkgName: string + pkgVersion: string + } +> { + let pkgName = moduleName + let pkgVersion: string | undefined + + const reMatch = moduleName.match(packageRegex) + if (reMatch) { + if (reMatch[3]) { + pkgName = `${reMatch[1] || ''}${reMatch[2] || ''}` + pkgVersion = reMatch[3].slice(1) + } + } else { + consola.error(`Invalid package name \`${pkgName}\`.`) + return false + } + + const modulesDB = await fetchModules().catch((err) => { + consola.warn('Cannot search in the Nuxt Modules database: ' + err) + return [] + }) + + const matchedModule = modulesDB.find( + (module) => module.name === moduleName || module.npm === pkgName, + ) + + if (matchedModule?.npm) { + pkgName = matchedModule.npm + } + + if (matchedModule && matchedModule.compatibility.nuxt) { + // Get local Nuxt version + const nuxtVersion = await getNuxtVersion(cwd) + + // Check for Module Compatibility + if (!checkNuxtCompatibility(matchedModule, nuxtVersion)) { + consola.warn( + `The module \`${pkgName}\` is not compatible with Nuxt \`${nuxtVersion}\` (requires \`${matchedModule.compatibility.nuxt}\`)`, + ) + const shouldContinue = await consola.prompt( + 'Do you want to continue installing incompatible version?', + { + type: 'confirm', + initial: false, + }, + ) + if (shouldContinue !== true) { + return false + } + } + + // Match corresponding version of module for local Nuxt version + const versionMap = matchedModule.compatibility.versionMap + if (versionMap) { + for (const [_nuxtVersion, _moduleVersion] of Object.entries(versionMap)) { + if (satisfies(nuxtVersion, _nuxtVersion)) { + if (!pkgVersion) { + pkgVersion = _moduleVersion + } else { + consola.warn( + `Recommended version of \`${pkgName}\` for Nuxt \`${nuxtVersion}\` is \`${_moduleVersion}\` but you have requested \`${pkgVersion}\``, + ) + pkgVersion = await consola.prompt('Choose a version:', { + type: 'select', + options: [_moduleVersion, pkgVersion], + }) + } + break + } + } + } + } + + return { + nuxtModule: matchedModule, + pkg: `${pkgName}@${pkgVersion || 'latest'}`, + pkgName, + pkgVersion: pkgVersion || 'latest', + } +}