Utilities for TypeScript Repositories.
A comprehensive toolkit for managing TypeScript projects with strict ESM support, providing essential utilities for file validation, code formatting, git operations, and project automation.
npm add --save-dev ts-repo-utils
yarn add --dev ts-repo-utils
pnpm add --save-dev ts-repo-utils
ts-repo-utils provides several CLI commands that can be used directly or through npm scripts.
assert-repo-is-cleanChecks if the repository is clean (i.e., there are no uncommitted changes, untracked files, or staged files) and exits with code 1 if any are present.
# Basic usage
npm exec -- assert-repo-is-clean
# Silent mode
npm exec -- assert-repo-is-clean --silent
# Example in GitHub Actions
- name: Format check
run: npm run fmt
- name: Check if there is no file diff
run: npm exec -- assert-repo-is-clean
Options:
--silent - Suppress output messages (optional)format-uncommittedFormats only untracked/modified files using Prettier.
# Basic usage
npm exec -- format-uncommitted
# Silent mode
npm exec -- format-uncommitted --silent
Options:
--exclude-untracked - Exclude untracked files (default: false)--exclude-modified - Exclude modified files (default: false)--exclude-staged - Exclude staged files (default: false)--silent - Suppress output messages (default: false)--ignore-unknown - Skip files without a Prettier parser instead of erroring (default: true)format-diff-fromFormats only files that differ from the specified base branch or commit.
# Format files different from main branch
npm exec -- format-diff-from main
# Format files different from origin/main
npm exec -- format-diff-from origin/main
# Exclude untracked files
npm exec -- format-diff-from main --exclude-untracked
# Silent mode
npm exec -- format-diff-from main --silent
Example in npm scripts:
{
"scripts": {
"fmt": "npm exec -- format-diff-from origin/main"
}
}
Options:
<base> - Base branch name or commit hash to compare against (required)--exclude-untracked - Exclude untracked files (default: false)--exclude-modified - Exclude modified files (default: false)--exclude-staged - Exclude staged files (default: false)--silent - Suppress output messages (default: false)--ignore-unknown - Skip files without a Prettier parser instead of erroring (default: true)gen-index-tsGenerates index.ts files recursively in target directories with automatic barrel exports.
# Basic usage with required options
npm exec -- gen-index-ts ./src --target-ext .mts --index-ext .mts --export-ext .mjs
# With formatting command
npm exec -- gen-index-ts ./src --target-ext .mts --index-ext .mts --export-ext .mjs --fmt 'npm run fmt'
# Multiple target extensions
npm exec -- gen-index-ts ./src --target-ext .mts --target-ext .tsx --index-ext .mts --export-ext .mjs
# With exclude patterns
npm exec -- gen-index-ts ./src --target-ext .ts --index-ext .ts --export-ext .js --exclude '*.test.ts' --exclude '*.spec.ts'
# Example in npm scripts
"gi": "gen-index-ts ./src --index-ext .mts --export-ext .mjs --target-ext .mts --target-ext .tsx --fmt 'npm run fmt'"
Features:
Benefits:
Options:
<target-directory> - Directory where the index file will be generated (comma-separated list can be used)--target-ext - File extensions to include in the index file (required, can be specified multiple times)--index-ext - Extension of the index file to be generated (required)--export-ext - Extension of the export statements in the index file (required, or 'none')--exclude - Glob patterns of files to exclude (optional, can be specified multiple times)--fmt - Command to format after generating the index file (optional)--silent - Suppress output messages (optional)check-should-run-type-checksChecks whether TypeScript type checks should run based on file changes from the base branch. Optimizes CI/CD pipelines by skipping type checks when only non-TypeScript files have changed. The determination of "non-TypeScript files" is based on configurable ignore patterns, which can be specified using the --paths-ignore option.
# Basic usage (compares against origin/main)
npm exec -- check-should-run-type-checks
# Custom base branch
npm exec -- check-should-run-type-checks --base-branch origin/develop
# Custom ignore patterns
npm exec -- check-should-run-type-checks \
--paths-ignore '.github/' \
--paths-ignore 'docs/' \
--paths-ignore '**.md' \
--paths-ignore '**.yml'
# Example in GitHub Actions
- name: Check if type checks should run
id: check_diff
run: npm exec -- check-should-run-type-checks
- name: Run type checks
if: steps.check_diff.outputs.should_run == 'true'
run: npm run type-check
Options:
--paths-ignore - Patterns to ignore when checking if type checks should run (optional, can be specified multiple times)
.cspell.config.yamldocs/ (matches any file in docs directory)**.md (matches any markdown file)['LICENSE', '.editorconfig', '.gitignore', '.cspell.config.yaml', '.markdownlint-cli2.mjs', '.npmignore', '.prettierignore', '.prettierrc', 'docs/', '**.md', '**.txt']--base-branch - Base branch to compare against for determining changed files (default: origin/main)GitHub Actions Integration:
When running in GitHub Actions, the command sets the GITHUB_OUTPUT environment variable with should_run=true or should_run=false, which can be used in subsequent steps.
$(command: string, options?: ExecOptions): Promise<ExecResult>Executes a shell command asynchronously with type-safe results.
import { $, Result } from 'ts-repo-utils';
// or
// import "ts-repo-utils"; // $ and Result are globally defined in ts-repo-utils
const result = await $('npm test');
if (Result.isOk(result)) {
console.log('Tests passed:', result.value.stdout);
} else {
console.error('Tests failed:', result.value.message);
}
Options:
silent?: boolean - Don't log command/output (default: false)'node:child_process' exec function optionsReturn Type:
import { type ExecException } from 'node:child_process';
type Ret = Promise<
Result<
Readonly<{ stdout: string | Buffer; stderr: string | Buffer }>,
ExecException
>
>;
isDirectlyExecuted(fileUrl: string): booleanDetermines whether a script is being executed directly via CLI or imported as a module. This is useful for creating scripts that can both be imported as libraries and executed directly.
import { isDirectlyExecuted } from 'ts-repo-utils';
// or
// import "ts-repo-utils"; // isDirectlyExecuted is globally defined in ts-repo-utils
// calculator.mjs
export const add = (a: number, b: number): number => a + b;
export const multiply = (a: number, b: number): number => a * b;
// Only run main logic when executed directly: node calculator.mjs (or tsx calculator.mts)
// When imported elsewhere, only the functions are available
if (isDirectlyExecuted(import.meta.url)) {
console.log('Calculator CLI');
console.log('2 + 3 =', add(2, 3));
console.log('4 × 5 =', multiply(4, 5));
}
When executed directly (node calculator.mjs), it runs the main function and prints the results. When imported (import { add } from './calculator.mjs'), it only provides the functions without executing the main logic.
NOTE: If you use tsx or ts-node, run your scripts with the extension .(m)ts instead of .(m)js so that isDirectlyExecuted can correctly determine if the script is executed directly.
Use Cases:
pathExists(filePath: string): Promise<boolean>Checks if a file or directory exists at the specified path.
import { pathExists } from 'ts-repo-utils';
const exists = await pathExists('./src/index.ts');
console.log(exists satisfies boolean); // true or false
assertPathExists(filePath: string, description?: string): Promise<void>Validates that a path exists and exits with code 1 if it doesn't.
import { assertPathExists } from 'ts-repo-utils';
// If the file doesn't exist, this will exit the process with code 1
await assertPathExists('./src/index.ts', 'Entry point file');
checkExt(config: CheckExtConfig): Promise<Result<undefined, Readonly<{ message: string; files: readonly string[] }>>>Runs the extension validation and reports findings without exiting the process. Useful when you want to combine extension checks with other validations or surface the failure information in a custom way.
import { assertExt } from 'ts-repo-utils';
await assertExt({
directories: [
{
path: './src',
extension: '.ts',
ignorePatterns: ['*.d.ts', '*.test.ts'],
},
{
path: './scripts',
extension: '.mjs',
},
],
});
assertExt(config: CheckExtConfig): Promise<void>Validates that all files in specified directories have the correct extensions. Exits with code 1 if any files have incorrect extensions.
type CheckExtConfig = Readonly<{
directories: readonly Readonly<{
path: string; // Directory path to check
extension: string; // Expected file extension (including the dot)
ignorePatterns?: readonly string[]; // Optional glob patterns to ignore
}>[];
}>;
Configuration Type:
import { makeEmptyDir } from 'ts-repo-utils';
// Reset ./tmp/build before writing artifacts
await makeEmptyDir('./tmp/build');
createResultAssert(options): (config) => Promise<TOk>Creates an assert-style wrapper around a function that returns a Result, exiting the process with a non-zero code when the underlying function yields an error. The wrapper keeps success handling customizable while reusing the composable Result-based variant elsewhere.
import { repoIsDirty } from 'ts-repo-utils';
const isDirty = await repoIsDirty();
if (isDirty) {
console.log('Repository has uncommitted changes');
}
Options:
run - Function returning a Result to assert (required)onSuccess - Optional callback invoked with the OK valueonError - Optional callback invoked with the Err value before exitingexitCode - Custom exit code on failure (default: 1)makeEmptyDir(dir: string): Promise<void>Removes any existing directory at dir and recreates it, ensuring a clean target for generated assets or build output.
import { assertRepoIsClean } from 'ts-repo-utils';
// Use in CI/build scripts to ensure clean state
await assertRepoIsClean();
This helper uses fs.rm with recursive cleanup before calling fs.mkdir, so prefer it over manual rimraf + mkdir sequences when scripting workflows.
repoIsDirty(): Promise<boolean>Checks if the repository has uncommitted changes.
import { type ExecException } from 'node:child_process';
type Ret = Result<
readonly string[],
ExecException | Readonly<{ message: string }>
>;
assertRepoIsClean(): Promise<void>Checks if the repository is clean and exits with code 1 if it has uncommitted changes (shows changes and diff).
(Function version of the assert-repo-is-clean command)
import { checkShouldRunTypeChecks } from 'ts-repo-utils';
// Use default settings (compare against origin/main)
const shouldRun = await checkShouldRunTypeChecks();
if (shouldRun) {
await $('npm run type-check');
}
// Custom ignore patterns and base branch
const shouldRun2 = await checkShouldRunTypeChecks({
pathsIgnore: ['.eslintrc.json', 'docs/', '**.md', 'scripts/'],
baseBranch: 'origin/develop',
});
Options:
silent? - Suppress output messages (default: false)getUntrackedFiles(options?)Gets untracked files from the working tree (files not added to git).
Runs git ls-files --others --exclude-standard [--deleted]
getModifiedFiles(options?)Gets modified files from the working tree (files that have been changed but not staged).
Runs git diff --name-only [--diff-filter=d]
getStagedFiles(options?)Gets files that are staged for commit (files added with git add).
Runs git diff --staged --name-only [--diff-filter=d]
getDiffFrom(base: string, options?)Gets files that differ from the specified base branch or commit.
Runs git diff --name-only <base> [--diff-filter=d]
Common options:
excludeDeleted?: boolean - Exclude deleted files (for formatters etc.) (default: true)silent?: boolean - Don't log command/output (default: false)Common Return Type:
import { formatFilesGlob } from 'ts-repo-utils';
// Format all TypeScript files in src
await formatFilesGlob('src/**/*.ts');
// Format specific files
await formatFilesGlob('src/{index,utils}.ts');
// With custom ignore function
await formatFilesGlob('src/**/*.ts', {
ignore: (filePath) => filePath.includes('generated'),
ignoreUnknown: false, // Error on files without parser
});
checkShouldRunTypeChecks(options?): Promise<boolean>Checks whether TypeScript type checks should run based on file changes from the base branch. Optimizes CI/CD pipelines by skipping type checks when only non-TypeScript files have changed.
(Function version of the check-should-run-type-checks command)
import { formatUncommittedFiles } from 'ts-repo-utils';
// Format only modified files
await formatUncommittedFiles();
// With custom options
await formatUncommittedFiles({
untracked: false, // Skip untracked files
ignore: (filePath) => filePath.includes('test'),
});
Options:
pathsIgnore? - Patterns to ignore when checking if type checks should run:
.cspell.config.yamldocs/ (matches any file in docs directory)**.md (matches any markdown file)['LICENSE', '.editorconfig', '.gitignore', '.cspell.config.yaml', '.markdownlint-cli2.mjs', '.npmignore', '.prettierignore', '.prettierrc', 'docs/', '**.md', '**.txt']baseBranch? - Base branch to compare against (default: origin/main)formatFilesGlob(pathGlob: string, options?): Promise<Result<undefined, unknown>>Formats files matching a glob pattern using Prettier.
import { type ExecException } from 'node:child_process';
type Ret = Promise<
Result<
undefined,
ExecException | Readonly<{ message: string }> | readonly unknown[]
>
>;
Options:
silent? - Suppress output messages (default: false)ignoreUnknown? - Skip files without a Prettier parser instead of erroring (default: true)ignore? - Custom function to ignore files (default: built-in ignore list)formatUncommittedFiles(options?): Promise<Result>Formats only files that have been changed according to git status.
(Function version of the format-uncommitted command)
import { formatDiffFrom } from 'ts-repo-utils';
// Format files different from main branch
await formatDiffFrom('main');
// Format files different from specific commit
await formatDiffFrom('abc123');
// With custom options
await formatDiffFrom('main', {
includeUntracked: false,
ignore: (filePath) => filePath.includes('vendor'),
ignoreUnknown: false, // Error on files without parser
});
Options:
untracked? - Format untracked files (default: true)modified? - Format modified files (default: true)staged? - Format staged files (default: true)silent? - Suppress output messages (default: false)ignoreUnknown? - Skip files without a Prettier parser instead of erroring (default: true)ignore? - Custom function to ignore files (default: built-in ignore list)Return Type:
import { type ExecException } from 'node:child_process';
type Ret = Promise<
Result<
undefined,
ExecException | Readonly<{ message: string }> | readonly unknown[]
>
>;
formatDiffFrom(base: string, options?): Promise<Result>Formats only files that differ from the specified base branch or commit.
(Function version of the format-diff-from command)
import { genIndex } from 'ts-repo-utils';
await genIndex({
targetDirectory: './src',
exclude: ['*.test.ts', '*.spec.ts'],
});
Options:
includeUntracked? - Include untracked files in addition to diff files (default: true)includeModified? - Include modified files in addition to diff files (default: true)includeStaged? - Include staged files in addition to diff files (default: true)silent? - Suppress output messages (default: false)ignoreUnknown? - Skip files without a Prettier parser instead of erroring (default: true)ignore? - Custom function to ignore files (default: built-in ignore list)Return Type:
type GenIndexConfig = Readonly<{
/** Target directories to generate index files for (string or array of strings) */
targetDirectory: string | readonly string[];
/**
* Glob patterns for files or predicate function to exclude from exports
* (default: excludes `'**\/*.{test,spec}.?(c|m)[jt]s?(x)'` and
* `'**\/*.d.?(c|m)ts'`)
*/
exclude?:
| readonly string[]
| ((
args: Readonly<{
absolutePath: string;
relativePath: string;
fileName: string;
}>,
) => boolean);
/**
* File extensions of source files to include in exports (default: ['.ts',
* '.tsx'])
*/
targetExtensions?: readonly `.${string}`[];
/** File extension of index files to generate (default: '.ts') */
indexFileExtension?: `.${string}`;
/** File extension to use in export statements (default: '.js') */
exportStatementExtension?: `.${string}` | 'none';
/** Command to run for formatting generated files (optional) */
formatCommand?: string;
/** Whether to suppress output during execution (default: false) */
silent?: boolean;
}>;
genIndex(config: GenIndexConfig): Promise<Result<undefined, unknown>>Generates index files recursively in target directories with automatic barrel exports.
(Function version of the gen-index-ts command)
import { runCmdInStagesAcrossWorkspaces } from 'ts-repo-utils';
// Run build in dependency order
await runCmdInStagesAcrossWorkspaces({
rootPackageJsonDir: '../',
cmd: 'build',
concurrency: 3,
filterWorkspacePattern: (name) => !name.includes('experimental'),
});
Configuration Type:
import { runCmdInParallelAcrossWorkspaces } from 'ts-repo-utils';
// Run tests in parallel across all packages
await runCmdInParallelAcrossWorkspaces({
rootPackageJsonDir: '../',
cmd: 'test',
concurrency: 5,
filterWorkspacePattern: (name) => !name.includes('experimental'),
});
Features:
Benefits:
runCmdInStagesAcrossWorkspaces(options): Promise<void>Executes an npm script command across all workspace packages in dependency order stages. Packages are grouped into stages where each stage contains packages whose dependencies have been completed in previous stages. Uses fail-fast behavior.
import { getWorkspacePackages } from 'ts-repo-utils';
const packages = await getWorkspacePackages('.');
console.log(packages.map((pkg) => pkg.name));
// ['@myorg/package-a', '@myorg/package-b', ...]
Options:
rootPackageJsonDir - Directory containing the root package.json filecmd - The npm script command to execute in each packageconcurrency? - Maximum packages to process simultaneously within each stage (default: 3)filterWorkspacePattern? - Optional function to filter packages by namerunCmdInParallelAcrossWorkspaces(options): Promise<void>Executes an npm script command across all workspace packages in parallel. Uses fail-fast behavior - stops execution immediately when any package fails.
type Package = Readonly<{
name: string;
path: string;
packageJson: JsonValue;
dependencies: Readonly<Record<string, string>>;
}>;
Options:
rootPackageJsonDir - Directory containing the root package.json filecmd - The npm script command to execute in each packageconcurrency? - Maximum packages to process simultaneously (default: 3)filterWorkspacePattern? - Optional function to filter packages by namegetWorkspacePackages(rootPackageJsonDir): Promise<readonly Package[]>Retrieves all workspace packages from a monorepo based on the workspace patterns defined in the root package.json file.
import { executeParallel, getWorkspacePackages } from 'ts-repo-utils';
const packages = await getWorkspacePackages('.');
await executeParallel(packages, 'lint', 4);
Return Type:
import { executeStages, getWorkspacePackages } from 'ts-repo-utils';
const packages = await getWorkspacePackages('.');
await executeStages(packages, 'build', 3);
executeParallel(packages, scriptName, concurrency?): Promise<readonly Result[]>Executes an npm script across multiple packages in parallel with a concurrency limit. Lower-level function used by runCmdInParallelAcrossWorkspaces.
import 'ts-repo-utils';
// Now these functions are globally available
const result = await $('npm test');
if (Result.isErr(result)) {
console.error(result.value);
}
echo('Building project...');
const filePath: string = path.join('src', 'index.ts');
const configJson: string = await fs.readFile('./config.json', {
encoding: 'utf8',
});
const home = os.homedir();
const files: readonly string[] = await glob('**/*.ts');
if (isDirectlyExecuted(import.meta.url)) {
echo('Running as CLI');
}
executeStages(packages, scriptName, concurrency?): Promise<void>Executes an npm script across packages in dependency order stages. Lower-level function used by runCmdInStagesAcrossWorkspaces.
import {
assertExt,
assertRepoIsClean,
formatUncommittedFiles,
} from 'ts-repo-utils';
// Validate file extensions
await assertExt({
directories: [{ path: './src', extension: '.ts' }],
});
// Format changed files
await formatUncommittedFiles();
// Ensure repository is clean (exits if dirty)
await assertRepoIsClean();
Features:
When you import ts-repo-utils without destructuring, several utilities become globally available. This is useful for scripts where you want quick access to common functions without explicit imports.
import { formatFilesGlob, genIndex } from 'ts-repo-utils';
// Generate barrel exports
await genIndex({ targetDirectory: './src' });
// Type check
await $('tsc --noEmit');
// Build
await $('rollup -c');
// Format output
await formatFilesGlob('dist/**/*.js');
$ - The command execution utility described above.Result - A utility for Result pattern (from ts-data-forge)echo - Equivalent to console.logcd - Equivalent to process.chdirpath - node:pathfs - node:fs/promisesos - node:osglob - fast-globisDirectlyExecuted - The script execution utility described above.import { assertExt, assertPathExists, assertRepoIsClean } from 'ts-repo-utils';
// Check required files exist (exits with code 1 if files don't exist)
await assertPathExists('./package.json', 'Package manifest');
await assertPathExists('./tsconfig.json', 'TypeScript config');
// Validate extensions
await assertExt({
directories: [
{ path: './src', extension: '.ts' },
{ path: './scripts', extension: '.mjs' },
],
});
// Verify clean repository state (exits with code 1 if repo is dirty)
await assertRepoIsClean();
import { formatFilesGlob, genIndex } from 'ts-repo-utils';
// Generate barrel exports
await genIndex({ targetDirectory: './src' });
// Type check
await $('tsc --noEmit');
// Build
await $('rollup -c');
// Format output
await formatFilesGlob('dist/**/*.js');
import { assertExt, assertPathExists, assertRepoIsClean } from 'ts-repo-utils';
// Check required files exist (exits with code 1 if files don't exist)
await assertPathExists('./package.json', 'Package manifest');
await assertPathExists('./tsconfig.json', 'TypeScript config');
// Validate extensions
await assertExt({
directories: [
{ path: './src', extension: '.ts' },
{ path: './scripts', extension: '.mjs' },
],
});
// Verify clean repository state (exits with code 1 if repo is dirty)
await assertRepoIsClean();
Apache-2.0