ts-repo-utils
    Preparing search index...

    ts-repo-utils

    npm version

    npm downloads License codecov

    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.

    Checks 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)

    Formats 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)

    Formats 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)

    Generates 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:

    • Creates barrel exports for all subdirectories
    • Supports complex glob exclusion patterns (using micromatch)
    • Automatically formats generated files using the project's Prettier config
    • Works with both single directories and directory arrays
    • Respects source and export extension configuration

    Benefits:

    • Prevents forgetting to export modules
    • TypeScript can detect duplicate variables, type names, etc.

    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)

    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. 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)
      • Supports exact file matches: .cspell.config.yaml
      • Directory prefixes: docs/ (matches any file in docs directory)
      • File extensions: **.md (matches any markdown file)
      • Default: ['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.

    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 options

    Return Type:

    import { type ExecException } from 'node:child_process';

    type Ret = Promise<
    Result<
    Readonly<{ stdout: string | Buffer; stderr: string | Buffer }>,
    ExecException
    >
    >;

    Determines 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:

    • Creating CLI tools that can also be used as libraries
    • Preventing automatic execution when a file is imported
    • Running initialization code only during direct execution

    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

    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');

    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',
    },
    ],
    });

    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');

    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 value
    • onError - Optional callback invoked with the Err value before exiting
    • exitCode - Custom exit code on failure (default: 1)

    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.

    Checks if the repository has uncommitted changes.

    import { type ExecException } from 'node:child_process';

    type Ret = Result<
    readonly string[],
    ExecException | Readonly<{ message: string }>
    >;

    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)

    Gets untracked files from the working tree (files not added to git).
    Runs git ls-files --others --exclude-standard [--deleted]

    Gets modified files from the working tree (files that have been changed but not staged).
    Runs git diff --name-only [--diff-filter=d]

    Gets files that are staged for commit (files added with git add).
    Runs git diff --staged --name-only [--diff-filter=d]

    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
    });

    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:
      • Exact file matches: .cspell.config.yaml
      • Directory prefixes: docs/ (matches any file in docs directory)
      • File extensions: **.md (matches any markdown file)
      • Default: ['LICENSE', '.editorconfig', '.gitignore', '.cspell.config.yaml', '.markdownlint-cli2.mjs', '.npmignore', '.prettierignore', '.prettierrc', 'docs/', '**.md', '**.txt']
    • baseBranch? - Base branch to compare against (default: origin/main)

    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)

    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[]
    >
    >;

    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;
    }>;

    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:

    • Creates barrel exports for all subdirectories
    • Supports complex glob exclusion patterns (using micromatch)
    • Automatically formats generated files using the project's Prettier config
    • Works with both single directories and directory arrays
    • Respects source and export extension configuration

    Benefits:

    • Prevents forgetting to export modules
    • TypeScript can detect duplicate variables, type names, etc.

    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 file
    • cmd - The npm script command to execute in each package
    • concurrency? - Maximum packages to process simultaneously within each stage (default: 3)
    • filterWorkspacePattern? - Optional function to filter packages by name

    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 file
    • cmd - The npm script command to execute in each package
    • concurrency? - Maximum packages to process simultaneously (default: 3)
    • filterWorkspacePattern? - Optional function to filter packages by name

    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);

    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');
    }

    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:

    • Automatic dependency graph construction
    • Topological sorting for correct build order
    • Parallel execution within each stage
    • Fail-fast behavior on errors
    • Circular dependency detection

    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.log
    • cd - Equivalent to process.chdir
    • path - node:path
    • fs - node:fs/promises
    • os - node:os
    • glob - fast-glob
    • isDirectlyExecuted - 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