Skip to content
Open
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
11 changes: 8 additions & 3 deletions packages/tooling/vis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,14 @@ vis affected test --base=main
# Visualize the project dependency graph
vis graph

# Check for outdated catalog dependencies
# Lint, format, and type-check the workspace
vis check

# Check for outdated catalog dependencies
vis outdated

# Check with security vulnerability scanning
vis check --security
vis outdated --security

# Update catalog dependencies interactively, plus any
# GitHub Actions, Dockerfile, docker-compose, or GitLab CI
Expand Down Expand Up @@ -222,7 +225,8 @@ When aube is the installer, `vis doctor` surfaces aube's effective hardening pos
| `vis ci <targets>` | | One-shot CI entry point: toolchain, install, then affected runs for each comma-separated target |
| `vis ignore <project>` | | CI build gating for Vercel / Netlify "Ignored Build Step" |
| `vis graph` | | Visualize the project dependency graph |
| `vis check [packages]` | `c` | Check for outdated dependencies in workspace catalogs |
| `vis check [files]` | | Lint, check formatting, and type-check the workspace (`--fix` applies changes) |
| `vis outdated [packages]` | | Check for outdated dependencies in workspace catalogs |
| `vis update [packages]` | `up` | Update packages, GitHub Actions, Dockerfiles, and GitLab CI refs |
| `vis install` | `i` | Install dependencies via the detected package manager |
| `vis info <package>` | `view` | Show npm registry metadata for a package (wraps `npm view` et al.) |
Expand All @@ -235,6 +239,7 @@ When aube is the installer, `vis doctor` surfaces aube's effective hardening pos
| `vis toolchain <cmd>` | | Inspect / delegate to the version manager (proto, mise, fnm, volta…) |
| `vis lint [files]` | | Orchestrate detected source-code linters (oxlint, biome, eslint, stylelint) |
| `vis fmt [files]` | | Orchestrate detected formatters (oxfmt, biome, dprint, prettier) |
| `vis test [files]` | | Run the package test script, or Vitest / Rstest (asks which to install when neither exists) |
| `vis staged` | | Run tasks on staged files (built-in `lint-staged` replacement) |
| `vis migrate <type>` | | Migrate from other tools — now including `gitleaks` and `secretlint` |

Expand Down
100 changes: 100 additions & 0 deletions packages/tooling/vis/__tests__/commands/check/check-gate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const lint = vi.hoisted(() => vi.fn());
const fmt = vi.hoisted(() => vi.fn());

vi.mock(import("../../../src/commands/lint/handler"), () => { return { default: lint }; });
vi.mock(import("../../../src/commands/fmt/handler"), () => { return { default: fmt }; });

const { default: checkExecute } = await import("../../../src/commands/check/handler");

let root: string;

const toolbox = (options: Record<string, unknown>, extra: Record<string, unknown> = {}) =>
({
argument: [],
fs: { access },
logger: { info: vi.fn(), warn: vi.fn() },
options: { fix: false, since: undefined, staged: false, ...options },
visConfig: undefined,
workspaceRoot: root,
...extra,
}) as never;

const writeTypeScriptWorkspace = async (): Promise<void> => {
await writeFile(join(root, "package.json"), JSON.stringify({ devDependencies: { typescript: "^5.0.0" } }));
await writeFile(join(root, "tsconfig.json"), "{}");
};

describe("vis check", () => {
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "vis-check-gate-"));
lint.mockReset();
fmt.mockReset();
});

afterEach(async () => {
await rm(root, { force: true, recursive: true });
});

it("enables the type check when TypeScript is detected", async () => {
expect.assertions(1);

await writeTypeScriptWorkspace();
await checkExecute(toolbox({}));

expect(lint.mock.calls[0][0].visConfig.lint.adapters.typecheck.enabled).toBe(true);
});

it("leaves the type check off when TypeScript is not detected", async () => {
expect.assertions(1);

await checkExecute(toolbox({}));

expect(lint.mock.calls[0][0].visConfig.lint.adapters.typecheck.enabled).toBe(false);
});

it("respects an explicit typecheck opt-out in config", async () => {
expect.assertions(1);

await writeTypeScriptWorkspace();
await checkExecute(toolbox({}, { visConfig: { lint: { adapters: { typecheck: { enabled: false } } } } }));

expect(lint.mock.calls[0][0].visConfig.lint.adapters.typecheck.enabled).toBe(false);
});

it("runs fmt in check mode by default and in fix mode with --fix", async () => {
expect.assertions(3);

await checkExecute(toolbox({}));
await checkExecute(toolbox({ fix: true }));

expect(fmt.mock.calls[0][0].options.check).toBe(true);
expect(fmt.mock.calls[1][0].options.check).toBe(false);
expect(lint.mock.calls[1][0].options.fix).toBe(true);
});

it("forwards file arguments and git scope to both tools", async () => {
expect.assertions(2);

await mkdir(join(root, "src"));
await writeFile(join(root, "src", "foo.ts"), "");
await checkExecute(toolbox({ staged: true }, { argument: ["src/foo.ts"] }));

expect(lint.mock.calls[0][0].options).toMatchObject({ _: ["src/foo.ts"], staged: true });
expect(fmt.mock.calls[0][0].options).toMatchObject({ _: ["src/foo.ts"], staged: true });
});

