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
60 changes: 59 additions & 1 deletion packages/nuxi/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import type { PackageManagerName } from 'nypm'
import type { TemplateData } from '../utils/starter-templates'

import { existsSync } from 'node:fs'
import { rm } from 'node:fs/promises'
import process from 'node:process'

import { box, cancel, confirm, intro, isCancel, outro, select, spinner, tasks, text } from '@clack/prompts'
import { defineCommand } from 'citty'
import { colors } from 'consola/utils'
import { downloadTemplate, startShell } from 'giget'
import { installDependencies } from 'nypm'
import { detectPackageManager, installDependencies } from 'nypm'
import { $fetch } from 'ofetch'
import { basename, join, relative, resolve } from 'pathe'
import { findFile, readPackageJSON, writePackageJSON } from 'pkg-types'
Expand Down Expand Up @@ -347,6 +348,11 @@ export default defineCommand({
selectedPackageManager = result
}

// Align the template with the selected package manager: remove foreign
// lockfiles and the hardcoded `packageManager` field (e.g. `pnpm@x`) that
// would otherwise mismatch the package manager the user just picked.
await alignPackageManager(template.dir, selectedPackageManager)

// Determine if we should init git
let gitInit: boolean | undefined = ctx.args.gitInit === 'false' as unknown ? false : ctx.args.gitInit
if (gitInit === undefined) {
Expand Down Expand Up @@ -597,6 +603,58 @@ async function getTemplateDependencies(templateDir: string) {
}
}

export async function alignPackageManager(templateDir: string, packageManager: PackageManagerName) {
// Detect the package manager the template ships with (from its
// `packageManager` field, lockfile or marker files). Scope detection to the
// template directory so we don't pick up the parent project's setup.
const detected = await detectPackageManager(templateDir, {
includeParentDirs: false,
ignoreArgv: true,
}).catch(() => undefined)

// Nothing to do if the template doesn't pin a package manager or it already
// matches the one the user picked (keep its lockfile to speed up install).
if (!detected || detected.name === packageManager) {
return
}

// Drop the detected package manager's lockfile and marker files (e.g.
// `pnpm-workspace.yaml`) so the install generates a fresh, correct lockfile
// and nypm no longer mis-detects the package manager later.
const filesToRemove = [detected.lockFile, detected.files]
.flat()
.filter((file): file is string => Boolean(file))

await Promise.all(
filesToRemove.map(async (file) => {
const filePath = join(templateDir, file)
if (existsSync(filePath)) {
await rm(filePath, { force: true }).catch((err) => {
logger.warn(`Could not remove ${colors.cyan(file)}: ${err}`)
})
}
}),
)

// Drop the hardcoded `packageManager` field: its baked-in version belongs to
// a different package manager, so it's no longer valid.
try {
const packageJsonPath = join(templateDir, 'package.json')
if (!existsSync(packageJsonPath)) {
return
}

const pkg = await readPackageJSON(packageJsonPath, { try: true })
if (pkg?.packageManager) {
delete pkg.packageManager
await writePackageJSON(packageJsonPath, pkg)
}
}
catch (err) {
logger.warn(`Could not update the \`packageManager\` field: ${err}`)
}
}

function detectCurrentPackageManager() {
const userAgent = process.env.npm_config_user_agent
if (!userAgent) {
Expand Down
68 changes: 68 additions & 0 deletions packages/nuxi/test/unit/commands/init.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { existsSync } from 'node:fs'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'

import { alignPackageManager } from '../../../src/commands/init'

describe('alignPackageManager', () => {
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))
}

async function readPkg() {
return JSON.parse(await readFile(join(dir, 'package.json'), 'utf-8'))
}

it('removes the lockfile, marker files and `packageManager` field on mismatch', async () => {
await writePkg({ name: 'app', packageManager: 'pnpm@9.0.0' })
await writeFile(join(dir, 'pnpm-lock.yaml'), '')
await writeFile(join(dir, 'pnpm-workspace.yaml'), 'packages: []')

await alignPackageManager(dir, 'npm')

expect(existsSync(join(dir, 'pnpm-lock.yaml'))).toBe(false)
expect(existsSync(join(dir, 'pnpm-workspace.yaml'))).toBe(false)
expect((await readPkg()).packageManager).toBeUndefined()
})

it('keeps everything when the template already matches the selection', async () => {
await writePkg({ name: 'app', packageManager: 'pnpm@9.0.0' })
await writeFile(join(dir, 'pnpm-lock.yaml'), '')
await writeFile(join(dir, 'pnpm-workspace.yaml'), 'packages: []')

await alignPackageManager(dir, 'pnpm')

expect(existsSync(join(dir, 'pnpm-lock.yaml'))).toBe(true)
expect(existsSync(join(dir, 'pnpm-workspace.yaml'))).toBe(true)
expect((await readPkg()).packageManager).toBe('pnpm@9.0.0')
})

it('removes a lockfile even when the template has no `packageManager` field', async () => {
await writePkg({ name: 'app' })
await writeFile(join(dir, 'pnpm-lock.yaml'), '')

await alignPackageManager(dir, 'npm')

expect(existsSync(join(dir, 'pnpm-lock.yaml'))).toBe(false)
expect((await readPkg()).packageManager).toBeUndefined()
})

it('is a no-op when the template pins no package manager', async () => {
await writePkg({ name: 'app' })

await expect(alignPackageManager(dir, 'npm')).resolves.not.toThrow()
expect((await readPkg()).name).toBe('app')
})
})
Loading