Ctrl K
ComunidadGuías…

scripts/typecheck.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bun

/**
 * Type-check the repo with the TypeScript 7 native compiler.
 *
 *   bun scripts/typecheck.ts                   -> check every project the root solution references
 *   bun scripts/typecheck.ts tsconfig.bun.json -> check just the named project
 *
 * The project list comes from the `references` in the root `tsconfig.json`, so a project joins CI by
 * being referenced there and nowhere else.
 *
 * Every project is checked even after one fails, so a single run reports every error in the repo.
 * Projects are checked concurrently, which is why this fans out `tsc --build` per project instead of
 * running `tsc --build tsconfig.json` once: build mode walks a solution sequentially.
 */
import {$} from 'bun';
import CLI from 'expensify-common/CLI';
import {readdirSync} from 'fs';
import path from 'path';

const projectRoot = `${import.meta.dir}/..`;

// typescript-eslint still needs the TypeScript 6 compiler API, so it requires `@typescript/typescript6`
// (see the patches under patches/@typescript-eslint/). The root `typescript` package is TypeScript 7.
// Invoke that bin by path so a leftover `.bin/tsc` from `@typescript/old` cannot win.
const tsc = `${projectRoot}/node_modules/typescript/bin/tsc`;

function isRecord(value: unknown): value is Record<string, unknown> {
    return typeof value === 'object' && value !== null;
}

/** `Array.isArray` narrows to `any[]`, which loses the `unknown` we are trying to validate. */
function isUnknownArray(value: unknown): value is unknown[] {
    return Array.isArray(value);
}

/**
 * Pull the `references[].path` list out of a `tsc --showConfig` payload.
 *
 * Every entry is validated rather than filtered: silently dropping a malformed reference would
 * silently drop a project out of the typecheck gate.
 */
function readReferencePaths(config: unknown): string[] {
    if (!isRecord(config) || !isUnknownArray(config.references)) {
        throw new Error('tsconfig.json has no `references` array, so there is nothing to type-check.');
    }
    return config.references.map((reference, index) => {
        if (!isRecord(reference) || typeof reference.path !== 'string' || reference.path === '') {
            throw new Error(`tsconfig.json is malformed at references[${index}]: every reference needs a non-empty string "path".`);
        }
        return reference.path;
    });
}

/**
 * The projects the root solution references, relative to the repo root.
 *
 * `--showConfig` is how we read them: the root config carries comments, so it is not plain JSON, and
 * TypeScript 7 does not yet expose a compiler API we could parse it with.
 */
async function getReferencedProjects(): Promise<string[]> {
    const result = await $`${tsc} -p tsconfig.json --showConfig`.cwd(projectRoot).quiet().nothrow();
    if (result.exitCode !== 0) {
        throw new Error(`Could not read the projects referenced by tsconfig.json:\n${result.stderr.toString()}`);
    }
    const references = readReferencePaths(JSON.parse(result.stdout.toString()));
    if (references.length === 0) {
        throw new Error('tsconfig.json references no projects, so there is nothing to type-check.');
    }
    assertEveryRootProjectIsReferenced(references);
    return references;
}

/**
 * Fail loudly when a root-level project is missing from `tsconfig.json`'s `references`.
 *
 * `tsc --showConfig` drops a malformed reference (say, a misspelled `path` key) without reporting
 * anything, which would quietly take that project out of the typecheck gate. This turns both that
 * and "added a project but forgot to reference it" into an error.
 */
function assertEveryRootProjectIsReferenced(references: string[]) {
    const referenced = new Set(references.map((reference) => path.basename(reference)));
    const unreferenced = readdirSync(projectRoot)
        // `tsconfig.base.json` holds shared options rather than files, so it is not a project.
        .filter((entry) => /^tsconfig\..+\.json$/.test(entry) && entry !== 'tsconfig.base.json')
        .filter((entry) => !referenced.has(entry));
    if (unreferenced.length > 0) {
        throw new Error(`These projects are not referenced by tsconfig.json, so nothing type-checks them: ${unreferenced.join(', ')}`);
    }
}

/** Type-check one project. Build mode skips it entirely when its build info file is already current. */
async function typecheckProject(project: string) {
    const result = await $`${tsc} --build ${project}`.cwd(projectRoot).quiet().nothrow();
    return {project, result};
}

const cli = new CLI({
    positionalArgs: [
        {
            name: 'projects',
            description: 'tsconfig paths to type-check, relative to the repo root',
            variadic: true,
            default: [],
        },
    ],
});

const projects = cli.positionalArgs.projects.length > 0 ? cli.positionalArgs.projects : await getReferencedProjects();

// All projects are checked concurrently. Projects share source files but each writes its own build
// info file (`tsBuildInfoFile`), so there is nothing for concurrent builds to race over.
// `--noEmit` lives in the configs because build mode rejects it as a flag.
const results = await Promise.all(projects.map(typecheckProject));

const failed: string[] = [];
for (const {project, result} of results) {
    console.log(`\nType checking ${project}...`);
    const output = `${result.stdout.toString()}${result.stderr.toString()}`.trim();
    if (output) {
        console.log(output);
    }
    if (result.exitCode !== 0) {
        failed.push(project);
    }
}

if (failed.length > 0) {
    console.error(`\nType checking failed for: ${failed.join(', ')}`);
    process.exit(1);
}

console.log('\nType checking passed.');