Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/create-nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@types/node": "^24.19.0",
"citty": "^0.2.2",
"giget": "^3.3.1",
"nypm": "^0.6.10",
"package-manager-detector": "^1.8.0",
"pathe": "^2.0.3",
"pkg-types": "^2.3.3",
"std-env": "^4.2.0",
Expand Down
77 changes: 19 additions & 58 deletions packages/create-nuxt/src/init.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import type { ArgsDef, CommandDef } from 'citty'
import type { DownloadTemplateResult } from 'giget'
import type { PackageManagerName } from 'nypm'
import type { AgentName } from 'package-manager-detector'
import type { InstallResult } from '../../nuxt-cli/src/utils/install'
import type { TemplateData } from '../../nuxt-cli/src/utils/starter-templates'
import { existsSync } from 'node:fs'

import { existsSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import process from 'node:process'
import { styleText } from 'node:util'

import { cancel, confirm, intro, isCancel, outro, S_BAR, select, text } from '@clack/prompts'
import { defineCommand, showUsage } from 'citty'
import { downloadTemplate, startShell } from 'giget'
import { detectPackageManager } from 'nypm'
import { basename, join, relative, resolve } from 'pathe'
import { findFile, readPackageJSON, writePackageJSON } from 'pkg-types'
import { hasTTY } from 'std-env'
Expand All @@ -26,9 +25,10 @@ import { runCommandDef as runCommand } from '../../nuxt-cli/src/run-command'
import { nuxtIcon } from '../../nuxt-cli/src/utils/ascii'
import { fetchJson } from '../../nuxt-cli/src/utils/fetch'
import { formatHeadlessCommand } from '../../nuxt-cli/src/utils/headless'
import { createInstallLog, resolvePackageManagerDescriptor, runInstall, takeUnreportedIgnoredBuilds } from '../../nuxt-cli/src/utils/install'
import { createInstallLog, runInstall, takeUnreportedIgnoredBuilds } from '../../nuxt-cli/src/utils/install'
import { debug, logger } from '../../nuxt-cli/src/utils/logger'
import { classifyNetworkError, describeNetworkError, logNetworkError, probeNetworkError } from '../../nuxt-cli/src/utils/network'
import { detectPackageManager, isPackageManagerName, packageManagerNames } from '../../nuxt-cli/src/utils/package-managers'
import { relativeToProcess } from '../../nuxt-cli/src/utils/paths'
import { createSpinner } from '../../nuxt-cli/src/utils/spinner'
import { getTemplates, TEMPLATES_API_URL } from '../../nuxt-cli/src/utils/starter-templates'
Expand All @@ -43,19 +43,6 @@ const DEFAULT_REGISTRY = 'https://raw.githubusercontent.com/nuxt/starter/templat
const DEFAULT_TEMPLATE_NAME = 'minimal'
const NIGHTLY_DIST_TAGS_URL = 'https://registry.npmjs.org/nuxt-nightly'

const pms: Record<PackageManagerName, undefined> = {
npm: undefined,
pnpm: undefined,
yarn: undefined,
bun: undefined,
deno: undefined,
aube: undefined,
nub: undefined,
}

// this is for type safety to prompt updating code in nuxi when nypm adds a new package manager
const packageManagerOptions = Object.keys(pms) as PackageManagerName[]

// Arguments that would otherwise be gathered through interactive prompts,
// so they must be explicitly provided when no TTY is available
const nonInteractiveRequiredArgs = ['dir', 'template', 'packageManager', 'gitInit'] as const
Expand Down Expand Up @@ -120,7 +107,7 @@ export function getNextSteps(options: {
installFailure?: unknown
installSkipped?: boolean
recoveryCommands: string[]
packageManager: PackageManagerName
packageManager: AgentName
}): string[] {
const { dir, shell, installFailure, installSkipped, recoveryCommands, packageManager } = options
const runCmd = packageManager === 'deno' ? 'task' : 'run'
Expand Down Expand Up @@ -186,7 +173,7 @@ export default defineCommand({
packageManager: {
type: 'string',
description: 'Package manager choice',
valueHint: packageManagerOptions.join('|'),
valueHint: packageManagerNames.join('|'),
},
modules: {
type: 'string',
Expand All @@ -207,8 +194,8 @@ export default defineCommand({
// banner or network work) so a typo fails fast with a clear message instead
// of being silently ignored once a template's own package manager is
// detected.
if (ctx.args.packageManager && !packageManagerOptions.includes(ctx.args.packageManager as PackageManagerName)) {
logger.error(`Invalid package manager: ${styleText('cyan', ctx.args.packageManager)}. Choose one of ${packageManagerOptions.map(pm => styleText('cyan', pm)).join(', ')}.`)
if (ctx.args.packageManager && !isPackageManagerName(ctx.args.packageManager)) {
logger.error(`Invalid package manager: ${styleText('cyan', ctx.args.packageManager)}. Choose one of ${packageManagerNames.map(pm => styleText('cyan', pm)).join(', ')}.`)
process.exit(ARG_ERROR_EXIT_CODE)
}

Expand Down Expand Up @@ -266,7 +253,7 @@ export default defineCommand({
if (ctx.args.template) {
return false
}
return !packageManagerOptions.includes(ctx.args.packageManager as PackageManagerName)
return !isPackageManagerName(ctx.args.packageManager)
}
return ctx.args[name] === undefined || ctx.args[name] === ''
})
Expand Down Expand Up @@ -484,8 +471,8 @@ export default defineCommand({
const recoveryCommands: string[] = []

const currentPackageManager = detectCurrentPackageManager()
const packageManagerArg = ctx.args.packageManager as PackageManagerName
const packageManagerSelectOptions = packageManagerOptions.map(pm => ({
const packageManagerArg = ctx.args.packageManager
const packageManagerSelectOptions = packageManagerNames.map(pm => ({
label: pm,
value: pm,
hint: currentPackageManager === pm ? 'current' : undefined,
Expand All @@ -497,16 +484,16 @@ export default defineCommand({
// workspace config (e.g. `pnpm-workspace.yaml`) behind and silently break
// the project. Shipping a template that works across package managers (i.e.
// without a lockfile) is left to the template author.
const templatePackageManager = await detectTemplatePackageManager(template.dir)
const templatePackageManager = await detectPackageManager(template.dir, { includeParentDirs: false })

let selectedPackageManager: PackageManagerName
let selectedPackageManager: AgentName
// Set when an explicit `--packageManager` conflicts with the template's pin:
// installing would run the requested package manager against the template's
// lockfile and workspace config for a different one, leaving a broken
// project. We won't mutate the template, so we scaffold it as-is and skip
// the install, letting the user reconcile the package manager themselves.
let skipInstallOnConflict = false
if (packageManagerOptions.includes(packageManagerArg)) {
if (isPackageManagerName(packageManagerArg)) {
selectedPackageManager = packageManagerArg
if (templatePackageManager && templatePackageManager.name !== packageManagerArg) {
skipInstallOnConflict = true
Expand Down Expand Up @@ -578,10 +565,9 @@ export default defineCommand({

const result = await runInstall({
cwd: template.dir,
packageManager: resolvePackageManagerDescriptor(
selectedPackageManager,
templatePackageManager?.name === selectedPackageManager ? templatePackageManager.version : undefined,
),
packageManager: templatePackageManager?.name === selectedPackageManager
? templatePackageManager
: { name: selectedPackageManager, agent: selectedPackageManager },
onOutput: installLog.onOutput,
onStatus: message => installSpinner.message(message),
signal: installController.signal,
Expand Down Expand Up @@ -864,31 +850,6 @@ async function getTemplateDependencies(templateDir: string) {
}
}

export interface TemplatePackageManager {
name: PackageManagerName
version?: string
}

/**
* Detect the package manager a template pins, scoped to the template directory
* (so we don't pick up the parent project's setup) via its lockfile, marker
* files or `packageManager` field. Returns `undefined` when the template pins
* none, in which case it is package-manager agnostic and the user is free to
* pick any. Detection errors are treated as "no pin".
*/
export async function detectTemplatePackageManager(templateDir: string): Promise<TemplatePackageManager | undefined> {
const detected = await detectPackageManager(templateDir, {
includeParentDirs: false,
ignoreArgv: true,
}).catch(() => undefined)

if (!detected) {
return
}

return { name: detected.name, version: detected.version }
}

function isVerbose(logLevel?: string) {
return logLevel === 'verbose' || Boolean(process.env.DEBUG)
}
Expand All @@ -899,7 +860,7 @@ function detectCurrentPackageManager() {
return
}
const [name] = userAgent.split('/')
if (packageManagerOptions.includes(name as PackageManagerName)) {
return name as PackageManagerName
if (isPackageManagerName(name)) {
return name
}
}
39 changes: 1 addition & 38 deletions packages/create-nuxt/test/unit/init.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'

import { detectTemplatePackageManager, getNextSteps, useYarnNodeModulesLinker } from '../../src/init'
import { getNextSteps, useYarnNodeModulesLinker } from '../../src/init'

describe('useYarnNodeModulesLinker', () => {
let dir: string
Expand Down Expand Up @@ -76,40 +76,3 @@ describe('getNextSteps', () => {
expect(getNextSteps({ ...base, dir: 'my-app', packageManager: 'deno' })).toEqual(['cd my-app', 'deno task dev'])
})
})

describe('detectTemplatePackageManager', () => {
let dir: string

beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), 'nuxt-init-test-'))
})

afterEach(async () => {
await rm(dir, { recursive: true, force: true })
})

async function writePkg(pkg: Record<string, unknown>) {
await writeFile(join(dir, 'package.json'), JSON.stringify(pkg, null, 2))
}

it('detects the package manager from a lockfile', async () => {
await writePkg({ name: 'app' })
await writeFile(join(dir, 'pnpm-lock.yaml'), '')

expect((await detectTemplatePackageManager(dir))?.name).toBe('pnpm')
})

it('detects the package manager and version from the `packageManager` field', async () => {
await writePkg({ name: 'app', packageManager: 'yarn@4.0.0' })

const detected = await detectTemplatePackageManager(dir)
expect(detected?.name).toBe('yarn')
expect(detected?.version).toBe('4.0.0')
})

it('returns undefined when the template pins no package manager', async () => {
await writePkg({ name: 'app' })

expect(await detectTemplatePackageManager(dir)).toBeUndefined()
})
})
2 changes: 1 addition & 1 deletion packages/nuxt-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"get-port-please": "^3.2.0",
"my-bad": "^0.2.9",
"obug": "^3.0.0",
"package-manager-detector": "^1.8.0",
"pathe": "^2.0.3",
"perfect-debounce": "^2.1.0",
"pkg-types": "^2.3.3",
Expand All @@ -102,7 +103,6 @@
"jiti": "^2.7.0",
"nitro": "^3.0.260903-beta",
"nitropack": "^2.13.4",
"nypm": "^0.6.10",
"rolldown": "^1.2.11",
"tsdown": "^0.23.0",
"typescript": "^6.0.3",
Expand Down
5 changes: 2 additions & 3 deletions packages/nuxt-cli/src/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { styleText } from 'node:util'
import { box } from '@clack/prompts'
import { defineCommand } from 'citty'

import { detectPackageManager } from 'nypm'
import { readPackageJSON } from 'pkg-types'
import { camelCase } from 'scule'
import { isBun, isDeno, isMinimal } from 'std-env'
Expand All @@ -23,7 +22,7 @@ import { logger } from '../utils/logger'
import { resolveNitroVersion } from '../utils/nitro'
import { getNuxtConfig } from '../utils/nuxt-config'
import { readDependencyPackageJson } from '../utils/package-json'
import { getPackageManagerVersion } from '../utils/packageManagers'
import { detectPackageManager, getPackageManagerVersion } from '../utils/package-managers'
import { resolveRootDir } from '../utils/paths'
import { tryResolveNuxt } from '../utils/resolve-nuxt'
import { rootDirArgs } from './_shared'
Expand Down Expand Up @@ -96,7 +95,7 @@ export default defineCommand({
const moduleNames = modules.filter(module => module !== null)
const builder = nuxtConfig.builder || 'vite'
const packageManager = detectedPackageManager
? `${detectedPackageManager.name}@${getPackageManagerVersion(detectedPackageManager.command)}`
? `${detectedPackageManager.name}@${getPackageManagerVersion(detectedPackageManager.name)}`
: 'unknown'
const osType = os.type()
const cpus = os.cpus()
Expand Down
8 changes: 0 additions & 8 deletions packages/nuxt-cli/src/commands/module/_utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import type { PackageManager } from 'nypm'
import type { PackageJson } from 'pkg-types'

import { existsSync } from 'node:fs'

import { styleText } from 'node:util'
import { confirm, isCancel } from '@clack/prompts'
import { resolve } from 'pathe'
import { satisfies } from 'verkit'

import { fetchJson } from '../../utils/fetch'
Expand Down Expand Up @@ -240,10 +236,6 @@ export async function ensureNuxtDependency(cwd: string, projectPkg: PackageJson)
return !isCancel(shouldContinue) && shouldContinue === true
}

export function isPnpmWorkspace(packageManager: PackageManager | undefined, cwd: string): boolean {
return packageManager?.name === 'pnpm' && existsSync(resolve(cwd, 'pnpm-workspace.yaml'))
}

/** Forward `cwd` and log-level args to a chained command invocation. */
export function forwardCommandArgs(args: Record<string, unknown>): string[] {
return Object.entries(args)
Expand Down
Loading
Loading