core/analyzers/export.analyzer.ts

core/analyzers/export.analyzer.ts is a file in Project Scripts. 86 lines of code and 19 definitions.

import type { ExportEntry, ImportEntry } from "#types/closure.types";
import { MANIFEST_NAME, TEST_MARK } from "#configuration/constants/closure.constants";
import { ROOT, absolutePath } from "@ssot/paths";
import { RUNNER_MODULES, WHOLE } from "@banes-lab/build-scripts/configuration/constants/loader.constants.ts";
import { existsSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { stringAt, stringRecordAt } from "#core/selectors/manifest.selector";
import { BUILD_CONFIG_FILES } from "@ssot/govlab/shared/resolvers/anchor.resolver.ts";
import { MEMBER_ROOTS } from "@ssot/govlab/shared/registries/location.registry.ts";
import { anchorSpecifier } from "#core/resolvers/specifier.resolver";
import { manifestOf } from "#core/loaders/manifest.loader";
import { sourceFilesUnder } from "#core/loaders/source.loader";
import ts from "typescript";

const PROJECT_ANCHOR = ".";

const namedImportsOf = function namedImportsOf(clause: ts.ImportClause): string[] {
    const bindings = clause.namedBindings;
    const named = bindings && ts.isNamedImports(bindings) ? bindings.elements : [];
    return [
        ...named.map((element) => element.propertyName?.text ?? element.name.text),
        ...(clause.name ? [clause.name.text] : []),
    ];
};

const importOf = function importOf(
    root: string,
    manifest: unknown,
    filePath: string,
    statement: ts.Statement,
): ImportEntry | null {
    if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
        return null;
    }
    const names = statement.importClause ? namedImportsOf(statement.importClause) : [];
    if (names.length === 0) {
        return null;
    }
    const from = anchorSpecifier(root, manifest, filePath, statement.moduleSpecifier.text);
    return { file: PROJECT_ANCHOR, from, names };
};

const importsIn = function importsIn(root: string, manifest: unknown, filePath: string): ImportEntry[] {
    const sourceFile = ts.createSourceFile(filePath, readFileSync(filePath, "utf8"), ts.ScriptTarget.Latest, true);
    return sourceFile.statements.flatMap((statement) => {
        const entry = importOf(root, manifest, filePath, statement);
        return entry === null ? [] : [entry];
    });
};

const dependentMembers = function dependentMembers(root: string, manifest: unknown): string[] {
    const name = stringAt(manifest, "name");
    return MEMBER_ROOTS.map((member) => join(ROOT, member)).filter((member) => {
        if (resolve(member) === resolve(root) || !existsSync(join(member, MANIFEST_NAME))) {
            return false;
        }
        return name in stringRecordAt(manifestOf(member), "dependencies");
    });
};

const consumerFiles = function consumerFiles(root: string, manifest: unknown): string[] {
    const tests = absolutePath("codebase.testing.app");
    const configs = BUILD_CONFIG_FILES.map((file) => join(root, file)).filter((file) => existsSync(file));
    const siblings = dependentMembers(root, manifest).flatMap((member) =>
        sourceFilesUnder(member).filter((file) => !file.includes(TEST_MARK)),
    );
    return [...(existsSync(tests) ? sourceFilesUnder(tests) : []), ...configs, ...siblings];
};

const runnerImports = function runnerImports(
    root: string,
    manifest: unknown,
    exports: readonly ExportEntry[],
): ImportEntry[] {
    return Object.values(RUNNER_MODULES).map((module) => {
        const from = anchorSpecifier(root, manifest, root, module.path);
        const names =
            module.names === WHOLE
                ? exports.filter((entry) => `${PROJECT_ANCHOR}/${entry.file}` === from).map((entry) => entry.name)
                : module.names;
        return { file: PROJECT_ANCHOR, from, names };
    });
};

export const collectExternalConsumers = function collectExternalConsumers(
    root: string,
    exports: readonly ExportEntry[],
): ImportEntry[] {
    const manifest = manifestOf(root);
    return [
        ...consumerFiles(root, manifest).flatMap((file) => importsIn(root, manifest, file)),
        ...runnerImports(root, manifest, exports),
    ];
};