Skip to content

Custom Rules

You can extend domainlint with your own rules by creating a domainlint.rules.ts (or .js) file at the project root. Each custom rule receives the import graph, a query helper, and an emitViolation callback to report violations.

Create domainlint.rules.ts in your project root:

import type { Rule } from "domainlint";
const noUtilsImport: Rule = {
name: "no-utils-import",
check({ query, emitViolation }) {
for (const edge of query.edgesTo("src/lib/utils/**").edges) {
emitViolation({
code: "CUSTOM_NO_UTILS_IMPORT",
file: edge.from,
line: edge.importInfo.line,
col: edge.importInfo.col,
message: "Importing from utils/ is not allowed. Use a feature barrel instead.",
});
}
},
};
export const rules: Rule[] = [noUtilsImport];

Each rule must have:

PropertyTypeDescription
namestringUnique rule identifier.
check(context) => void | Promise<void>Function that inspects the graph and calls emitViolation for each violation.

The context object contains:

PropertyTypeDescription
emitViolation(result) => voidCallback to report a violation.
queryGraphQueryHigh-level query API for the import graph (see GraphQuery API).
graphDependencyGraphThe raw import graph with nodes, edges, and adjacency list.
configFeatureBoundariesConfigThe resolved domainlint configuration.

Each violation passed to emitViolation must include:

PropertyTypeDescription
codestring?Violation code (e.g. CUSTOM_NO_UTILS_IMPORT). Auto-generated from rule name if omitted.
filestringAbsolute path to the file with the violation.
linenumber1-based line number.
colnumber1-based column number.
messagestringHuman-readable explanation.

By default, domainlint looks for domainlint.rules.ts or domainlint.rules.js in the project root. To use a different path:

{
"rulesFile": "config/my-rules.ts"
}
import type { Rule } from "domainlint";
// Ban features from importing internal lib
const noInternalLib: Rule = {
name: "no-internal-lib",
check({ query, emitViolation }) {
for (const edge of query.edgesBetween("src/features/**", "src/lib/internal/**").edges) {
emitViolation({
code: "NO_INTERNAL_LIB",
file: edge.from,
line: edge.importInfo.line,
col: edge.importInfo.col,
message: "Features must not import internal lib",
});
}
},
};
// Flag files with too many imports
const maxFanOut: Rule = {
name: "max-fan-out",
check({ query, emitViolation }) {
const MAX = 15;
for (const file of query.filesMatching("src/**/*.ts")) {
const count = query.fanOut(file);
if (count > MAX) {
emitViolation({
code: "TOO_MANY_IMPORTS",
file,
line: 1,
col: 0,
message: `File has ${count} imports (max ${MAX})`,
});
}
}
},
};
// Enforce layered architecture
const noUpwardImports: Rule = {
name: "no-upward-imports",
check({ query, emitViolation }) {
for (const edge of query.edgesFrom("src/domain/**").edges) {
if (edge.to.includes("/infrastructure/")) {
emitViolation({
code: "NO_UPWARD_IMPORT",
file: edge.from,
line: edge.importInfo.line,
col: edge.importInfo.col,
message: "Domain layer must not import from infrastructure",
});
}
}
},
};
export const rules: Rule[] = [noInternalLib, maxFanOut, noUpwardImports];