From 2c699b532669da2f93e14040f21fa461ff415675 Mon Sep 17 00:00:00 2001 From: Eckhardt-D Date: Thu, 30 May 2024 07:42:25 +0200 Subject: [PATCH 1/3] feat(init): Use user provided dir for package.json name field --- src/commands/init.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 4c90c4e88..1a001ca9c 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,10 +1,11 @@ import { downloadTemplate, startShell } from 'giget' import type { DownloadTemplateResult } from 'giget' -import { relative, resolve } from 'pathe' +import { relative, resolve, join } from 'pathe' import { consola } from 'consola' import { installDependencies } from 'nypm' import type { PackageManagerName } from 'nypm' import { defineCommand } from 'citty' +import { readPackageJSON, writePackageJSON } from 'pkg-types' import { sharedArgs } from './_shared' @@ -83,6 +84,13 @@ export default defineCommand({ preferOffline: Boolean(ctx.args.preferOffline), registry: process.env.NUXI_INIT_REGISTRY || DEFAULT_REGISTRY, }) + + if (ctx.args.dir.length > 0) { + const pkg = await readPackageJSON(template.dir) + // Handles paths like ../some/dir with sane fallback + pkg.name = ctx.args.dir.split('/').at(-1) || pkg.name + await writePackageJSON(join(template.dir, 'package.json'), pkg) + } } catch (err) { if (process.env.DEBUG) { From ea9b055721ae4eddeaa542ff2e8be0502fb022fe Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 24 Sep 2025 11:20:10 +0100 Subject: [PATCH 2/3] fix: use findFile + slugify directory --- packages/nuxi/src/commands/init.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index d8e1192fd..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' @@ -236,10 +236,23 @@ export default defineCommand({ }) if (ctx.args.dir.length > 0) { - const pkg = await readPackageJSON(template.dir) - // Handles paths like ../some/dir with sane fallback - pkg.name = ctx.args.dir.split('/').at(-1) || pkg.name - await writePackageJSON(join(template.dir, 'package.json'), pkg) + 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) { From de8086fd447dee5fabcdaf9368d2b301c4e79459 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 24 Sep 2025 11:27:32 +0100 Subject: [PATCH 3/3] test: add tests for new behaviour --- packages/nuxt-cli/test/e2e/init.spec.ts | 121 ++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 packages/nuxt-cli/test/e2e/init.spec.ts 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 }) + } + }) +})