mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
798553b13b
Components reach up into pages/ for models and helpers (28 imports in 25 files: the journey detail helpers, the collections, atlas and dashboard models) and sideways into mobile/ for phone hooks and sheets (36 in 30 files), and a few stores and a repo import from components/. Nothing stopped the next one, and every such import ties two trees together that should move and split independently. lint:layers reads each source file's imports from its syntax tree (multi-line imports, re-exports, import() and type imports included, nothing inside comments or strings) and checks them against a small rule table: a component imports neither pages/ nor mobile/, a shared hook no view, nothing under the views (store, repo, sync, api, db, utils and the other helpers) any view, a repo no store, and the Dexie schema none of the layers above it. mobile/ and pages/ stay free, since the phone screens reuse the page hooks by design. The 80 imports that break a rule today are counted per file in scripts/layers-baseline.json, and a file may hold no more than its entry. Type imports count: a type the lower layer needs belongs below it. Tests are not checked.
97 lines
3.4 KiB
JavaScript
97 lines
3.4 KiB
JavaScript
/*
|
|
* The rules and the comparison behind lint:layers (scripts/layer-lint.mjs).
|
|
*/
|
|
import { join } from 'node:path'
|
|
import { countMap, lowerCounts, readBaseline, writeBaseline } from './ratchet.mjs'
|
|
import { sourceGraph } from './imports.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. Directories without an entry (mobile, pages, the app root) are
|
|
* free.
|
|
*/
|
|
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,
|
|
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
|
|
}
|