Files
TREK/client/scripts/lib/offline.mjs
Maurice 0344a86aec style(client): format the ratchet scripts with Prettier
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.
2026-10-08 13:04:40 +02:00

78 lines
2.9 KiB
JavaScript

/*
* The comparison behind lint:offline (scripts/offline-lint.mjs).
*/
import { join } from 'node:path';
import { sourceGraph } from './imports.mjs';
import { fileList, lowerList, readBaseline, writeBaseline } from './ratchet.mjs';
/** The layers that render or drive a view, and so must not hold a network call. */
export const VIEW_LAYERS = ['components', 'mobile', 'pages', 'hooks'];
/**
* The view files that import from src/api/ for anything but types, each with
* the imports that do it. A type-only import is erased from the bundle and
* carries no request, so it does not count.
*/
export function bypasses(root) {
const found = {};
for (const file of sourceGraph(root)) {
if (!VIEW_LAYERS.includes(file.layer)) continue;
const direct = file.imports.filter((imp) => imp.layer === 'api' && !imp.typeOnly);
if (direct.length) found[file.key] = direct.map((imp) => `${imp.line}: '${imp.specifier}'`);
}
return found;
}
/**
* Runs the check against the list at baselinePath. A view file outside the
* list may not import src/api/. A listed file that no longer does fails the
* check as well until --update takes it out, so the list only shrinks and
* the next file at that path does not inherit the exemption. Returns the exit
* code.
*/
export function check({
root,
baselinePath = join(root, 'scripts/offline-baseline.json'),
update = false,
list = false,
log = console.log,
error = console.error,
}) {
const found = bypasses(root);
const offenders = Object.keys(found);
let baseline = readBaseline(baselinePath, fileList);
if (update) {
baseline = lowerList(baseline, offenders);
writeBaseline(baselinePath, baseline);
}
const allowed = new Set(baseline);
const fresh = offenders.filter((key) => !allowed.has(key));
const stale = baseline.filter((key) => !found[key]);
if (list) {
for (const [key, direct] of Object.entries(found)) {
log(`${allowed.has(key) ? 'listed' : 'new '} ${key}`);
for (const line of direct) log(` ${line}`);
}
}
for (const key of fresh) {
error(`FAIL ${key} imports src/api/ directly:`);
for (const line of found[key]) error(` ${line}`);
}
if (fresh.length) {
error(
'A view reads through a repo (src/repo/) or a store and writes through the mutation queue, ' +
'so it keeps working offline. A domain that is online-only on purpose still gets its calls ' +
'in a store or a repo, not in the view. See client/CLAUDE.md, "Data flow".'
);
}
for (const key of stale) {
error(`FAIL ${key} is listed in scripts/offline-baseline.json but no longer imports src/api/ (or is gone).`);
}
if (stale.length) error('Run npm run lint:offline -- --update to take it off the list.');
log(`offline: ${offenders.length} view file(s) import src/api/ directly, ${baseline.length} listed in the baseline`);
return fresh.length || stale.length ? 1 : 0;
}