diff --git a/scripts/verify-tarball.js b/scripts/verify-tarball.js index e91c7f0..f4f8bf3 100644 --- a/scripts/verify-tarball.js +++ b/scripts/verify-tarball.js @@ -1,7 +1,7 @@ -const { execSync } = require("node:child_process"); -const fs = require("node:fs"); -const os = require("node:os"); -const path = require("node:path"); +const { execSync } = require("child_process"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); function run(cmd, cwd = process.cwd()) { console.log(`\n> ${cmd}`); diff --git a/src/cli/help.ts b/src/cli/help.ts index 802196c..505a643 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -14,6 +14,7 @@ ${colors.bold}Commands:${colors.reset} internal Run intra-file identifier analysis only. ${colors.bold}General Options:${colors.reset} + --cwd Set the current working directory for analysis (default: process.cwd()) --help Show this help message --json Machine-readable output --silent Suppress all non-JSON output diff --git a/src/cli/index.ts b/src/cli/index.ts index 01bbcbd..1cf0d31 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -6,21 +6,79 @@ import { logUnusedExports, logUnusedFiles, logUnusedLocals, logVerbose, summary import { showHelp } from "./help"; import { parseArgs } from "./parser"; +import { statSync } from "fs"; +import path from "path"; + +export function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return ( + typeof error === "object" && + error !== null && + "code" in error && + typeof error?.code === "string" + ); +} + +function resolveAndValidateCwd(rawCwd?: string): string { + const resolvedCwd = rawCwd ? path.resolve(process.cwd(), rawCwd) : process.cwd(); + + if (!rawCwd) return resolvedCwd; + + let stat; + + try { + stat = statSync(resolvedCwd); + + if (!stat.isDirectory()) { + log(`Error: --cwd is not a directory: ${resolvedCwd}`); + process.exit(1); + } + } catch (error) { + if (isNodeError(error)) { + switch (error.code) { + case "ENOENT": + log(`Error: --cwd path does not exist:\n ${resolvedCwd}`); + break; + + case "ENOTDIR": + log(`Error: --cwd contains a non-directory segment:\n ${resolvedCwd}`); + break; + + case "EACCES": + case "EPERM": + log(`Error: Permission denied accessing --cwd:\n ${resolvedCwd}`); + break; + + default: + log(`Error: Unable to access --cwd:\n ${resolvedCwd}`); + } + } else { + log(`Error: Unable to access --cwd:\n ${resolvedCwd}`); + } + + process.exit(1); + } + + return resolvedCwd; +} + (async () => { const args = parseArgs(process.argv.slice(2)); - const [command, commandTarget] = args._; + const [command, targetPath] = args._; + + const cwd = resolveAndValidateCwd(args.cwd); // Internal single-file analysis mode if (command === "internal") { if (args.help) { if (!args.silent) { log("Usage: devoid internal [options]"); - log("Run `devoid --help` for full command list."); + log("Run `devoid --help` for global options."); } process.exit(0); } - const filePath = commandTarget; + const filePath = targetPath ? path.resolve(cwd, targetPath) : null; + if (!filePath) { if (!args.silent) { log("Error: No file provided for internal analysis."); @@ -62,7 +120,8 @@ import { parseArgs } from "./parser"; } // Project root path - const projectRoot = args._[0]; + const projectRoot = args._[0] ? path.resolve(cwd, args._[0]) : null; + if (!projectRoot) { if (!silent) { log("Error: No project path provided."); diff --git a/src/cli/internalMode.ts b/src/cli/internalMode.ts index a38f773..60ff8c5 100644 --- a/src/cli/internalMode.ts +++ b/src/cli/internalMode.ts @@ -1,4 +1,5 @@ -import fs from "fs"; +import { readFileSync } from "fs"; + import { analyzeLocalUsage } from "../core/locals/analyzeLocalUsage"; import { log } from "../utils"; import { colors } from "./colors"; @@ -19,7 +20,7 @@ export async function runInternalMode(filePath: string, args: any): Promise { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "devoid-cwd-")); + const projectDir = path.join(tmpRoot, "project"); + const srcDir = path.join(projectDir, "src"); + + writeFile(path.join(srcDir, "a.ts"), `export const foo = 123;\n`); + + const { code, stdout } = runCLI(["--cwd", "project", "src", "--exports"], { cwd: tmpRoot }); + + assert.equal(code, 0); + assert.match(stdout, /Unused Exports/i); + assert.match(stdout, /\bfoo\b/); +}); + +test("--cwd: resolves internal mode file path relative to cwd", () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "devoid-cwd-")); + const projectDir = path.join(tmpRoot, "project"); + const srcDir = path.join(projectDir, "src"); + + writeFile( + path.join(srcDir, "internal.ts"), + ` +function unusedFn() {} +function usedFn() {} +usedFn(); +`, + ); + + const { code, stdout } = runCLI(["--cwd", "project", "internal", "src/internal.ts"], { + cwd: tmpRoot, + }); + + assert.equal(code, 0); + assert.match(stdout, /Internal Usage Analysis/i); + assert.match(stdout, /\bunusedFn\b/); +}); + +test("--cwd: errors if directory does not exist", () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "devoid-cwd-")); + + const { code, stdout } = runCLI(["--cwd", "nope", "src", "--exports"], { cwd: tmpRoot }); + + assert.equal(code, 1); + assert.match(stdout, /--cwd/i); + assert.match(stdout, /does not exist/i); +}); + +test("--cwd: errors if path exists but is not a directory", () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "devoid-cwd-")); + const filePath = path.join(tmpRoot, "not-a-dir.txt"); + fs.writeFileSync(filePath, "hi", "utf8"); + + const { code, stdout } = runCLI(["--cwd", "not-a-dir.txt", "src"], { cwd: tmpRoot }); + + assert.equal(code, 1); + assert.match(stdout, /--cwd/i); + assert.match(stdout, /not a directory/i); +}); + +test("--cwd: works without --cwd (baseline behavior unchanged)", () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "devoid-cwd-")); + const srcDir = path.join(tmpRoot, "src"); + + writeFile(path.join(srcDir, "a.ts"), `export const foo = 123;\n`); + + const { code, stdout } = runCLI(["src", "--exports"], { cwd: tmpRoot }); + + assert.equal(code, 0); + assert.match(stdout, /\bfoo\b/); +}); diff --git a/tests/cli/utils/runCLI.ts b/tests/cli/utils/runCLI.ts new file mode 100644 index 0000000..4a2098c --- /dev/null +++ b/tests/cli/utils/runCLI.ts @@ -0,0 +1,17 @@ +import { spawnSync } from "child_process"; +import path from "path"; + +const NODE = process.execPath; +const CLI_ENTRY = path.resolve(__dirname, "../../../bin/devoid.js"); + +export function runCLI(args: string[], opts?: { cwd?: string }) { + const result = spawnSync(NODE, [CLI_ENTRY, ...args], { + cwd: opts?.cwd, + encoding: "utf8", + }); + + return { + code: result.status ?? 1, + stdout: (result.stdout ?? "") + (result.stderr ?? ""), + }; +} diff --git a/tests/exports/conflictingExports.test.ts b/tests/exports/conflictingExports.test.ts index 631a3ec..2cc2b75 100644 --- a/tests/exports/conflictingExports.test.ts +++ b/tests/exports/conflictingExports.test.ts @@ -1,5 +1,5 @@ +import assert from "assert/strict"; import fs from "fs"; -import assert from "node:assert/strict"; import test from "node:test"; import path from "path"; diff --git a/tests/exports/exportChains.test.ts b/tests/exports/exportChains.test.ts index 00fbb99..636e2de 100644 --- a/tests/exports/exportChains.test.ts +++ b/tests/exports/exportChains.test.ts @@ -1,7 +1,7 @@ -import assert from "node:assert"; -import fs from "node:fs"; -import path from "node:path"; +import assert from "assert"; +import fs from "fs"; import test from "node:test"; +import path from "path"; import { analyzeExportUsage } from "../../src/core/exports/exportUsage"; import { scanExports } from "../../src/core/exports/scanExports"; diff --git a/tests/exports/exportGraphResolver.test.ts b/tests/exports/exportGraphResolver.test.ts index d5e5086..90cc3d1 100644 --- a/tests/exports/exportGraphResolver.test.ts +++ b/tests/exports/exportGraphResolver.test.ts @@ -1,5 +1,5 @@ +import assert from "assert/strict"; import fs from "fs"; -import assert from "node:assert/strict"; import test from "node:test"; import path from "path"; diff --git a/tests/exports/exportUsage.test.ts b/tests/exports/exportUsage.test.ts index e743cbe..7c9f78e 100644 --- a/tests/exports/exportUsage.test.ts +++ b/tests/exports/exportUsage.test.ts @@ -1,7 +1,7 @@ -import assert from "node:assert"; -import fs from "node:fs"; -import path from "node:path"; +import assert from "assert"; +import fs from "fs"; import test from "node:test"; +import path from "path"; import { analyzeExportUsage } from "../../src/core/exports/exportUsage"; import { scanExports } from "../../src/core/exports/scanExports.js"; diff --git a/tests/exports/localPriority.test.ts b/tests/exports/localPriority.test.ts index 28232bd..c97eeed 100644 --- a/tests/exports/localPriority.test.ts +++ b/tests/exports/localPriority.test.ts @@ -1,5 +1,5 @@ +import assert from "assert/strict"; import fs from "fs"; -import assert from "node:assert/strict"; import test from "node:test"; import path from "path"; diff --git a/tests/exports/multiHop.test.ts b/tests/exports/multiHop.test.ts index 5dc75e4..8d845fa 100644 --- a/tests/exports/multiHop.test.ts +++ b/tests/exports/multiHop.test.ts @@ -1,5 +1,5 @@ +import assert from "assert/strict"; import fs from "fs"; -import assert from "node:assert/strict"; import test from "node:test"; import path from "path"; diff --git a/tests/exports/namedReexports.test.ts b/tests/exports/namedReexports.test.ts index 9401456..dd82624 100644 --- a/tests/exports/namedReexports.test.ts +++ b/tests/exports/namedReexports.test.ts @@ -1,5 +1,5 @@ +import assert from "assert/strict"; import fs from "fs"; -import assert from "node:assert/strict"; import test from "node:test"; import path from "path"; diff --git a/tests/exports/unresolvedReexports.test.ts b/tests/exports/unresolvedReexports.test.ts index 4d60568..ee0c39d 100644 --- a/tests/exports/unresolvedReexports.test.ts +++ b/tests/exports/unresolvedReexports.test.ts @@ -1,5 +1,5 @@ +import assert from "assert/strict"; import fs from "fs"; -import assert from "node:assert/strict"; import test from "node:test"; import path from "path"; diff --git a/tests/exports/wildcardExportChains.test.ts b/tests/exports/wildcardExportChains.test.ts index 172e0a5..76a7660 100644 --- a/tests/exports/wildcardExportChains.test.ts +++ b/tests/exports/wildcardExportChains.test.ts @@ -1,7 +1,7 @@ -import assert from "node:assert"; -import fs from "node:fs"; -import path from "node:path"; +import assert from "assert"; +import fs from "fs"; import test from "node:test"; +import path from "path"; import { analyzeExportUsage } from "../../src/core/exports/exportUsage"; import { scanExports } from "../../src/core/exports/scanExports.js"; diff --git a/tests/exports/wildcardReexport.test.ts b/tests/exports/wildcardReexport.test.ts index b2c63b3..77c6427 100644 --- a/tests/exports/wildcardReexport.test.ts +++ b/tests/exports/wildcardReexport.test.ts @@ -1,5 +1,5 @@ +import assert from "assert/strict"; import fs from "fs"; -import assert from "node:assert/strict"; import test from "node:test"; import path from "path"; diff --git a/tests/imports/importGraph.test.ts b/tests/imports/importGraph.test.ts index 10bb803..22defd6 100644 --- a/tests/imports/importGraph.test.ts +++ b/tests/imports/importGraph.test.ts @@ -1,7 +1,7 @@ -import assert from "node:assert"; -import fs from "node:fs"; -import path from "node:path"; +import assert from "assert"; +import fs from "fs"; import test from "node:test"; +import path from "path"; import { buildImportGraph } from "../../src/core/imports/buildImportGraph"; import { loadTSConfig } from "../../src/core/tsconfig/tsconfigLoader"; diff --git a/tests/imports/mixedImports.test.ts b/tests/imports/mixedImports.test.ts index e9b100b..8618d12 100644 --- a/tests/imports/mixedImports.test.ts +++ b/tests/imports/mixedImports.test.ts @@ -1,5 +1,5 @@ +import assert from "assert"; import fs from "fs"; -import assert from "node:assert"; import test from "node:test"; import path from "path"; diff --git a/tests/imports/typeOnlyImports.test.ts b/tests/imports/typeOnlyImports.test.ts index d3c15df..52508c4 100644 --- a/tests/imports/typeOnlyImports.test.ts +++ b/tests/imports/typeOnlyImports.test.ts @@ -1,5 +1,5 @@ +import assert from "assert"; import fs from "fs"; -import assert from "node:assert"; import test from "node:test"; import path from "path"; diff --git a/tests/imports/unresolvedImports.test.ts b/tests/imports/unresolvedImports.test.ts index 52cd317..deaa88b 100644 --- a/tests/imports/unresolvedImports.test.ts +++ b/tests/imports/unresolvedImports.test.ts @@ -1,7 +1,7 @@ -import assert from "node:assert"; -import fs from "node:fs"; -import path from "node:path"; +import assert from "assert"; +import fs from "fs"; import test from "node:test"; +import path from "path"; import { analyzeExportUsage } from "../../src/core/exports/exportUsage"; import { scanExports } from "../../src/core/exports/scanExports.js"; diff --git a/tests/integrate/barrel.test.ts b/tests/integrate/barrel.test.ts index a5da526..050c35c 100644 --- a/tests/integrate/barrel.test.ts +++ b/tests/integrate/barrel.test.ts @@ -1,5 +1,5 @@ +import assert from "assert/strict"; import fs from "fs"; -import assert from "node:assert/strict"; import test from "node:test"; import path from "path"; diff --git a/tests/integrate/endToEnd.test.ts b/tests/integrate/endToEnd.test.ts index 7c1f660..e7bebda 100644 --- a/tests/integrate/endToEnd.test.ts +++ b/tests/integrate/endToEnd.test.ts @@ -1,6 +1,6 @@ -import assert from "node:assert"; -import path from "node:path"; +import assert from "assert"; import test from "node:test"; +import path from "path"; import { analyzeProject } from "../../src/core/analyzer"; diff --git a/tests/integrate/realistic.test.ts b/tests/integrate/realistic.test.ts index 6f6a886..0686dee 100644 --- a/tests/integrate/realistic.test.ts +++ b/tests/integrate/realistic.test.ts @@ -1,7 +1,7 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; +import assert from "assert/strict"; +import fs from "fs"; import test from "node:test"; +import path from "path"; import { analyzeProject } from "../../src/core/analyzer.js"; diff --git a/tests/internal/basicInternalUsage.test.ts b/tests/internal/basicInternalUsage.test.ts index 40068e2..51866e5 100644 --- a/tests/internal/basicInternalUsage.test.ts +++ b/tests/internal/basicInternalUsage.test.ts @@ -1,7 +1,7 @@ -import assert from "node:assert"; -import fs from "node:fs"; -import path from "node:path"; +import assert from "assert"; +import fs from "fs"; import test from "node:test"; +import path from "path"; import { analyzeLocalUsage } from "../../src/core/locals/analyzeLocalUsage"; diff --git a/tests/internal/strictInternalUsage.test.ts b/tests/internal/strictInternalUsage.test.ts index 96496cd..809c28e 100644 --- a/tests/internal/strictInternalUsage.test.ts +++ b/tests/internal/strictInternalUsage.test.ts @@ -1,7 +1,7 @@ -import assert from "node:assert"; -import fs from "node:fs"; -import path from "node:path"; +import assert from "assert"; +import fs from "fs"; import test from "node:test"; +import path from "path"; import { analyzeLocalUsage } from "../../src/core/locals/analyzeLocalUsage"; diff --git a/tests/utils/fileWalker.test.ts b/tests/utils/fileWalker.test.ts index c3304b8..0503021 100644 --- a/tests/utils/fileWalker.test.ts +++ b/tests/utils/fileWalker.test.ts @@ -1,6 +1,6 @@ -import assert from "node:assert"; -import path from "node:path"; +import assert from "assert"; import test from "node:test"; +import path from "path"; import { walkFiles } from "../../src/core/fileSystem/walkFiles"; diff --git a/tests/utils/pathNormalization.test.ts b/tests/utils/pathNormalization.test.ts index 928941b..58f198e 100644 --- a/tests/utils/pathNormalization.test.ts +++ b/tests/utils/pathNormalization.test.ts @@ -1,6 +1,6 @@ -import assert from "node:assert"; -import path from "node:path"; +import assert from "assert"; import test from "node:test"; +import path from "path"; import { normalizeFilePath } from "../../src/core/fileSystem/normalizePath";