#!/usr/bin/env node /* deno-lint-ignore-file */ /* Automatic test matrix for project root detection. Creates temporary fixtures for various ecosystems and validates findProjectRoot(). No external options or flags required. Safe to run multiple times. */ const os = require("node:os"); const path = require("node:path"); const fs = require("fs-extra"); const { promisify } = require("node:util"); const { execFile } = require("node:child_process"); const process = require("node:process"); const execFileAsync = promisify(execFile); const { findProjectRoot } = require("./projectRoot.js"); async function cmdAvailable(cmd) { try { await execFileAsync(cmd, ["--version"], { timeout: 500, windowsHide: true }); return true; } catch { return false; } async function testSvnMarker() { const root = await mkTmpDir("svn"); const nested = path.join(root, "proj", "code"); await fs.ensureDir(nested); await fs.ensureDir(path.join(root, ".svn")); const found = await findProjectRoot(nested); assertEqual(found, root, ".svn marker should be detected"); return { name: "svn-marker", ok: true }; } async function testSymlinkStart() { const root = await mkTmpDir("symlink-start"); const nested = path.join(root, "a", "b"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, ".project-root"), "\n"); const tmp = await mkTmpDir("symlink-tmp"); const link = path.join(tmp, "link-to-b"); try { await fs.symlink(nested, link); } catch { // symlink may not be permitted on some systems; skip return { name: "symlink-start", ok: true, skipped: true }; } const found = await findProjectRoot(link); assertEqual(found, root, "should resolve symlinked start to real root"); return { name: "symlink-start", ok: true }; } async function testSubmoduleLikeInnerGitFile() { const root = await mkTmpDir("submodule-like"); const mid = path.join(root, "mid"); const leaf = path.join(mid, "leaf"); await fs.ensureDir(leaf); // outer repo await fs.ensureDir(path.join(root, ".git")); // inner submodule-like .git file await fs.writeFile(path.join(mid, ".git"), "gitdir: ../.git/modules/mid\n"); const found = await findProjectRoot(leaf); assertEqual(found, root, "outermost .git should win on tie weight"); return { name: "submodule-like-gitfile", ok: true }; } } async function mkTmpDir(name) { const base = await fs.realpath(os.tmpdir()); const dir = await fs.mkdtemp(path.join(base, `flattener-${name}-`)); return dir; } function assertEqual(actual, expected, msg) { if (actual !== expected) { throw new Error(`${msg}: expected=\"${expected}\" actual=\"${actual}\"`); } } async function testSentinel() { const root = await mkTmpDir("sentinel"); const nested = path.join(root, "a", "b", "c"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, ".project-root"), "\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "sentinel .project-root should win"); return { name: "sentinel", ok: true }; } async function testOtherSentinels() { const root = await mkTmpDir("other-sentinels"); const nested = path.join(root, "x", "y"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, ".workspace-root"), "\n"); const found1 = await findProjectRoot(nested); assertEqual(found1, root, "sentinel .workspace-root should win"); await fs.remove(path.join(root, ".workspace-root")); await fs.writeFile(path.join(root, ".repo-root"), "\n"); const found2 = await findProjectRoot(nested); assertEqual(found2, root, "sentinel .repo-root should win"); return { name: "other-sentinels", ok: true }; } async function testGitCliAndMarker() { const hasGit = await cmdAvailable("git"); if (!hasGit) return { name: "git-cli", ok: true, skipped: true }; const root = await mkTmpDir("git"); const nested = path.join(root, "pkg", "src"); await fs.ensureDir(nested); await execFileAsync("git", ["init"], { cwd: root, timeout: 2000 }); const found = await findProjectRoot(nested); await assertEqual(found, root, "git toplevel should be detected"); return { name: "git-cli", ok: true }; } async function testHgMarkerOrCli() { // Prefer simple marker test to avoid requiring Mercurial install const root = await mkTmpDir("hg"); const nested = path.join(root, "lib"); await fs.ensureDir(nested); await fs.ensureDir(path.join(root, ".hg")); const found = await findProjectRoot(nested); await assertEqual(found, root, ".hg marker should be detected"); return { name: "hg-marker", ok: true }; } async function testWorkspacePnpm() { const root = await mkTmpDir("pnpm-workspace"); const pkgA = path.join(root, "packages", "a"); await fs.ensureDir(pkgA); await fs.writeFile(path.join(root, "pnpm-workspace.yaml"), "packages:\n - packages/*\n"); const found = await findProjectRoot(pkgA); await assertEqual(found, root, "pnpm-workspace.yaml should be detected"); return { name: "pnpm-workspace", ok: true }; } async function testPackageJsonWorkspaces() { const root = await mkTmpDir("package-workspaces"); const pkgA = path.join(root, "packages", "a"); await fs.ensureDir(pkgA); await fs.writeJson(path.join(root, "package.json"), { private: true, workspaces: ["packages/*"] }, { spaces: 2 }); const found = await findProjectRoot(pkgA); await assertEqual(found, root, "package.json workspaces should be detected"); return { name: "package.json-workspaces", ok: true }; } async function testLockfiles() { const root = await mkTmpDir("lockfiles"); const nested = path.join(root, "src"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, "yarn.lock"), "\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "yarn.lock should be detected"); return { name: "lockfiles", ok: true }; } async function testLanguageConfigs() { const root = await mkTmpDir("lang-configs"); const nested = path.join(root, "x", "y"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, "pyproject.toml"), "[tool.poetry]\nname='tmp'\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "pyproject.toml should be detected"); return { name: "language-configs", ok: true }; } async function testPreferOuterOnTie() { const root = await mkTmpDir("tie"); const mid = path.join(root, "mid"); const leaf = path.join(mid, "leaf"); await fs.ensureDir(leaf); // same weight marker at two levels await fs.writeFile(path.join(root, "requirements.txt"), "\n"); await fs.writeFile(path.join(mid, "requirements.txt"), "\n"); const found = await findProjectRoot(leaf); await assertEqual(found, root, "outermost directory should win on equal weight"); return { name: "prefer-outermost-tie", ok: true }; } // Additional coverage: Bazel, Nx/Turbo/Rush, Go workspaces, Deno, Java/Scala, PHP, Rust, Nix, Changesets, env markers, // and priority interaction between package.json and lockfiles. async function testBazelWorkspace() { const root = await mkTmpDir("bazel"); const nested = path.join(root, "apps", "svc"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, "WORKSPACE"), "workspace(name=\"tmp\")\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "Bazel WORKSPACE should be detected"); return { name: "bazel-workspace", ok: true }; } async function testNx() { const root = await mkTmpDir("nx"); const nested = path.join(root, "apps", "web"); await fs.ensureDir(nested); await fs.writeJson(path.join(root, "nx.json"), { npmScope: "tmp" }, { spaces: 2 }); const found = await findProjectRoot(nested); await assertEqual(found, root, "nx.json should be detected"); return { name: "nx", ok: true }; } async function testTurbo() { const root = await mkTmpDir("turbo"); const nested = path.join(root, "packages", "x"); await fs.ensureDir(nested); await fs.writeJson(path.join(root, "turbo.json"), { pipeline: {} }, { spaces: 2 }); const found = await findProjectRoot(nested); await assertEqual(found, root, "turbo.json should be detected"); return { name: "turbo", ok: true }; } async function testRush() { const root = await mkTmpDir("rush"); const nested = path.join(root, "apps", "a"); await fs.ensureDir(nested); await fs.writeJson(path.join(root, "rush.json"), { projectFolderMinDepth: 1 }, { spaces: 2 }); const found = await findProjectRoot(nested); await assertEqual(found, root, "rush.json should be detected"); return { name: "rush", ok: true }; } async function testGoWorkAndMod() { const root = await mkTmpDir("gowork"); const mod = path.join(root, "modA"); const nested = path.join(mod, "pkg"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, "go.work"), "go 1.22\nuse ./modA\n"); await fs.writeFile(path.join(mod, "go.mod"), "module example.com/a\ngo 1.22\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "go.work should define the workspace root"); return { name: "go-work", ok: true }; } async function testDenoJson() { const root = await mkTmpDir("deno"); const nested = path.join(root, "src"); await fs.ensureDir(nested); await fs.writeJson(path.join(root, "deno.json"), { tasks: {} }, { spaces: 2 }); const found = await findProjectRoot(nested); await assertEqual(found, root, "deno.json should be detected"); return { name: "deno-json", ok: true }; } async function testGradleSettings() { const root = await mkTmpDir("gradle"); const nested = path.join(root, "app"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, "settings.gradle"), "rootProject.name='tmp'\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "settings.gradle should be detected"); return { name: "gradle-settings", ok: true }; } async function testMavenPom() { const root = await mkTmpDir("maven"); const nested = path.join(root, "module"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, "pom.xml"), "\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "pom.xml should be detected"); return { name: "maven-pom", ok: true }; } async function testSbtBuild() { const root = await mkTmpDir("sbt"); const nested = path.join(root, "sub"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, "build.sbt"), "name := \"tmp\"\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "build.sbt should be detected"); return { name: "sbt-build", ok: true }; } async function testComposer() { const root = await mkTmpDir("composer"); const nested = path.join(root, "src"); await fs.ensureDir(nested); await fs.writeJson(path.join(root, "composer.json"), { name: "tmp/pkg" }, { spaces: 2 }); await fs.writeFile(path.join(root, "composer.lock"), "{}\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "composer.{json,lock} should be detected"); return { name: "composer", ok: true }; } async function testCargo() { const root = await mkTmpDir("cargo"); const nested = path.join(root, "src"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, "Cargo.toml"), "[package]\nname='tmp'\nversion='0.0.0'\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "Cargo.toml should be detected"); return { name: "cargo", ok: true }; } async function testNixFlake() { const root = await mkTmpDir("nix"); const nested = path.join(root, "work"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, "flake.nix"), "{ }\n"); const found = await findProjectRoot(nested); await assertEqual(found, root, "flake.nix should be detected"); return { name: "nix-flake", ok: true }; } async function testChangesetConfig() { const root = await mkTmpDir("changeset"); const nested = path.join(root, "pkg"); await fs.ensureDir(nested); await fs.ensureDir(path.join(root, ".changeset")); await fs.writeJson(path.join(root, ".changeset", "config.json"), { $schema: "https://unpkg.com/@changesets/config@2.3.1/schema.json" }, { spaces: 2 }); const found = await findProjectRoot(nested); await assertEqual(found, root, ".changeset/config.json should be detected"); return { name: "changesets", ok: true }; } async function testEnvCustomMarker() { const root = await mkTmpDir("env-marker"); const nested = path.join(root, "dir"); await fs.ensureDir(nested); await fs.writeFile(path.join(root, "MY_ROOT"), "\n"); const prev = process.env.PROJECT_ROOT_MARKERS; process.env.PROJECT_ROOT_MARKERS = "MY_ROOT"; try { const found = await findProjectRoot(nested); await assertEqual(found, root, "custom env marker should be honored"); } finally { if (prev === undefined) delete process.env.PROJECT_ROOT_MARKERS; else process.env.PROJECT_ROOT_MARKERS = prev; } return { name: "env-custom-marker", ok: true }; } async function testPackageLowPriorityVsLock() { const root = await mkTmpDir("pkg-vs-lock"); const nested = path.join(root, "nested"); await fs.ensureDir(path.join(nested, "deep")); await fs.writeJson(path.join(nested, "package.json"), { name: "nested" }, { spaces: 2 }); await fs.writeFile(path.join(root, "yarn.lock"), "\n"); const found = await findProjectRoot(path.join(nested, "deep")); await assertEqual(found, root, "lockfile at root should outrank nested package.json"); return { name: "package-vs-lock-priority", ok: true }; } async function run() { const tests = [ testSentinel, testOtherSentinels, testGitCliAndMarker, testHgMarkerOrCli, testWorkspacePnpm, testPackageJsonWorkspaces, testLockfiles, testLanguageConfigs, testPreferOuterOnTie, testBazelWorkspace, testNx, testTurbo, testRush, testGoWorkAndMod, testDenoJson, testGradleSettings, testMavenPom, testSbtBuild, testComposer, testCargo, testNixFlake, testChangesetConfig, testEnvCustomMarker, testPackageLowPriorityVsLock, testSvnMarker, testSymlinkStart, testSubmoduleLikeInnerGitFile, ]; const results = []; for (const t of tests) { try { const r = await t(); results.push({ ...r, ok: true }); console.log(`✔ ${r.name}${r.skipped ? " (skipped)" : ""}`); } catch (err) { console.error(`✖ ${t.name}:`, err && err.message ? err.message : err); results.push({ name: t.name, ok: false, error: String(err) }); } } const failed = results.filter((r) => !r.ok); console.log("\nSummary:"); for (const r of results) { console.log(`- ${r.name}: ${r.ok ? "ok" : "FAIL"}${r.skipped ? " (skipped)" : ""}`); } if (failed.length) { process.exitCode = 1; } } run().catch((e) => { console.error("Fatal error:", e); process.exit(1); });