|
1 | 1 | import type { PackageJson } from 'pkg-types' |
2 | 2 |
|
| 3 | +import type { InstallResult } from '../utils/install' |
| 4 | + |
3 | 5 | import { existsSync } from 'node:fs' |
4 | 6 | import process from 'node:process' |
5 | 7 |
|
6 | | -import { cancel, intro, isCancel, note, outro, select, spinner, tasks } from '@clack/prompts' |
| 8 | +import { cancel, intro, isCancel, note, outro, select, spinner } from '@clack/prompts' |
7 | 9 | import { defineCommand } from 'citty' |
8 | | -import { addDependency, dedupeDependencies, detectPackageManager } from 'nypm' |
| 10 | +import { detectPackageManager } from 'nypm' |
9 | 11 | import { dirname, relative, resolve } from 'pathe' |
10 | 12 | import colors from 'picocolors' |
11 | 13 | import { findWorkspaceDir, readPackageJSON } from 'pkg-types' |
12 | 14 |
|
| 15 | +import { createInstallLog, runDedupe, runInstall, takeUnreportedIgnoredBuilds } from '../utils/install' |
13 | 16 | import { loadKit } from '../utils/kit' |
14 | 17 | import { logger } from '../utils/logger' |
15 | 18 | import { cleanupNuxtDirs, nuxtVersionToGitIdentifier } from '../utils/nuxt' |
@@ -188,59 +191,53 @@ export default defineCommand({ |
188 | 191 |
|
189 | 192 | const versionType = ctx.args.channel === 'nightly' ? 'nightly' : `latest ${ctx.args.channel}` |
190 | 193 |
|
191 | | - const spin = spinner() |
192 | | - spin.start('Upgrading Nuxt') |
193 | | - |
194 | | - await tasks([ |
195 | | - { |
196 | | - title: `Installing ${versionType} Nuxt ${nuxtVersion} release`, |
197 | | - task: async () => { |
198 | | - await addDependency(npmPackages, { |
199 | | - cwd, |
200 | | - packageManager, |
201 | | - dev: nuxtDependencyType === 'devDependencies', |
202 | | - workspace: packageManager?.name === 'pnpm' && existsSync(resolve(cwd, 'pnpm-workspace.yaml')), |
203 | | - }) |
204 | | - return 'Nuxt packages installed' |
205 | | - }, |
206 | | - }, |
207 | | - ...(method === 'force' |
208 | | - ? [{ |
209 | | - title: `Recreating ${forceRemovals}`, |
210 | | - task: async () => { |
211 | | - await dedupeDependencies({ recreateLockfile: true }) |
212 | | - return 'Lockfile recreated' |
213 | | - }, |
214 | | - }] |
215 | | - : []), |
216 | | - ...(method === 'dedupe' |
217 | | - ? [{ |
218 | | - title: 'Deduping dependencies', |
219 | | - task: async () => { |
220 | | - await dedupeDependencies() |
221 | | - return 'Dependencies deduped' |
222 | | - }, |
223 | | - }] |
224 | | - : []), |
225 | | - { |
226 | | - title: 'Cleaning up build directories', |
227 | | - task: async () => { |
228 | | - let buildDir: string = '.nuxt' |
229 | | - try { |
230 | | - const { loadNuxtConfig } = await loadKit(cwd) |
231 | | - const nuxtOptions = await loadNuxtConfig({ cwd }) |
232 | | - buildDir = nuxtOptions.buildDir |
233 | | - } |
234 | | - catch { |
235 | | - // Use default buildDir (.nuxt) |
236 | | - } |
237 | | - await cleanupNuxtDirs(cwd, buildDir) |
238 | | - return 'Build directories cleaned' |
239 | | - }, |
240 | | - }, |
241 | | - ]) |
242 | | - |
243 | | - spin.stop() |
| 194 | + const verbose = ctx.args.logLevel === 'verbose' || Boolean(process.env.DEBUG) |
| 195 | + |
| 196 | + const installFailed = await withInstallSpinner( |
| 197 | + `Installing ${versionType} Nuxt ${nuxtVersion} release`, |
| 198 | + 'Nuxt packages installed', |
| 199 | + { verbose }, |
| 200 | + hooks => runInstall({ |
| 201 | + cwd, |
| 202 | + packageManager, |
| 203 | + dependencies: npmPackages, |
| 204 | + dev: nuxtDependencyType === 'devDependencies', |
| 205 | + workspace: packageManager.name === 'pnpm' && existsSync(resolve(cwd, 'pnpm-workspace.yaml')), |
| 206 | + ...hooks, |
| 207 | + }), |
| 208 | + ) |
| 209 | + |
| 210 | + if (installFailed) { |
| 211 | + process.exit(1) |
| 212 | + } |
| 213 | + |
| 214 | + if (method === 'force' || method === 'dedupe') { |
| 215 | + const recreateLockfile = method === 'force' |
| 216 | + const failed = await withInstallSpinner( |
| 217 | + recreateLockfile ? `Recreating ${forceRemovals}` : 'Deduping dependencies', |
| 218 | + recreateLockfile ? 'Lockfile recreated' : 'Dependencies deduped', |
| 219 | + { verbose }, |
| 220 | + hooks => runDedupe({ cwd, packageManager, recreateLockfile, ...hooks }), |
| 221 | + ) |
| 222 | + |
| 223 | + if (failed) { |
| 224 | + process.exit(1) |
| 225 | + } |
| 226 | + } |
| 227 | + |
| 228 | + const cleanupSpinner = spinner() |
| 229 | + cleanupSpinner.start('Cleaning up build directories') |
| 230 | + let buildDir: string = '.nuxt' |
| 231 | + try { |
| 232 | + const { loadNuxtConfig } = await loadKit(cwd) |
| 233 | + const nuxtOptions = await loadNuxtConfig({ cwd }) |
| 234 | + buildDir = nuxtOptions.buildDir |
| 235 | + } |
| 236 | + catch { |
| 237 | + // Use default buildDir (.nuxt) |
| 238 | + } |
| 239 | + await cleanupNuxtDirs(cwd, buildDir, { silent: true }) |
| 240 | + cleanupSpinner.stop('Build directories cleaned') |
244 | 241 |
|
245 | 242 | if (method === 'force') { |
246 | 243 | logger.info(`If you encounter any issues, revert the changes and try with ${colors.cyan('--no-force')}`) |
@@ -276,6 +273,53 @@ export default defineCommand({ |
276 | 273 | }, |
277 | 274 | }) |
278 | 275 |
|
| 276 | +interface InstallHooks { |
| 277 | + onOutput: (line: string) => void |
| 278 | + onStatus: (message: string) => void |
| 279 | + signal: AbortSignal |
| 280 | +} |
| 281 | + |
| 282 | +/** |
| 283 | + * Run a package manager step behind a spinner, surfacing its output only when it |
| 284 | + * fails (or when running verbosely). Returns whether the step failed. |
| 285 | + */ |
| 286 | +async function withInstallSpinner( |
| 287 | + title: string, |
| 288 | + success: string, |
| 289 | + options: { verbose: boolean }, |
| 290 | + run: (hooks: InstallHooks) => Promise<InstallResult>, |
| 291 | +): Promise<boolean> { |
| 292 | + const controller = new AbortController() |
| 293 | + const installLog = createInstallLog({ verbose: options.verbose }) |
| 294 | + const spin = spinner({ |
| 295 | + indicator: 'timer', |
| 296 | + onCancel: () => controller.abort(), |
| 297 | + }) |
| 298 | + |
| 299 | + spin.start(title) |
| 300 | + const result = await run({ |
| 301 | + onOutput: installLog.onOutput, |
| 302 | + onStatus: message => spin.message(message), |
| 303 | + signal: controller.signal, |
| 304 | + }) |
| 305 | + |
| 306 | + if (result.success) { |
| 307 | + spin.stop(success) |
| 308 | + } |
| 309 | + else { |
| 310 | + spin.error(result.error ?? `${title} failed`) |
| 311 | + } |
| 312 | + |
| 313 | + installLog.finish(result) |
| 314 | + |
| 315 | + const ignoredBuilds = takeUnreportedIgnoredBuilds(result.ignoredBuilds) |
| 316 | + if (ignoredBuilds.length > 0) { |
| 317 | + logger.warn(`Build scripts were not run for ${ignoredBuilds.map(name => colors.cyan(name)).join(', ')}.`) |
| 318 | + } |
| 319 | + |
| 320 | + return !result.success |
| 321 | +} |
| 322 | + |
279 | 323 | // Find which lock file is in use since `nypm.detectPackageManager` doesn't return this |
280 | 324 | export function findLockFile(cwd: string, workspaceDir: string, lockFiles: string | Array<string> | undefined) { |
281 | 325 | const candidates = typeof lockFiles === 'string' ? [lockFiles] : lockFiles |
|
0 commit comments