diff --git a/CHANGELOG.md b/CHANGELOG.md index 34f3cbff..6088feb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on Keep a Changelog and the project follows Semantic Version ## [Unreleased] +### Bug Fixes + +- **create-rezi/cli**: Fixed `npm create rezi` / `bun create rezi` no-op scaffolding by making CLI main-entry detection symlink-safe. + ## [0.1.0-alpha.58] - 2026-03-06 ### Breaking Changes diff --git a/packages/create-rezi/src/__tests__/mainEntry.test.ts b/packages/create-rezi/src/__tests__/mainEntry.test.ts new file mode 100644 index 00000000..487caa42 --- /dev/null +++ b/packages/create-rezi/src/__tests__/mainEntry.test.ts @@ -0,0 +1,38 @@ +import { mkdtemp, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { assert, test } from "@rezi-ui/testkit"; +import { isMainModuleEntry } from "../mainEntry.js"; + +test("isMainModuleEntry returns true for direct script execution", async () => { + const root = await mkdtemp(join(tmpdir(), "rezi-main-entry-")); + const scriptPath = join(root, "create-rezi.mjs"); + await writeFile(scriptPath, "export {};\n", "utf8"); + + const moduleUrl = pathToFileURL(scriptPath).href; + assert.equal(isMainModuleEntry(scriptPath, moduleUrl), true); +}); + +test("isMainModuleEntry resolves symlinked launchers", async () => { + const root = await mkdtemp(join(tmpdir(), "rezi-main-entry-")); + const scriptPath = join(root, "dist-index.mjs"); + const launcherPath = join(root, "launcher.mjs"); + await writeFile(scriptPath, "export {};\n", "utf8"); + await symlink(scriptPath, launcherPath); + + const moduleUrl = pathToFileURL(scriptPath).href; + assert.equal(isMainModuleEntry(launcherPath, moduleUrl), true); +}); + +test("isMainModuleEntry rejects non-matching entry paths", async () => { + const root = await mkdtemp(join(tmpdir(), "rezi-main-entry-")); + const scriptPath = join(root, "dist-index.mjs"); + const otherPath = join(root, "other.mjs"); + await writeFile(scriptPath, "export {};\n", "utf8"); + await writeFile(otherPath, "export {};\n", "utf8"); + + const moduleUrl = pathToFileURL(scriptPath).href; + assert.equal(isMainModuleEntry(otherPath, moduleUrl), false); + assert.equal(isMainModuleEntry(undefined, moduleUrl), false); +}); diff --git a/packages/create-rezi/src/index.ts b/packages/create-rezi/src/index.ts index 422f594b..31cced61 100644 --- a/packages/create-rezi/src/index.ts +++ b/packages/create-rezi/src/index.ts @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { relative, resolve } from "node:path"; import { cwd, exit, stdin, stdout } from "node:process"; import { createInterface } from "node:readline/promises"; -import { fileURLToPath } from "node:url"; +import { isMainModuleEntry } from "./mainEntry.js"; import { TEMPLATE_DEFINITIONS, createProject, @@ -276,7 +276,7 @@ async function main(): Promise { } } -const isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]); +const isMain = isMainModuleEntry(process.argv[1], import.meta.url); if (isMain) { main().catch((err) => { stdout.write(`\ncreate-rezi error: ${err instanceof Error ? err.message : String(err)}\n`); diff --git a/packages/create-rezi/src/mainEntry.ts b/packages/create-rezi/src/mainEntry.ts new file mode 100644 index 00000000..ac993ab1 --- /dev/null +++ b/packages/create-rezi/src/mainEntry.ts @@ -0,0 +1,20 @@ +import { realpathSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +function resolveCanonicalPath(pathValue: string): string { + try { + return realpathSync(pathValue); + } catch { + return pathValue; + } +} + +export function isMainModuleEntry(argvPath: string | undefined, moduleUrl: string): boolean { + if (!argvPath) { + return false; + } + const modulePath = resolveCanonicalPath(fileURLToPath(moduleUrl)); + const entryPath = resolveCanonicalPath(resolve(argvPath)); + return modulePath === entryPath; +}