mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
3e37dcfb45
A file's layer was the first path segment under src/, so src/types.ts, the shared types module every layer imports as '../types', got the layer "types.ts" and no rule at all, while the imports into it already resolved to "types". It could import a component or a page unnoticed. A file directly in src/ is now the layer of its name without the extension, for the importer and the import alike. No file changes its count: types.ts imports only @trek/shared today.
106 lines
3.8 KiB
JavaScript
106 lines
3.8 KiB
JavaScript
/*
|
|
* The rules and the comparison behind lint:layers (scripts/layer-lint.mjs).
|
|
*/
|
|
import { join } from 'node:path';
|
|
import { sourceGraph } from './imports.mjs';
|
|
import { countMap, lowerCounts, readBaseline, writeBaseline } from './ratchet.mjs';
|
|
|
|
const VIEW = ['components', 'mobile', 'pages', 'hooks'];
|
|
|
|
/**
|
|
* For each directory under src/, the directories its files may not import
|
|
* from. The view layers sit on top: a page composes components, a component
|
|
* may use a shared hook, and the phone screens reuse both. Nothing below the
|
|
* views (stores, repos, sync, api, the Dexie schema and the helpers) knows a
|
|
* view exists, a repo sits under the stores, and the Dexie schema under all
|
|
* of them. help/ (the help center's screen registry, plain data) and vacay/
|
|
* (date helpers) are helpers like utils/. Directories without an entry are
|
|
* free on purpose: mobile and pages sit at the top, the app root wires
|
|
* everything, and managed/ is the slot an operator fills with whole screens
|
|
* at build time. A file directly in src/ is the layer of its name, so
|
|
* src/types.ts falls under types like the src/types/ directory.
|
|
*/
|
|
export const RULES = {
|
|
components: ['pages', 'mobile'],
|
|
hooks: ['pages', 'mobile', 'components'],
|
|
store: VIEW,
|
|
sync: VIEW,
|
|
api: VIEW,
|
|
utils: VIEW,
|
|
i18n: VIEW,
|
|
theme: VIEW,
|
|
constants: VIEW,
|
|
services: VIEW,
|
|
push: VIEW,
|
|
types: VIEW,
|
|
help: VIEW,
|
|
vacay: VIEW,
|
|
repo: [...VIEW, 'store'],
|
|
db: [...VIEW, 'store', 'repo', 'sync', 'api'],
|
|
};
|
|
|
|
/**
|
|
* Every import that crosses a boundary the wrong way, per file. A type-only
|
|
* import counts too: a type the lower layer needs belongs below it.
|
|
*/
|
|
export function violations(root, rules = RULES) {
|
|
const found = {};
|
|
for (const file of sourceGraph(root)) {
|
|
const banned = rules[file.layer];
|
|
if (!banned) continue;
|
|
const wrong = file.imports.filter((imp) => imp.layer && banned.includes(imp.layer));
|
|
if (wrong.length)
|
|
found[file.key] = wrong.map((imp) => `${imp.line}: '${imp.specifier}' (${file.layer} into ${imp.layer})`);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/** Runs the check against the baseline at baselinePath and returns the exit code. */
|
|
export function check({
|
|
root,
|
|
baselinePath = join(root, 'scripts/layers-baseline.json'),
|
|
rules = RULES,
|
|
update = false,
|
|
list = false,
|
|
log = console.log,
|
|
error = console.error,
|
|
}) {
|
|
const found = violations(root, rules);
|
|
const counts = Object.fromEntries(Object.entries(found).map(([key, list]) => [key, list.length]));
|
|
let baseline = readBaseline(baselinePath, countMap);
|
|
|
|
if (list) {
|
|
for (const [key, wrong] of Object.entries(found)) {
|
|
log(`${key} (${wrong.length}, baseline ${baseline[key] ?? 0})`);
|
|
for (const line of wrong) log(` ${line}`);
|
|
}
|
|
}
|
|
|
|
if (update) {
|
|
baseline = lowerCounts(baseline, counts);
|
|
writeBaseline(baselinePath, baseline);
|
|
}
|
|
|
|
const grown = Object.entries(counts).filter(([key, n]) => n > (baseline[key] ?? 0));
|
|
const lowerable = Object.entries(baseline).filter(([key, n]) => (counts[key] ?? 0) < n);
|
|
|
|
for (const [key, n] of grown) {
|
|
error(`FAIL ${key}: ${n} import(s) against the layering, baseline ${baseline[key] ?? 0}:`);
|
|
for (const line of found[key]) error(` ${line}`);
|
|
}
|
|
if (grown.length) {
|
|
error(
|
|
'Move what both sides need down into a layer they may share (a model or helper under utils/, ' +
|
|
'a hook under hooks/, a type under types/) instead of importing upwards.'
|
|
);
|
|
}
|
|
if (lowerable.length && !update) {
|
|
log(`${lowerable.length} file(s) hold fewer of these imports than the baseline: run with --update to lower it.`);
|
|
}
|
|
const sum = (map) => Object.values(map).reduce((a, b) => a + b, 0);
|
|
log(
|
|
`layers: ${sum(counts)} import(s) against the layering in ${Object.keys(counts).length} file(s), baseline allows ${sum(baseline)}`
|
|
);
|
|
return grown.length ? 1 : 0;
|
|
}
|