Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions scripts/verify-tarball.js
Original file line number Diff line number Diff line change
@@ -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}`);
Expand Down
1 change: 1 addition & 0 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ${colors.bold}Commands:${colors.reset}
internal <file> Run intra-file identifier analysis only.

${colors.bold}General Options:${colors.reset}
--cwd <path> 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
Expand Down
67 changes: 63 additions & 4 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file.ts> [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.");
Expand Down Expand Up @@ -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.");
Expand Down
5 changes: 3 additions & 2 deletions src/cli/internalMode.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -19,7 +20,7 @@ export async function runInternalMode(filePath: string, args: any): Promise<void

// Read file
try {
sourceText = fs.readFileSync(filePath, "utf8");
sourceText = readFileSync(filePath, "utf8");
} catch (error) {
if (!SILENT_MODE) {
log(`${colors.red}Error: Unable to read file: ${filePath}${colors.reset}`);
Expand Down
8 changes: 4 additions & 4 deletions tests/cli/cli.basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// tests/cli.basic.test.ts

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import { resolve } from "node:path";
import assert from "assert/strict";
import { spawnSync } from "child_process";
import fs from "fs";
import { test } from "node:test";
import { resolve } from "path";

const CLI_PATH = resolve("bin", "devoid.js");
const PKG_PATH = resolve("package.json");
Expand Down
83 changes: 83 additions & 0 deletions tests/cli/cli.cwd.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import assert from "assert/strict";
import fs from "fs";
import test from "node:test";
import os from "os";
import path from "path";

import { runCLI } from "./utils/runCLI";

function writeFile(filePath: string, contents: string) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, contents, "utf8");
}

test("--cwd: resolves project root 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, "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/);
});
17 changes: 17 additions & 0 deletions tests/cli/utils/runCLI.ts
Original file line number Diff line number Diff line change
@@ -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 ?? ""),
};
}
2 changes: 1 addition & 1 deletion tests/exports/conflictingExports.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
6 changes: 3 additions & 3 deletions tests/exports/exportChains.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion tests/exports/exportGraphResolver.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
6 changes: 3 additions & 3 deletions tests/exports/exportUsage.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion tests/exports/localPriority.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
2 changes: 1 addition & 1 deletion tests/exports/multiHop.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
2 changes: 1 addition & 1 deletion tests/exports/namedReexports.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
2 changes: 1 addition & 1 deletion tests/exports/unresolvedReexports.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
6 changes: 3 additions & 3 deletions tests/exports/wildcardExportChains.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion tests/exports/wildcardReexport.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
6 changes: 3 additions & 3 deletions tests/imports/importGraph.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion tests/imports/mixedImports.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
2 changes: 1 addition & 1 deletion tests/imports/typeOnlyImports.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
6 changes: 3 additions & 3 deletions tests/imports/unresolvedImports.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion tests/integrate/barrel.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
4 changes: 2 additions & 2 deletions tests/integrate/endToEnd.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
6 changes: 3 additions & 3 deletions tests/integrate/realistic.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
Loading