mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
522 lines
21 KiB
JavaScript
522 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* lint:tx: a method that writes more than once must do it inside one transaction.
|
|
*
|
|
* A crash or a thrown error between two writes leaves half a change behind (a
|
|
* journey without its owner row, a reservation whose budget line was not
|
|
* updated). The rule in server/CLAUDE.md is that a multi-statement write runs
|
|
* in `await this.uow.transactional(...)`; this script finds the methods that
|
|
* do not, as far as a static read of the source can.
|
|
*
|
|
* HOW IT COUNTS
|
|
* Every class under src/ is read with the TypeScript parser. A repository
|
|
* method (src/db/repositories/) is a write when it builds an INSERT, UPDATE
|
|
* or DELETE (insertInto/updateTable/deleteFrom, nativeInsert/nativeUpdate/
|
|
* nativeDelete, insert/insertMany/upsert/upsertMany, a query builder's
|
|
* update/delete, flush, or a raw SQL string that starts with a write verb),
|
|
* directly or through another method of its own class.
|
|
*
|
|
* A method's "write units" are then counted over its body:
|
|
* - a call to a write, on a constructor-injected field (`this.trips.x()`)
|
|
* or on its own class (`this.x()`), is one unit;
|
|
* - a `.transactional(...)` call that holds any write is one unit, however
|
|
* many writes it holds;
|
|
* - a call to a method of another class that is itself reported counts as
|
|
* one unit here (the fault is reported once, where it lives);
|
|
* - a write inside a loop, or inside a callback handed to forEach/map/
|
|
* reduce and friends, counts twice (it can run any number of times);
|
|
* - of two branches (if/else, a ternary, the cases of a switch, an `if`
|
|
* that returns or throws against the rest of its block) only the larger
|
|
* one counts, since only one of them runs.
|
|
* A method tagged `@txStandalone` in its JSDoc (an audit row, a notification,
|
|
* a failure record, an idempotent lazy provision, a non-fatal derived refresh)
|
|
* adds no unit to its callers; the tag says why in its text. A method tagged
|
|
* `@txIndependent` (writes that are each complete on their own on purpose,
|
|
* such as a retention sweep in passes or a sync with network I/O between its
|
|
* writes) is not reported itself.
|
|
* A method holding two or more units is reported, unless every call to it
|
|
* that the script can resolve sits inside a `.transactional(...)` callback or
|
|
* in a method that is itself only ever called that way (a helper that runs
|
|
* inside its callers' transaction).
|
|
*
|
|
* WHAT IT CANNOT SEE
|
|
* A local helper (`const f = async () => ...` inside the method) is counted
|
|
* where it is called, not where it is declared.
|
|
* Calls through anything but `this.<field>.<method>()`, `this.<method>()` and
|
|
* such a local helper (a free function, a value passed in as an argument, a
|
|
* dynamic dispatch) are not followed, and a write after network I/O that cannot share a
|
|
* transaction with it is still reported: those entries stay in the baseline.
|
|
*
|
|
* THE RATCHET
|
|
* scripts/tx-writes-baseline.json holds today's reported methods with their
|
|
* unit count. A method not in it fails, and so does one past its count. An
|
|
* entry above what its method holds now, or for a method that is no longer
|
|
* reported, fails too until --update lowers or drops it, so a fixed method
|
|
* cannot quietly regress.
|
|
*
|
|
* npm run lint:tx check against the baseline (CI)
|
|
* npm run lint:tx -- --update lower the baseline to what the code holds now;
|
|
* it never raises or adds an entry
|
|
* npm run lint:tx -- --list print every reported method with its count
|
|
* npm run lint:tx -- --explain the same, with the line of each call that counted
|
|
*
|
|
* --dir=<path> points the check at another server root (the unit tests use it).
|
|
*/
|
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { join, relative, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const require_ = createRequire(import.meta.url);
|
|
const ts = require_('typescript');
|
|
|
|
const SOURCE = /\.ts$/;
|
|
const SKIP = /\.(?:test|spec|d)\.ts$/;
|
|
const REPOSITORY_DIR = 'src/db/repositories/';
|
|
|
|
/** Calls that issue a write wherever a repository makes them. */
|
|
const WRITE_CALLS = new Set([
|
|
'insertInto',
|
|
'updateTable',
|
|
'deleteFrom',
|
|
'replaceInto',
|
|
'nativeInsert',
|
|
'nativeUpdate',
|
|
'nativeDelete',
|
|
'insert',
|
|
'insertMany',
|
|
'upsert',
|
|
'upsertMany',
|
|
'update',
|
|
'delete',
|
|
'truncate',
|
|
'flush',
|
|
'persistAndFlush',
|
|
'removeAndFlush',
|
|
]);
|
|
|
|
/** The base-class writes a service may call on a repository field directly. */
|
|
const BASE_WRITES = new Set([
|
|
'nativeInsert',
|
|
'nativeUpdate',
|
|
'nativeDelete',
|
|
'insert',
|
|
'insertMany',
|
|
'upsert',
|
|
'upsertMany',
|
|
]);
|
|
|
|
/**
|
|
* A method whose JSDoc carries this tag writes something that stands on its own,
|
|
* whatever its caller does around it: an audit row, a notification, a failure
|
|
* record, an idempotent lazy provision, a non-fatal derived refresh. Its calls
|
|
* add no unit to the caller. The method's own body is still counted.
|
|
*/
|
|
export const STANDALONE_TAG = 'txStandalone';
|
|
|
|
/**
|
|
* A method whose JSDoc carries this tag writes in steps that are each complete
|
|
* on their own on purpose: a retention sweep that deletes in passes, a sync
|
|
* whose network round trips sit between its writes, one transaction per item
|
|
* of a batch. It is not reported; the tag's text says why.
|
|
*/
|
|
export const INDEPENDENT_TAG = 'txIndependent';
|
|
|
|
const RAW_WRITE = /^\s*(?:INSERT|UPDATE|DELETE|REPLACE)\b/i;
|
|
|
|
/** Array methods that run their callback once per element. */
|
|
const LOOP_CALLBACKS = new Set(['forEach', 'map', 'flatMap', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex']);
|
|
|
|
function walk(dir, files = []) {
|
|
for (const name of readdirSync(dir)) {
|
|
if (name === 'node_modules') continue;
|
|
const path = join(dir, name);
|
|
if (statSync(path).isDirectory()) walk(path, files);
|
|
else if (SOURCE.test(name) && !SKIP.test(name)) files.push(path);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function nameOf(node) {
|
|
return node && node.name && (ts.isIdentifier(node.name) || ts.isPrivateIdentifier(node.name)) ? node.name.text : null;
|
|
}
|
|
|
|
/** The class a type annotation names: `FooRepository`, `Foo | undefined`, `Readonly<Foo>` all give `Foo...`. */
|
|
function typeName(type) {
|
|
if (!type) return null;
|
|
if (ts.isTypeReferenceNode(type)) {
|
|
const n = type.typeName;
|
|
return ts.isIdentifier(n) ? n.text : n.right.text;
|
|
}
|
|
if (ts.isUnionTypeNode(type)) {
|
|
for (const t of type.types) {
|
|
const found = typeName(t);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Every class in the tree, with its methods and the classes its constructor injects. */
|
|
export function collect(serverDir) {
|
|
const srcDir = join(serverDir, 'src');
|
|
if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {
|
|
throw new Error(`src/ does not exist under ${serverDir}: the check would pass without looking at anything`);
|
|
}
|
|
const classes = new Map();
|
|
for (const path of walk(srcDir)) {
|
|
const file = relative(serverDir, path).split('\\').join('/');
|
|
const sf = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
const visit = (node) => {
|
|
if (ts.isClassDeclaration(node) && node.name) {
|
|
const cls = {
|
|
name: node.name.text,
|
|
file,
|
|
repository: file.startsWith(REPOSITORY_DIR),
|
|
fields: new Map(),
|
|
methods: new Map(),
|
|
standalone: new Set(),
|
|
independent: new Set(),
|
|
};
|
|
for (const member of node.members) {
|
|
if (ts.isConstructorDeclaration(member)) {
|
|
for (const p of member.parameters) {
|
|
const field = ts.isIdentifier(p.name) ? p.name.text : null;
|
|
const type = typeName(p.type);
|
|
if (field && type) cls.fields.set(field, type);
|
|
}
|
|
} else if (ts.isPropertyDeclaration(member) && member.initializer && ts.isArrowFunction(member.initializer)) {
|
|
const method = nameOf(member);
|
|
if (method) cls.methods.set(method, member.initializer);
|
|
} else if (ts.isPropertyDeclaration(member)) {
|
|
const field = nameOf(member);
|
|
const type = typeName(member.type);
|
|
if (field && type) cls.fields.set(field, type);
|
|
} else if (ts.isMethodDeclaration(member) && member.body) {
|
|
const method = nameOf(member);
|
|
if (method) cls.methods.set(method, member);
|
|
const tags = method ? ts.getJSDocTags(member).map((tag) => tag.tagName.text) : [];
|
|
if (tags.includes(STANDALONE_TAG)) cls.standalone.add(method);
|
|
if (tags.includes(INDEPENDENT_TAG)) cls.independent.add(method);
|
|
}
|
|
}
|
|
// Two classes of one name: keep the first; the second is reached by no field type.
|
|
if (!classes.has(cls.name)) classes.set(cls.name, cls);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sf);
|
|
}
|
|
return classes;
|
|
}
|
|
|
|
/** `this.x` → 'x', `this.#x` → '#x', otherwise null. */
|
|
function thisMember(expr) {
|
|
if (ts.isPropertyAccessExpression(expr) && expr.expression.kind === ts.SyntaxKind.ThisKeyword) return expr.name.text;
|
|
return null;
|
|
}
|
|
|
|
function exits(stmt) {
|
|
if (!stmt) return false;
|
|
if (ts.isReturnStatement(stmt) || ts.isThrowStatement(stmt)) return true;
|
|
if (ts.isBlock(stmt)) return stmt.statements.length > 0 && exits(stmt.statements[stmt.statements.length - 1]);
|
|
if (ts.isIfStatement(stmt)) return exits(stmt.thenStatement) && exits(stmt.elseStatement);
|
|
return false;
|
|
}
|
|
|
|
export function analyse(classes) {
|
|
const units = new Map(); // "Class.method" → number
|
|
const inProgress = new Set();
|
|
const callSites = new Map(); // "Class.method" → [{ caller, covered }]
|
|
const reading = []; // the "Class.method" whose body is being read
|
|
const sites = new Map(); // "Class.method" → the calls that added a unit, for --explain
|
|
const frames = [];
|
|
const locals = []; // per method read: local `const f = async () => ...` helpers, counted where they are called
|
|
const running = new Set();
|
|
|
|
/** Records the call that added a unit to the method being read, and returns that unit. */
|
|
function unit(node, label) {
|
|
const sf = node.getSourceFile();
|
|
const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
frames[frames.length - 1]?.push(`${line}: ${label}`);
|
|
return 1;
|
|
}
|
|
|
|
/** What `callee` is: { cls, method } on a known class, a base write, or null. */
|
|
function resolve(cls, callee) {
|
|
if (!ts.isPropertyAccessExpression(callee)) return null;
|
|
const method = callee.name.text;
|
|
const own = thisMember(callee);
|
|
if (own !== null) return cls.methods.has(own) ? { cls, method: own } : null;
|
|
const field = thisMember(callee.expression);
|
|
if (field === null) return null;
|
|
const target = classes.get(cls.fields.get(field));
|
|
if (!target) return null;
|
|
if (target.methods.has(method)) return { cls: target, method };
|
|
if (target.repository && BASE_WRITES.has(method)) return { write: true };
|
|
return null;
|
|
}
|
|
|
|
function directWrite(cls, call) {
|
|
if (!cls.repository) return false;
|
|
const callee = call.expression;
|
|
if (ts.isPropertyAccessExpression(callee) && WRITE_CALLS.has(callee.name.text)) {
|
|
// `this.update(...)` on a repository's own method is resolved as a call instead.
|
|
if (callee.expression.kind === ts.SyntaxKind.ThisKeyword && cls.methods.has(callee.name.text)) return false;
|
|
return true;
|
|
}
|
|
const first = call.arguments[0];
|
|
return Boolean(
|
|
first && (ts.isStringLiteral(first) || ts.isNoSubstitutionTemplateLiteral(first)) && RAW_WRITE.test(first.text),
|
|
);
|
|
}
|
|
|
|
/** Write units of `node`, read inside `cls`. `covered` is true inside a transactional callback. */
|
|
function count(cls, node, covered) {
|
|
if (!node) return 0;
|
|
if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) return 0;
|
|
if (ts.isBlock(node) || ts.isSourceFile(node) || ts.isModuleBlock(node))
|
|
return statements(cls, node.statements, 0, covered);
|
|
if (ts.isIfStatement(node)) {
|
|
return (
|
|
count(cls, node.expression, covered) +
|
|
Math.max(count(cls, node.thenStatement, covered), count(cls, node.elseStatement, covered))
|
|
);
|
|
}
|
|
if (ts.isConditionalExpression(node)) {
|
|
return (
|
|
count(cls, node.condition, covered) +
|
|
Math.max(count(cls, node.whenTrue, covered), count(cls, node.whenFalse, covered))
|
|
);
|
|
}
|
|
if (ts.isSwitchStatement(node)) {
|
|
let max = 0;
|
|
for (const clause of node.caseBlock.clauses) max = Math.max(max, statements(cls, clause.statements, 0, covered));
|
|
return count(cls, node.expression, covered) + max;
|
|
}
|
|
if (
|
|
ts.isForStatement(node) ||
|
|
ts.isForOfStatement(node) ||
|
|
ts.isForInStatement(node) ||
|
|
ts.isWhileStatement(node) ||
|
|
ts.isDoStatement(node)
|
|
) {
|
|
let head = 0;
|
|
if (ts.isForStatement(node)) head = count(cls, node.initializer, covered);
|
|
else if (!ts.isDoStatement(node) && !ts.isWhileStatement(node)) head = count(cls, node.expression, covered);
|
|
return head + 2 * count(cls, node.statement, covered);
|
|
}
|
|
if (ts.isCallExpression(node)) return call(cls, node, covered);
|
|
if (
|
|
ts.isVariableDeclaration(node) &&
|
|
ts.isIdentifier(node.name) &&
|
|
node.initializer &&
|
|
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))
|
|
) {
|
|
locals[locals.length - 1].set(node.name.text, node.initializer.body);
|
|
return 0;
|
|
}
|
|
let total = 0;
|
|
ts.forEachChild(node, (child) => {
|
|
total += count(cls, child, covered);
|
|
});
|
|
return total;
|
|
}
|
|
|
|
/** A block read statement by statement: an `if` that always leaves competes with the rest of the block. */
|
|
function statements(cls, list, from, covered) {
|
|
let total = 0;
|
|
for (let i = from; i < list.length; i++) {
|
|
const stmt = list[i];
|
|
if (ts.isIfStatement(stmt) && !stmt.elseStatement && exits(stmt.thenStatement)) {
|
|
const rest = statements(cls, list, i + 1, covered);
|
|
return total + count(cls, stmt.expression, covered) + Math.max(count(cls, stmt.thenStatement, covered), rest);
|
|
}
|
|
total += count(cls, stmt, covered);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
function call(cls, node, covered) {
|
|
const callee = node.expression;
|
|
const method = ts.isPropertyAccessExpression(callee) ? callee.name.text : null;
|
|
const receiver = ts.isPropertyAccessExpression(callee)
|
|
? count(cls, callee.expression, covered)
|
|
: count(cls, callee, covered);
|
|
if (method === 'transactional') {
|
|
let inner = 0;
|
|
frames.push([]); // what runs inside is one unit, not a list of its own
|
|
for (const arg of node.arguments) inner += count(cls, arg, true);
|
|
frames.pop();
|
|
return receiver + (inner > 0 ? unit(node, 'transactional(...)') : 0);
|
|
}
|
|
let args = 0;
|
|
for (const arg of node.arguments) args += count(cls, arg, covered);
|
|
if (method && LOOP_CALLBACKS.has(method)) args *= 2;
|
|
const local = ts.isIdentifier(callee) ? callee.text : null;
|
|
const helper = local !== null ? locals[locals.length - 1].get(local) : undefined;
|
|
if (helper && !running.has(helper)) {
|
|
running.add(helper);
|
|
const inner = count(cls, helper, covered);
|
|
running.delete(helper);
|
|
return receiver + args + inner;
|
|
}
|
|
const text = ts.isPropertyAccessExpression(callee) ? callee.getText().replace(/\s+/g, '') : method;
|
|
if (directWrite(cls, node)) return receiver + args + unit(node, text);
|
|
const target = resolve(cls, callee);
|
|
if (!target) return receiver + args;
|
|
if (target.write) return receiver + args + unit(node, text);
|
|
if (target.cls.standalone.has(target.method)) return receiver + args;
|
|
const key = `${target.cls.name}.${target.method}`;
|
|
const list = callSites.get(key) ?? [];
|
|
list.push({ caller: reading[reading.length - 1], covered });
|
|
callSites.set(key, list);
|
|
return receiver + args + (unitsOf(target.cls, target.method) > 0 ? unit(node, text) : 0);
|
|
}
|
|
|
|
function unitsOf(cls, method) {
|
|
const key = `${cls.name}.${method}`;
|
|
if (units.has(key)) return units.get(key);
|
|
if (inProgress.has(key)) return 0; // recursion: the outer frame counts the body once
|
|
inProgress.add(key);
|
|
const fn = cls.methods.get(method);
|
|
frames.push([]);
|
|
locals.push(new Map());
|
|
reading.push(key);
|
|
const n = count(cls, fn.body, false);
|
|
reading.pop();
|
|
locals.pop();
|
|
sites.set(key, frames.pop());
|
|
inProgress.delete(key);
|
|
units.set(key, n);
|
|
return n;
|
|
}
|
|
|
|
for (const cls of classes.values()) for (const method of cls.methods.keys()) unitsOf(cls, method);
|
|
|
|
// Bound to a transaction: called at least once, and every call either sits in a
|
|
// transactional callback or comes from a method that is bound itself. Grown from
|
|
// nothing to a fixed point, so a cycle of methods calling each other binds none.
|
|
const bound = new Set();
|
|
for (let grew = true; grew;) {
|
|
grew = false;
|
|
for (const [key, list] of callSites) {
|
|
if (bound.has(key) || list.length === 0) continue;
|
|
if (list.every((site) => site.covered || bound.has(site.caller))) {
|
|
bound.add(key);
|
|
grew = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const reported = {};
|
|
const explained = {};
|
|
for (const cls of classes.values()) {
|
|
for (const method of cls.methods.keys()) {
|
|
const key = `${cls.name}.${method}`;
|
|
const n = units.get(key);
|
|
if (n < 2) continue;
|
|
if (bound.has(key) || cls.independent.has(method)) continue;
|
|
reported[`${cls.file}#${key}`] = n;
|
|
explained[`${cls.file}#${key}`] = sites.get(key);
|
|
}
|
|
}
|
|
return {
|
|
found: Object.fromEntries(Object.entries(reported).sort(([a], [b]) => a.localeCompare(b))),
|
|
sites: explained,
|
|
};
|
|
}
|
|
|
|
/** The baseline as committed. Missing or malformed stops the run instead of reading as empty. */
|
|
export function readBaseline(path) {
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
} catch (err) {
|
|
throw new Error(`scripts/tx-writes-baseline.json cannot be read: ${err.message}`);
|
|
}
|
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
throw new Error('scripts/tx-writes-baseline.json must be an object of methods to write counts');
|
|
}
|
|
for (const [key, n] of Object.entries(parsed)) {
|
|
if (!Number.isInteger(n) || n < 2) {
|
|
throw new Error(
|
|
`scripts/tx-writes-baseline.json: ${key} holds ${JSON.stringify(n)}, expected an integer of at least 2`,
|
|
);
|
|
}
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
/** The baseline lowered to what the code holds now. Never raises an entry, never adds one. */
|
|
export function lowerBaseline(baseline, found) {
|
|
const lowered = {};
|
|
for (const [key, allowed] of Object.entries(baseline)) {
|
|
if (key in found) lowered[key] = Math.min(allowed, found[key]);
|
|
}
|
|
return Object.fromEntries(Object.entries(lowered).sort(([a], [b]) => a.localeCompare(b)));
|
|
}
|
|
|
|
export function compare(baseline, found) {
|
|
const grown = Object.entries(found).filter(([key, n]) => n > (baseline[key] ?? 1));
|
|
const lowered = lowerBaseline(baseline, found);
|
|
const stale = Object.entries(baseline).filter(([key, n]) => lowered[key] !== n);
|
|
return { grown, stale };
|
|
}
|
|
|
|
function main(argv) {
|
|
const dirArg = argv.find((a) => a.startsWith('--dir='));
|
|
const serverDir = dirArg ? resolve(dirArg.slice('--dir='.length)) : fileURLToPath(new URL('..', import.meta.url));
|
|
const baselinePath = join(serverDir, 'scripts', 'tx-writes-baseline.json');
|
|
|
|
const { found, sites } = analyse(collect(serverDir));
|
|
if (argv.includes('--list') || argv.includes('--explain')) {
|
|
for (const [key, n] of Object.entries(found)) {
|
|
console.log(`${n}\t${key}`);
|
|
if (argv.includes('--explain')) for (const site of sites[key]) console.log(`\t\t${site}`);
|
|
}
|
|
return 0;
|
|
}
|
|
let baseline = readBaseline(baselinePath);
|
|
if (argv.includes('--update')) {
|
|
baseline = lowerBaseline(baseline, found);
|
|
writeFileSync(baselinePath, JSON.stringify(baseline, null, 2) + '\n');
|
|
}
|
|
|
|
const { grown, stale } = compare(baseline, found);
|
|
for (const [key, n] of grown) {
|
|
const entry = baseline[key];
|
|
console.error(
|
|
`FAIL ${key}: ${n} writes outside one transaction` +
|
|
(entry ? `, its baseline is ${entry}. ` : '. ') +
|
|
'Wrap the sequence in await this.uow.transactional(...) in the service.',
|
|
);
|
|
}
|
|
for (const [key, entry] of stale) {
|
|
console.error(
|
|
`FAIL ${key} is held at ${entry} in scripts/tx-writes-baseline.json, ` +
|
|
(key in found ? `but it has ${found[key]} now.` : 'but it is no longer reported.'),
|
|
);
|
|
}
|
|
if (stale.length) {
|
|
console.error('Run npm run lint:tx -- --update to lower the baseline with the change that fixed it.');
|
|
}
|
|
console.log(
|
|
`tx: ${Object.keys(found).length} method(s) with writes outside one transaction, ${Object.keys(baseline).length} held at their baseline`,
|
|
);
|
|
return grown.length || stale.length ? 1 : 0;
|
|
}
|
|
|
|
const isCli =
|
|
Boolean(process.argv[1]) &&
|
|
existsSync(process.argv[1]) &&
|
|
realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
if (isCli) {
|
|
try {
|
|
process.exitCode = main(process.argv.slice(2));
|
|
} catch (err) {
|
|
console.error(`FAIL ${err.message}`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|