mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
0344a86aec
The checker scripts were written without semicolons, against the semi: true in .prettierrc, so every one of them failed the formatting rule lint:format enforces on src/ and tests/. This is prettier --write over the scripts this branch added or rewrote, nothing else.
78 lines
3.4 KiB
JavaScript
78 lines
3.4 KiB
JavaScript
/*
|
|
* The imports of a source file, read from its syntax tree rather than with a
|
|
* regular expression, so an import split over lines, a dynamic import() and a
|
|
* re-export count, and an import inside a comment or a string does not.
|
|
*/
|
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
import ts from 'typescript';
|
|
import { listFiles, readText, TEST_FILE, toKey } from './ratchet.mjs';
|
|
|
|
const bindingsAreTypes = (elements) => elements.length > 0 && elements.every((el) => el.isTypeOnly);
|
|
|
|
/**
|
|
* Every module the file names, with the line it is named on and whether the
|
|
* import only brings in types (erased from the bundle, so no runtime path).
|
|
*/
|
|
export function importsOf(source, file) {
|
|
const kind = file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, kind);
|
|
const found = [];
|
|
const add = (node, literal, typeOnly) => {
|
|
if (!literal || !ts.isStringLiteralLike(literal)) return;
|
|
found.push({
|
|
specifier: literal.text,
|
|
line: sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1,
|
|
typeOnly,
|
|
});
|
|
};
|
|
const visit = (node) => {
|
|
if (ts.isImportDeclaration(node)) {
|
|
const clause = node.importClause;
|
|
const named =
|
|
clause?.namedBindings && ts.isNamedImports(clause.namedBindings) ? clause.namedBindings.elements : null;
|
|
const typeOnly = Boolean(clause && (clause.isTypeOnly || (!clause.name && named && bindingsAreTypes(named))));
|
|
add(node, node.moduleSpecifier, typeOnly);
|
|
} else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
|
|
const named = node.exportClause && ts.isNamedExports(node.exportClause) ? node.exportClause.elements : null;
|
|
add(node, node.moduleSpecifier, node.isTypeOnly || Boolean(named && bindingsAreTypes(named)));
|
|
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
|
add(node, node.moduleReference.expression, node.isTypeOnly);
|
|
} else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
|
add(node, node.argument.literal, true);
|
|
} else if (
|
|
ts.isCallExpression(node) &&
|
|
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
(ts.isIdentifier(node.expression) && node.expression.text === 'require'))
|
|
) {
|
|
add(node, node.arguments[0], false);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sf);
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* The top directory under src/ a relative import lands in ("components",
|
|
* "api"; a file directly in src/ gives its own name), or null for a package
|
|
* or a path outside src/.
|
|
*/
|
|
export function layerOf(srcRoot, fromFile, specifier) {
|
|
if (!specifier.startsWith('.')) return null;
|
|
const target = relative(srcRoot, resolve(dirname(fromFile), specifier))
|
|
.split('\\')
|
|
.join('/');
|
|
if (!target || target.startsWith('..')) return null;
|
|
return target.split('/')[0];
|
|
}
|
|
|
|
/** Every source file under root/src outside the tests, with its key, its layer and its imports resolved to layers. */
|
|
export function sourceGraph(root) {
|
|
const src = join(root, 'src');
|
|
return listFiles(root, ['src'], (key) => /\.tsx?$/.test(key) && !TEST_FILE.test(key)).map((path) => ({
|
|
key: toKey(root, path),
|
|
layer: toKey(src, path).split('/')[0],
|
|
imports: importsOf(readText(path), path).map((imp) => ({ ...imp, layer: layerOf(src, path, imp.specifier) })),
|
|
}));
|
|
}
|