Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"some-layer/**"
],
"ignoreDependencies": [
"vue-router"
"vue-router",
"vue-tsc"
]
},
"packages/nuxt-cli": {
Expand Down
119 changes: 88 additions & 31 deletions packages/nuxi/src/commands/typecheck.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import process from 'node:process'

import { cancel, confirm, isCancel, spinner } from '@clack/prompts'
import { defineCommand } from 'citty'
import { colors } from 'consola/utils'
import { resolveModulePath } from 'exsolve'
import { addDevDependency, detectPackageManager } from 'nypm'
import { resolve } from 'pathe'
import { readTSConfig } from 'pkg-types'
import { isBun } from 'std-env'
import { hasTTY } from 'std-env'
import { x } from 'tinyexec'

import { loadKit } from '../utils/kit'
import { logger } from '../utils/logger'
import { cwdArgs, dotEnvArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared'

const REQUIRED_DEPS = {
'typescript': 'typescript',
'vue-tsc': 'vue-tsc/bin/vue-tsc.js',
} as const

type DepName = keyof typeof REQUIRED_DEPS

function resolveDeps({ cache }: { cache?: boolean } = {}) {
const out = {} as Record<DepName, string | undefined>
for (const name in REQUIRED_DEPS) {
out[name as DepName] = resolveModulePath(REQUIRED_DEPS[name as DepName], { try: true, cache })
}
return out
}

export default defineCommand({
meta: {
name: 'typecheck',
Expand All @@ -27,50 +46,88 @@ export default defineCommand({

const cwd = resolve(ctx.args.cwd || ctx.args.rootDir)

const [supportsProjects, resolvedTypeScript, resolvedVueTsc] = await Promise.all([
const [supportsProjects, vueTsc] = await Promise.all([
readTSConfig(cwd).then(r => !!(r.references?.length)),
// Prefer local install if possible
resolveModulePath('typescript', { try: true }),
resolveModulePath('vue-tsc/bin/vue-tsc.js', { try: true }),
ensureVueTsc(cwd, resolveDeps()),
writeTypes(cwd, ctx.args.dotenv, ctx.args.logLevel as 'silent' | 'info' | 'verbose', {
...ctx.data?.overrides,
...(ctx.args.extends && { extends: ctx.args.extends }),
}),
])

const typeCheckArgs = supportsProjects ? ['-b', '--noEmit'] : ['--noEmit']
if (resolvedTypeScript && resolvedVueTsc) {
return await x(resolvedVueTsc, typeCheckArgs, {
throwOnError: true,
nodeOptions: {
stdio: 'inherit',
cwd,
},
})
}

if (isBun) {
await x('bun', ['install', 'typescript', 'vue-tsc', '--global', '--silent'], {
throwOnError: true,
nodeOptions: { stdio: 'inherit', cwd },
})

return await x('bunx', ['vue-tsc', ...typeCheckArgs], {
throwOnError: true,
nodeOptions: {
stdio: 'inherit',
cwd,
},
})
if (!vueTsc) {
process.exitCode = 1
return
}

await x('npx', ['-p', 'vue-tsc', '-p', 'typescript', 'vue-tsc', ...typeCheckArgs], {
throwOnError: true,
const start = Date.now()
const result = await x(vueTsc, supportsProjects ? ['-b', '--noEmit'] : ['--noEmit'], {
nodeOptions: { stdio: 'inherit', cwd },
})
const duration = `${Date.now() - start}ms`

if (result.exitCode === 0) {
if (hasTTY) {
logger.success(`Type check passed in ${colors.cyan(duration)}.`)
}
return
}

if (hasTTY) {
logger.error(`Type check failed in ${colors.cyan(duration)}.`)
}
process.exitCode = result.exitCode ?? 1
},
})

async function ensureVueTsc(cwd: string, deps: Record<DepName, string | undefined>): Promise<string | undefined> {
const missing = (Object.keys(REQUIRED_DEPS) as DepName[]).filter(name => !deps[name])
if (missing.length === 0) {
return deps['vue-tsc']
}

const packageManager = await detectPackageManager(cwd, { includeParentDirs: true })
const pmName = packageManager?.name ?? 'npm'
const installCommand = `${packageManager?.command ?? pmName} add ${pmName === 'bun' ? '-d' : '-D'} ${missing.join(' ')}`

const list = missing.map(name => colors.cyan(name)).join(' and ')
const plural = missing.length > 1
const are = plural ? 'are' : 'is'
const devDependency = plural ? 'devDependencies' : 'a devDependency'

if (!hasTTY) {
logger.error(`${list} ${are} required for ${colors.cyan('nuxt typecheck')}. Install ${plural ? 'them' : 'it'} as ${devDependency}:\n\n ${colors.bold(installCommand)}\n`)
return
}

logger.warn(`${list} ${are} required for ${colors.cyan('nuxt typecheck')} but ${plural ? 'were' : 'was'} not found.`)

const shouldInstall = await confirm({
message: `Install ${list} as ${devDependency}?`,
initialValue: true,
})

if (isCancel(shouldInstall) || !shouldInstall) {
cancel(`Skipping installation. Run ${colors.bold(installCommand)} to install manually.`)
return
}

const spin = spinner()
spin.start(`Installing ${list} with ${colors.cyan(pmName)}`)
try {
await addDevDependency(missing, { cwd, packageManager, silent: true })
spin.stop(`Installed ${list}`)
}
catch (error) {
spin.error(`Failed to install ${list}`)
logger.error(error instanceof Error ? error.message : String(error))
logger.info(`You can install ${plural ? 'them' : 'it'} manually with:\n\n ${colors.bold(installCommand)}\n`)
return
}

return resolveDeps({ cache: false })['vue-tsc']
}

async function writeTypes(cwd: string, dotenv?: string, logLevel?: 'silent' | 'info' | 'verbose', overrides?: Record<string, any>) {
const { loadNuxt, buildNuxt, writeTypes } = await loadKit(cwd)
const nuxt = await loadNuxt({
Expand Down
4 changes: 3 additions & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"vue-router": "^5.0.6"
},
"devDependencies": {
"@nuxt/test-utils": "^4.0.3"
"@nuxt/test-utils": "^4.0.3",
"typescript": "^6.0.3",
"vue-tsc": "^3.2.8"
}
}
Loading
Loading