mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
Pull requests whose changes cannot reach any Tier A test (allowlist checked against declared Tier A inputs and an AST scan of cross-project reads) skip the unit coverage suite; master pushes always run it. Jest's transform cache is persisted with actions/cache: PRs restore only, master pushes start empty and save. Paired CI runs: Tier A 9m04s cold -> 6m09s warm. Nx Cloud is intentionally not used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
3.1 KiB
JavaScript
72 lines
3.1 KiB
JavaScript
/**
|
|
* Lists repository files outside a Tier A project that its code refers to by
|
|
* path: relative module specifiers and path strings such as
|
|
* `'../../../../../.github/workflows/build-and-make.yaml'` or
|
|
* `'tools/embedded-mpv/stage-runtime.mjs'`. The unit-coverage scope rule must
|
|
* never treat these as skippable; unit-coverage-scope.test.mjs asserts that.
|
|
*
|
|
* String literals are read from the TypeScript AST, so paths mentioned in
|
|
* comments do not count. Imports of other apps/libs code are project-graph
|
|
* edges and are left to Nx; node_modules is ignored.
|
|
*/
|
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import ts from 'typescript';
|
|
|
|
const SOURCE_FILE = /\.(ts|mts|cts|js|mjs|cjs)$/;
|
|
const WORKSPACE_PATH = /^(?:\.github|tools|docs|patches|scripts|snap|resources|build)\/|^[\w.-]+\.(?:json|ya?ml|md|js|cjs|mjs)$/;
|
|
|
|
function listSources(directory, out = []) {
|
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
if (entry.name === 'node_modules') continue;
|
|
const fullPath = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) listSources(fullPath, out);
|
|
else if (SOURCE_FILE.test(entry.name)) out.push(fullPath);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function stringLiterals(fileName, text) {
|
|
const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, false);
|
|
const literals = [];
|
|
const visit = (node) => {
|
|
if (ts.isStringLiteralLike(node)) literals.push(node.text);
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(source);
|
|
return literals;
|
|
}
|
|
|
|
function isOtherProjectCode(relative) {
|
|
return /^(apps|libs)\//.test(relative) && !/\.(json|ya?ml|md)$/.test(relative);
|
|
}
|
|
|
|
/** Returns Map<workspace-relative file, Set<referencing file>>. */
|
|
export function tierAExternalReferences({ workspaceRoot, tierAProjects }) {
|
|
const references = new Map();
|
|
const record = (target, from) => {
|
|
if (!references.has(target)) references.set(target, new Set());
|
|
references.get(target).add(path.relative(workspaceRoot, from));
|
|
};
|
|
for (const project of tierAProjects) {
|
|
const projectRoot = path.resolve(workspaceRoot, project.root);
|
|
for (const file of listSources(path.resolve(workspaceRoot, project.sourceRoot))) {
|
|
for (const literal of stringLiterals(file, readFileSync(file, 'utf8'))) {
|
|
let absolute = null;
|
|
if (literal.startsWith('../')) {
|
|
absolute = path.resolve(path.dirname(file), literal);
|
|
} else if (WORKSPACE_PATH.test(literal)) {
|
|
absolute = path.resolve(workspaceRoot, literal);
|
|
}
|
|
if (!absolute || !existsSync(absolute)) continue;
|
|
const relative = path.relative(workspaceRoot, absolute);
|
|
if (relative.startsWith('..') || relative.startsWith('node_modules')) continue;
|
|
if (absolute === projectRoot || absolute.startsWith(projectRoot + path.sep)) continue;
|
|
if (isOtherProjectCode(relative)) continue;
|
|
record(relative.split(path.sep).join('/'), file);
|
|
}
|
|
}
|
|
}
|
|
return references;
|
|
}
|