diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index 9df9fdc5e..0fdcbcc4a 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -10,8 +10,8 @@ import { colors } from 'consola/utils' import { downloadTemplate, startShell } from 'giget' import { installDependencies } from 'nypm' import { $fetch } from 'ofetch' -import { join, relative, resolve } from 'pathe' -import { readPackageJSON, writePackageJSON } from 'pkg-types' +import { basename, join, relative, resolve } from 'pathe' +import { findFile, readPackageJSON, writePackageJSON } from 'pkg-types' import { hasTTY } from 'std-env' import { x } from 'tinyexec' @@ -234,6 +234,26 @@ export default defineCommand({ preferOffline: Boolean(ctx.args.preferOffline), registry: process.env.NUXI_INIT_REGISTRY || DEFAULT_REGISTRY, }) + + if (ctx.args.dir.length > 0) { + const path = await findFile('package.json', { + startingFrom: join(templateDownloadPath, 'package.json'), + reverse: true, + }) + if (path) { + const pkg = await readPackageJSON(path, { try: true }) + if (pkg && pkg.name) { + const slug = basename(templateDownloadPath) + .replace(/[^\w-]/g, '-') + .replace(/-{2,}/g, '-') + .replace(/^-|-$/g, '') + if (slug) { + pkg.name = slug + await writePackageJSON(path, pkg) + } + } + } + } } catch (err) { if (process.env.DEBUG) { diff --git a/packages/nuxt-cli/test/e2e/init.spec.ts b/packages/nuxt-cli/test/e2e/init.spec.ts new file mode 100644 index 000000000..d1f7b61bc --- /dev/null +++ b/packages/nuxt-cli/test/e2e/init.spec.ts @@ -0,0 +1,121 @@ +import { existsSync } from 'node:fs' + +import { readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { isWindows } from 'std-env' +import { x } from 'tinyexec' +import { describe, expect, it } from 'vitest' + +const fixtureDir = fileURLToPath(new URL('../../../../playground', import.meta.url)) +const nuxi = fileURLToPath(new URL('../../bin/nuxi.mjs', import.meta.url)) + +describe('init command package name slugification', () => { + it('should slugify directory names with special characters', { timeout: isWindows ? 200000 : 50000 }, async () => { + const dir = tmpdir() + const specialDirName = 'my@special#project!' + const installPath = join(dir, specialDirName) + + await rm(installPath, { recursive: true, force: true }) + try { + await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--gitInit=false', '--preferOffline', '--install=false'], { + throwOnError: true, + nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, + }) + + // Check that package.json was created + const packageJsonPath = join(installPath, 'package.json') + expect(existsSync(packageJsonPath)).toBeTruthy() + + // Read package.json and verify the name was slugified + const packageJsonContent = await readFile(packageJsonPath, 'utf-8') + const packageJson = JSON.parse(packageJsonContent) + + // The name should be slugified: my@special#project! -> my-special-project + expect(packageJson.name).toBe('my-special-project') + } + finally { + await rm(installPath, { recursive: true, force: true }) + } + }) + + it('should handle consecutive special characters', { timeout: isWindows ? 200000 : 50000 }, async () => { + const dir = tmpdir() + const specialDirName = 'test___project@@@name!!!' + const installPath = join(dir, specialDirName) + + await rm(installPath, { recursive: true, force: true }) + try { + await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--gitInit=false', '--preferOffline', '--install=false'], { + throwOnError: true, + nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, + }) + + const packageJsonPath = join(installPath, 'package.json') + expect(existsSync(packageJsonPath)).toBeTruthy() + + const packageJsonContent = await readFile(packageJsonPath, 'utf-8') + const packageJson = JSON.parse(packageJsonContent) + + // Note: underscores are word characters (\w) so they are preserved + // Only @@@!!! are replaced with hyphens, then consecutive hyphens are collapsed + expect(packageJson.name).toBe('test___project-name') + } + finally { + await rm(installPath, { recursive: true, force: true }) + } + }) + + it('should handle leading and trailing special characters', { timeout: isWindows ? 200000 : 50000 }, async () => { + const dir = tmpdir() + const specialDirName = '---project-name---' + const installPath = join(dir, specialDirName) + + await rm(installPath, { recursive: true, force: true }) + try { + await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--gitInit=false', '--preferOffline', '--install=false'], { + throwOnError: true, + nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, + }) + + const packageJsonPath = join(installPath, 'package.json') + expect(existsSync(packageJsonPath)).toBeTruthy() + + const packageJsonContent = await readFile(packageJsonPath, 'utf-8') + const packageJson = JSON.parse(packageJsonContent) + + // Should remove leading and trailing hyphens + expect(packageJson.name).toBe('project-name') + } + finally { + await rm(installPath, { recursive: true, force: true }) + } + }) + + it('should preserve valid package names without modification', { timeout: isWindows ? 200000 : 50000 }, async () => { + const dir = tmpdir() + const validDirName = 'my-valid-project-name' + const installPath = join(dir, validDirName) + + await rm(installPath, { recursive: true, force: true }) + try { + await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--gitInit=false', '--preferOffline', '--install=false'], { + throwOnError: true, + nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, + }) + + const packageJsonPath = join(installPath, 'package.json') + expect(existsSync(packageJsonPath)).toBeTruthy() + + const packageJsonContent = await readFile(packageJsonPath, 'utf-8') + const packageJson = JSON.parse(packageJsonContent) + + // Valid names should remain unchanged + expect(packageJson.name).toBe('my-valid-project-name') + } + finally { + await rm(installPath, { recursive: true, force: true }) + } + }) +})