diff --git a/packages/nuxi/package.json b/packages/nuxi/package.json index 070c73f12d..9bf26ca14e 100644 --- a/packages/nuxi/package.json +++ b/packages/nuxi/package.json @@ -27,12 +27,16 @@ "@types/mri": "^1.1.1", "chokidar": "^3.5.2", "clear": "^0.1.0", + "clipboardy": "^2.3.0", "colorette": "^1.3.0", "debounce-promise": "^3.1.2", "deep-object-diff": "^1.1.0", + "destr": "^1.1.0", "flat": "^5.0.2", + "jiti": "^1.11.0", "listhen": "^0.2.4", "mri": "^1.1.6", + "scule": "^0.2.1", "unbuild": "^0.4.2", "upath": "^2.0.1", "v8-compile-cache": "^2.3.0" diff --git a/packages/nuxi/src/commands/index.ts b/packages/nuxi/src/commands/index.ts index 8f28d14e2e..408771f3fa 100644 --- a/packages/nuxi/src/commands/index.ts +++ b/packages/nuxi/src/commands/index.ts @@ -4,7 +4,8 @@ export const commands = { dev: () => import('./dev'), build: () => import('./build'), prepare: () => import('./prepare'), - usage: () => import('./usage') + usage: () => import('./usage'), + info: () => import('./info') } export type Command = keyof typeof commands diff --git a/packages/nuxi/src/commands/info.ts b/packages/nuxi/src/commands/info.ts new file mode 100644 index 0000000000..93d1f2d52a --- /dev/null +++ b/packages/nuxi/src/commands/info.ts @@ -0,0 +1,155 @@ +import os from 'os' +import { existsSync, readFileSync } from 'fs' +import { resolve, dirname } from 'upath' +import jiti from 'jiti' +import destr from 'destr' +import { splitByCase } from 'scule' +import clipboardy from 'clipboardy' +import { defineNuxtCommand } from './index' + +export default defineNuxtCommand({ + meta: { + name: 'info', + usage: 'npx nuxi info [rootDir]', + description: 'Get information about nuxt project' + }, + async invoke (args) { + // Resolve rootDir + const rootDir = resolve(args._[0] || '.') + + // Load nuxt.config + const nuxtConfig = getNuxtConfig(rootDir) + + // Find nearest package.json + const { dependencies = {}, devDependencies = {} } = findPackage(rootDir) + + // Utils to query a dependency version + const getDepVersion = name => getPkg(name, rootDir)?.version || dependencies[name] || devDependencies[name] + + const listModules = (arr = []) => arr + .map(normalizeConfigModule) + .filter(Boolean) + .map((name) => { + const npmName = name.split('/').splice(0, 2).join('/') // @foo/bar/baz => @foo/bar + const v = getDepVersion(npmName) + return '`' + (v ? `${name}@${v}` : name) + '`' + }) + .join(', ') + + const infoObj = { + OperatingSystem: os.type(), + NodeVersion: process.version, + NuxtVersion: getDepVersion('nuxt') || getDepVersion('nuxt-edge') || (getDepVersion('nuxt3') ? '3-' + getDepVersion('nuxt3') : null), + PackageManager: getPackageManager(rootDir), + Bundler: (nuxtConfig.vite || nuxtConfig?.buildModules?.find(m => m === 'nuxt-vite')) ? 'Vite' : 'Webpack', + UserConfig: Object.keys(nuxtConfig).map(key => '`' + key + '`').join(', '), + RuntimeModules: listModules(nuxtConfig.modules), + BuildModules: listModules(nuxtConfig.buildModules) + } + + console.log('RootDir:', rootDir) + + let maxLength = 0 + const entries = Object.entries(infoObj).map(([key, val]) => { + const label = splitByCase(key).join(' ') + if (label.length > maxLength) { maxLength = label.length } + return [label, val || '-'] + }) + let infoStr = '' + for (const [label, value] of entries) { + infoStr += '- ' + (label + ': ').padEnd(maxLength + 2) + (value.includes('`') ? value : '`' + value + '`') + '\n' + } + + const copied = await clipboardy.write(infoStr).then(() => true).catch(() => false) + const splitter = '------------------------------' + console.log(`Nuxt project info: ${copied ? '(copied to clipboard)' : ''}\n\n${splitter}\n${infoStr}${splitter}\n`) + + const isNuxt3 = infoObj.NuxtVersion.startsWith('3') || infoObj.BuildModules.includes('bridge') + const repo = isNuxt3 ? 'nuxt/framework' : 'nuxt/nuxt.js' + console.log([ + `👉 Report an issue: https://github.com/${repo}/issues/new`, + `👉 Suggest an improvement: https://github.com/${repo}/discussions/new`, + `👉 Read documentation: ${isNuxt3 ? 'https://v3.nuxtjs.org' : 'https://nuxtjs.org'}` + ].join('\n\n') + '\n') + } +}) + +function normalizeConfigModule (module, rootDir) { + if (!module) { + return null + } + if (typeof module === 'string') { + return module + .split(rootDir).pop() // Strip rootDir + .split('node_modules').pop() // Strip node_modules + .replace(/^\//, '') + } + if (typeof module === 'function') { + return `${module.name}()` + } + if (Array.isArray(module)) { + return normalizeConfigModule(module[0], rootDir) + } +} + +function findup (rootDir, fn) { + let dir = rootDir + while (dir !== dirname(dir)) { + const res = fn(dir) + if (res) { + return res + } + dir = dirname(dir) + } + return null +} + +function getPackageManager (rootDir) { + return findup(rootDir, (dir) => { + if (existsSync(resolve(dir, 'yarn.lock'))) { + return 'Yarn' + } + if (existsSync(resolve(dir, 'package-lock.json'))) { + return 'npm' + } + }) || 'unknown' +} + +function getNuxtConfig (rootDir) { + try { + return jiti(rootDir, { interopDefault: true })('./nuxt.config') + } catch (err) { + // TODO: Show error as warning if it is not 404 + return {} + } +} + +function getPkg (name, rootDir) { + // Assume it is in {rootDir}/node_modules/${name}/package.json + let pkgPath = resolve(rootDir, 'node_modules', name, 'package.json') + + // Try to resolve for more accuracy + try { pkgPath = require.resolve(name + '/package.json', { paths: [rootDir] }) } catch (_err) { + // console.log('not found:', name) + } + + return readJSONSync(pkgPath) +} + +function findPackage (rootDir) { + return findup(rootDir, (dir) => { + const p = resolve(dir, 'package.json') + if (existsSync(p)) { + return readJSONSync(p) + } + }) || {} +} + +function readJSONSync (filePath) { + try { + return destr(readFileSync(filePath, 'utf-8')) + } catch (err) { + // TODO: Warn error + return null + } +} diff --git a/yarn.lock b/yarn.lock index 9732bcef8b..60f48542ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9769,13 +9769,17 @@ fsevents@~2.3.2: "@types/mri": ^1.1.1 chokidar: ^3.5.2 clear: ^0.1.0 + clipboardy: ^2.3.0 colorette: ^1.3.0 debounce-promise: ^3.1.2 deep-object-diff: ^1.1.0 + destr: ^1.1.0 flat: ^5.0.2 fsevents: ~2.3.2 + jiti: ^1.11.0 listhen: ^0.2.4 mri: ^1.1.6 + scule: ^0.2.1 unbuild: ^0.4.2 upath: ^2.0.1 v8-compile-cache: ^2.3.0