it("rejects a dependency name passed as a path, pointing at vis outdated", async () => {
expect.assertions(2);

const run = checkExecute(toolbox({}, { argument: ["react"] }));

await expect(run).rejects.toThrow("Dependency checks moved to `vis outdated`.");
expect(lint).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import linkCommand from "../../src/commands/link";
import listCommand from "../../src/commands/list";
import migrateCommands from "../../src/commands/migrate";
import optimizeCommand from "../../src/commands/optimize";
import outdatedCommand from "../../src/commands/outdated";
import pmCommand from "../../src/commands/pm";
import removeCommand from "../../src/commands/remove";
import runCommand from "../../src/commands/run";
Expand Down Expand Up @@ -74,6 +75,7 @@ const FLAT_COMMANDS: Command[] = [
linkCommand,
listCommand,
optimizeCommand,
outdatedCommand,
pmCommand,
removeCommand,
runCommand,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";

import checkExecute from "../../../src/commands/check/handler";
import outdatedExecute from "../../../src/commands/outdated/handler";

const SYNC_WITHOUT_CONFIG_PATTERN = /--sync requires --security-config/;

Expand All @@ -13,18 +13,18 @@ const toolbox = (options: Record<string, unknown>) =>
workspaceRoot: "/tmp/vis-check-guard",
}) as never;

describe("vis check --sync", () => {
describe("vis outdated --sync", () => {
it("rejects --sync when --security-config is absent", async () => {
expect.assertions(1);

// `--sync` only acts inside the security-config branch, so accepting it
// alone is a command that reports success without doing the work.
await expect(checkExecute(toolbox({ sync: true }))).rejects.toThrow(SYNC_WITHOUT_CONFIG_PATTERN);
await expect(outdatedExecute(toolbox({ sync: true }))).rejects.toThrow(SYNC_WITHOUT_CONFIG_PATTERN);
});

it("does not reject --security-config on its own", async () => {
expect.assertions(1);

await expect(checkExecute(toolbox({ securityConfig: true }))).rejects.not.toThrow(SYNC_WITHOUT_CONFIG_PATTERN);
await expect(outdatedExecute(toolbox({ securityConfig: true }))).rejects.not.toThrow(SYNC_WITHOUT_CONFIG_PATTERN);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { afterEach, beforeEach, describe, expect, it } from "vitest";

import { readTestScript } from "../../../src/commands/test/handler";

const readText = (path: string) => readFile(path, "utf8").catch(() => undefined);

describe(readTestScript, () => {
let root: string;

beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "vis-test-script-"));
});

afterEach(async () => {
await rm(root, { force: true, recursive: true });
});

it("returns the package's test script", async () => {
expect.assertions(1);

await writeFile(join(root, "package.json"), JSON.stringify({ scripts: { test: "vitest run" } }));

await expect(readTestScript(root, readText)).resolves.toBe("vitest run");
});

it("ignores npm's init placeholder", async () => {
expect.assertions(1);

await writeFile(join(root, "package.json"), JSON.stringify({ scripts: { test: "echo \"Error: no test specified\" && exit 1" } }));

await expect(readTestScript(root, readText)).resolves.toBeUndefined();
});

it("returns undefined when there is no package.json or no test script", async () => {
expect.assertions(2);

await expect(readTestScript(root, readText)).resolves.toBeUndefined();

await writeFile(join(root, "package.json"), JSON.stringify({ scripts: { build: "packem build" } }));

await expect(readTestScript(root, readText)).resolves.toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { afterEach, beforeEach, describe, expect, it } from "vitest";

import { resolveRstestEntry } from "../../../src/commands/test/handler";

const readText = (path: string) => readFile(path, "utf8").catch(() => undefined);

describe(resolveRstestEntry, () => {
let root: string;

beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "vis-test-rstest-"));
});

afterEach(async () => {
await rm(root, { force: true, recursive: true });
});

it("returns the rstest bin from an @rstest/core install found in an ancestor", async () => {
expect.assertions(1);

const coreDirectory = join(root, "node_modules", "@rstest", "core");
const packageDirectory = join(root, "packages", "app");

await mkdir(coreDirectory, { recursive: true });
await mkdir(packageDirectory, { recursive: true });
await writeFile(join(coreDirectory, "package.json"), JSON.stringify({ bin: { rstest: "./bin/rstest.js" } }));

await expect(resolveRstestEntry(packageDirectory, readText)).resolves.toBe(join(coreDirectory, "bin", "rstest.js"));
});

it("returns undefined when no ancestor has @rstest/core", async () => {
expect.assertions(1);

const packageDirectory = join(root, "app");

await mkdir(packageDirectory, { recursive: true });

await expect(resolveRstestEntry(packageDirectory, readText)).resolves.toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { afterEach, beforeEach, describe, expect, it } from "vitest";

import { resolveVitestEntry } from "../../../src/commands/test/handler";

const readText = (path: string) => readFile(path, "utf8").catch(() => undefined);

describe(resolveVitestEntry, () => {
let root: string;

beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "vis-test-resolve-"));
});

afterEach(async () => {
await rm(root, { force: true, recursive: true });
});

it("returns the bin entry from a hoisted vitest install found in an ancestor", async () => {
expect.hasAssertions();

const vitestDirectory = join(root, "node_modules", "vitest");
const packageDirectory = join(root, "packages", "app", "src");

await mkdir(vitestDirectory, { recursive: true });
await mkdir(packageDirectory, { recursive: true });
await writeFile(join(vitestDirectory, "package.json"), JSON.stringify({ bin: { vitest: "./vitest.mjs" } }));

await expect(resolveVitestEntry(packageDirectory, readText)).resolves.toBe(join(vitestDirectory, "vitest.mjs"));
});

it("returns undefined when no ancestor has vitest installed", async () => {
expect.hasAssertions();

const packageDirectory = join(root, "app");

await mkdir(packageDirectory, { recursive: true });

// The temp directory lives under the OS tmpdir, which should not contain a
// vitest install in any ancestor; this guards against a false positive from
// the walk stopping early.
await expect(resolveVitestEntry(packageDirectory, readText)).resolves.toBeUndefined();
});
});
Loading
Loading