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
5 changes: 5 additions & 0 deletions .changeset/busy-bobcats-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vlandoss/run-run": patch
---

Add concurrency to tsc
10 changes: 2 additions & 8 deletions packages/clibuddy/src/services/shell/create.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { ShellService } from "./shell";
import type { CreateOptions } from "./types";
import { getPreferLocal } from "./utils";

export const cwd = fs.realpathSync(process.cwd());

Expand All @@ -25,8 +25,6 @@ export function quote(arg: string) {
export const isRaw = (arg: unknown): arg is { stdout: string } =>
typeof arg === "object" && arg !== null && "stdout" in arg && typeof arg.stdout === "string";

const getLocalBinPath = (dirPath: string) => path.join(dirPath, "node_modules", ".bin");

function defaultQuote(arg: unknown) {
if (typeof arg === "string") {
return quote(arg);
Expand All @@ -40,11 +38,7 @@ function defaultQuote(arg: unknown) {
}

export function createShellService(options: CreateOptions = {}) {
const preferLocal = !options.localBaseBinPath
? undefined
: Array.isArray(options.localBaseBinPath)
? options.localBaseBinPath.map(getLocalBinPath)
: [options.localBaseBinPath].map(getLocalBinPath);
const preferLocal = getPreferLocal(options.localBaseBinPath);

return new ShellService({
verbose: true,
Expand Down
31 changes: 24 additions & 7 deletions packages/clibuddy/src/services/shell/shell.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { $ as make$ } from "zx";
import type { Shell, ShellOptions } from "./types";
import { getPreferLocal } from "./utils";

export class ShellService {
#shell: Shell;
Expand All @@ -18,6 +19,13 @@ export class ShellService {
return this.#shell;
}

child(options: ShellOptions) {
return new ShellService({
...this.#options,
...options,
});
}

quiet(options?: ShellOptions) {
return this.child({
...options,
Expand All @@ -26,16 +34,25 @@ export class ShellService {
}

at(cwd: string, options?: ShellOptions) {
const getLocals = (locals: boolean | string | string[] | undefined) =>
// NOTE: the boolean handling is done outside when determining preferLocal
typeof locals === "boolean" ? [] : typeof locals === "undefined" ? [] : Array.isArray(locals) ? locals : [locals];

const cwdPreferLocal = getPreferLocal(cwd);

const preferLocal =
options?.preferLocal === false
? false
: [
...getLocals(this.#options.preferLocal),
...getLocals(options?.preferLocal),
...(cwdPreferLocal ? cwdPreferLocal : []),
];

return this.child({
...options,
cwd,
});
}

child(options: ShellOptions) {
return new ShellService({
...this.#options,
...options,
preferLocal,
});
}
}
11 changes: 11 additions & 0 deletions packages/clibuddy/src/services/shell/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import path from "node:path";
import { ProcessOutput } from "zx";

export function isProcessOutput(value: unknown): value is ProcessOutput {
return value instanceof ProcessOutput;
}

const getLocalBinPath = (dirPath: string) => path.join(dirPath, "node_modules", ".bin");

export function getPreferLocal(localBaseBinPath: string | Array<string> | undefined) {
return !localBaseBinPath
? undefined
: Array.isArray(localBaseBinPath)
? localBaseBinPath.map(getLocalBinPath)
: [localBaseBinPath].map(getLocalBinPath);
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ exports[`should match help messages for all commands: help-command-run 1`] = `
"Usage: rr run [options] <cmds...>

Arguments:
cmds commands to execute in sequence (e.g. 'check tsc')
cmds commands to execute concurrently (e.g. 'check tsc')

Options:
-h, --help display help for command
Expand Down
6 changes: 2 additions & 4 deletions packages/run-run/src/program/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import { createCommand } from "commander";
import type { Context } from "#/services/ctx";

export function createRunCommand(ctx: Context) {
const program = createCommand("run")
.argument("<cmds...>", "commands to execute in sequence (e.g. 'check tsc')")
return createCommand("run")
.argument("<cmds...>", "commands to execute concurrently (e.g. 'check tsc')")
.action(async function runRunAction(cmds: string[]) {
const { $ } = ctx.shell;
const commands = cmds.map((cmd) => $`rr ${cmd}`);
await Promise.all(commands);
});

return program;
}
87 changes: 50 additions & 37 deletions packages/run-run/src/program/commands/typecheck.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,81 @@
import type { Project } from "@vlandoss/clibuddy";
import { cwd } from "@vlandoss/clibuddy";
import type { AnyLogger } from "@vlandoss/loggy";
import { createCommand } from "commander";
import type { Context } from "#/services/ctx";
import { logger } from "#/services/logger";

type TypecheckAtOptions = {
dir: string;
scripts: Record<string, string | undefined> | undefined;
log: AnyLogger;
};

export function createTypecheckCommand(ctx: Context) {
return createCommand("tsc")
.alias("typecheck")
.description("check if TypeScript code is well typed 🎨")
.action(async function typecheckAction() {
const { appPkg, shell } = ctx;

async function singleTypecheck(dir?: string, options?: { logger?: typeof logger }): Promise<boolean | undefined> {
const log = options?.logger ?? logger;

if (!appPkg.hasFile("tsconfig.json", dir)) {
log.info("No tsconfig.json found, skipping typecheck");
return;
}

if (dir) {
await shell.at(dir).$`tsc --noEmit`;
} else {
await shell.$`tsc --noEmit`;
}
const isTsProject = (dir: string) => appPkg.hasFile("tsconfig.json", dir);

return true;
}
const getPreScript = (scripts: Record<string, string | undefined> | undefined) => scripts?.pretsc ?? scripts?.pretypecheck;

async function typecheckAtProject(project: Project) {
const childLogger = logger.child({
tag: project.manifest.name,
namespace: "typecheck",
});
async function typecheckAt({ dir, scripts, log }: TypecheckAtOptions) {
const shellAt = cwd === dir ? shell : shell.at(dir);

try {
childLogger.start("Type checking started");

const success = await singleTypecheck(project.rootDir, {
logger: childLogger,
});

if (success) {
childLogger.success("Typecheck completed");
const preScript = getPreScript(scripts);
if (preScript) {
log.start(`Running pre-script: ${preScript}`);
await shellAt.$`${preScript}`;
log.success("Pre-script completed");
}

log.start("Type checking started");
await shellAt.$`tsc --noEmit`;
log.success("Typecheck completed");
} catch (error) {
childLogger.error("Typecheck failed");
log.error("Typecheck failed");
throw error;
}
}

if (!appPkg.isMonorepo()) {
try {
await singleTypecheck();
} catch (error) {
logger.error("Typecheck failed");
throw error;
if (!isTsProject(appPkg.dirPath)) {
logger.info("No tsconfig.json found, skipping typecheck");
return;
}

await typecheckAt({
dir: appPkg.dirPath,
scripts: appPkg.packageJson.scripts,
log: logger,
});

return;
}

const projects = await appPkg.getWorkspaceProjects();
const tsProjects = projects.filter((project) => isTsProject(project.rootDir));

for (const project of projects) {
await typecheckAtProject(project);
if (!tsProjects.length) {
logger.warn("No TypeScript projects found in the monorepo, skipping typecheck");
return;
}

await Promise.all(
tsProjects.map((p) =>
typecheckAt({
dir: p.rootDir,
scripts: p.manifest.scripts,
log: logger.child({
tag: p.manifest.name,
namespace: "typecheck",
}),
}),
),
);
})
.addHelpText("afterAll", "\nUnder the hood, this command uses the TypeScript CLI to check the code.");
}