Features must not know each other
A feature folder is something a team can build, test, hand over or delete on its own. That stops being true the day one feature imports another, because from then on the two ship together. The answer is one rule with a capture: a module under src/features/<x>/ may import anything except a module under another src/features/<y>/.
- Plan: plan 0004, Step 5
- Source: the first of the design's three cookbook questions, design § The developer relations hat
- Rule language: rules.md § Captures
- Requirement: FR-REACH-01
The rule
from.path captures the feature's folder name as $1, and to.pathNot uses it, so an import inside the same feature is allowed and an import of any other feature is not. Shared code lives in src/shared/, and the app shell src/main.ts may import every feature, because neither is under src/features/. A new feature folder is covered the day it is created; nothing lists the features by name.
rulebearing.yaml$schema: https://benbahrenburg.github.io/rulebearing/schema/config-v1.json
rules:
dependencies:
forbidden:
- name: features-are-independent
comment: "A feature that imports another cannot be built, tested or removed on its own. plan:0004-wave-4-reach"
fix: "Move what both features need into src/shared/, or let the app shell (src/main.ts) pass one feature's result to the other."
severity: error
from: { path: "^src/features/([^/]+)/" }
to: { path: "^src/features/", pathNot: "^src/features/$1/" }
examples:
forbidden:
- "src/features/cart/cart.ts -> src/features/catalog/product.ts"
allowed:
- "src/features/cart/cart.ts -> src/features/cart/line.ts"
- "src/features/cart/cart.ts -> src/shared/money.ts"
- "src/main.ts -> src/features/cart/cart.ts"
The file is rulebearing.yaml. Its examples are the edges rulebearing test checks the rule against.
The same rule for dependency-cruiser
rulebearing config convert rulebearing.yaml --to dependency-cruiser writes the rule in dependency-cruiser's format, which dependency-cruiser runs as it is, and names what it dropped: the $schema, the fix and the examples, which dependency-cruiser has no field for.
.dependency-cruiser.json{
"forbidden": [
{
"name": "features-are-independent",
"comment": "A feature that imports another cannot be built, tested or removed on its own. plan:0004-wave-4-reach",
"to": {
"path": "^src/features/",
"pathNot": "^src/features/$1/"
},
"severity": "error",
"from": {
"path": "^src/features/([^/]+)/"
}
}
]
}
dropped (stderr of config convert)dropped 3 item(s) dependency-cruiser cannot hold:
$schema: points at the native schema
forbidden[features-are-independent].fix: a Rulebearing rule field
forbidden[features-are-independent].examples: a Rulebearing rule field
The files are converted/.dependency-cruiser.json and converted/dropped.txt.
The failing edge
The repository in repo/ has three features and a shared folder. src/features/cart/cart.ts calls findProduct from the catalog feature, so rulebearing cruise reports:
| Severity | Rule | From | To | Type | Through |
|---|---|---|---|---|---|
| error | features-are-independent | src/features/cart/cart.ts | src/features/catalog/product.ts | dependency |
features-are-independentfix: Move what both features need into src/shared/, or let the app shell (src/main.ts) pass one feature's result to the other.
The fix
The cart needs a product's price, not the catalog. Let the cart take the product as an argument, and let the app shell, which may import both features, look it up:
// src/features/cart/cart.ts
export function addToCart(lines: readonly Line[], product: Line): Line[] {
return [...lines, product];
}
// src/main.ts
const product = findProduct('sku-1');
console.log(charge(total(addToCart([], { sku: product.sku, price: product.price }))));
When two features need the same code rather than each other's results, move that code into src/shared/.
Try it
The playground holds this page's repository and rule. Run cruises the files in this browser, as rulebearing cruise does in repo/; change the rule and run it again, or pick another output type.
Everything runs in this browser, with the same library crates as the rulebearing binary: nothing you paste, choose or drop is uploaded or sent anywhere. The configuration is YAML or JSON. A JavaScript configuration is not accepted: the browser build has no JavaScript engine (ADR-0067). An extends of a file cannot be followed; the bundled presets resolve as on the command line. Errors are the module's own words, shown as they come.
- A cruise result: paste the JSON that
rulebearing cruise -T json(or dependency-cruiser) wrote, and Run evaluates the configuration's rules over it, asrulebearing cruise --graph graph.json -T <output type>does. This is the way in for .NET: assemblies are not accepted, since reading them needs a build a browser cannot run, so paste the cruise result the command line wrote for the solution. - Source files: choose a folder or files, or drop them, holding TypeScript and JavaScript or Python, and Run extracts and evaluates them, as
rulebearing cruise -T <output type>does in that folder. Paths are taken from the chosen folder. The language is the one picked, or, left on Infer from the files, the one with more source files outsidenode_modulesand virtual environments (a tie asks you to pick). Files that are not UTF-8 text, files over 1 MiB, anything past 32 MiB or 20,000 files in all, and version-control folders are skipped, and the note under the pickers names them. A configuration file at the folder's root (rulebearing.yaml,rulebearing.yml,rulebearing.jsonor.dependency-cruiser.json) is loaded into the configuration box. What needs the disk or a process is refused with the reason: the Node sidecar, Yarn Plug'n'Play, a ratchet's budget file, and a path outside the folder. - Explain prints what
rulebearing explain <rule> --graph graph.jsonprints for the named rule over the current cruise result: the pasted one, or thejsoncruise of the chosen files.
Examples:
No files chosen yet.