architecture/reserved-directory-names · Reserved directory names
A directory's subdirectories should only take names declared for that position.
Severity: info · Category: architecture
What it checks
Flags a directory whose name is not one of the names you declared for its position: a helpers/
inside a component unit that may only hold parts/, functions/ and tests/.
This rule is off until you configure it. It has no default idea of which names your project reserves.
Why it matters
A closed set of directory names is only worth writing down if it stays closed. The first directory outside it costs nothing, since it is correctly cased and sits in a plausible place, but the table stops describing the tree, and from then on a reader who has met one exception has to open every directory to learn what it holds.
architecture/directory-naming checks a directory’s casing; this checks its name. A
helpers/ is perfectly camelCase, so no casing declaration objects to it.
How to fix
Rename the directory to a declared name, move it under one of them, or add its name to the declaration. Deciding to widen the set is a legitimate outcome, as long as it is a decision.
Configuration
| Option | Type | Default |
|---|---|---|
scopes |
map of directory glob → allowed child names | {} |
unitScopes |
map of root glob → allowed child names, for units whose name begins A–Z | {} |
anyCaseUnitScopes |
map of root glob → allowed child names, for units of either case | {} |
exclude |
list of directory globs | [] |
export default {
rules: {
'architecture/reserved-directory-names': {
options: {
scopes: { 'src/lib': 'api|components|features|effect|db' },
unitScopes: { 'src/**': 'parts|functions|stores|types|tests|styleGuide' },
anyCaseUnitScopes: { 'src/**': 'functions|stores|types|tests' }
}
}
}
};
The scope maps differ in what their keys name
A scopes key names the parent directly. 'src/lib' matches src/lib, and the names its
immediate subdirectories may take are the ones you list.
A unitScopes key names a root. 'src/**' matches every directory beneath src, and the rule
governs the children of whichever of them are units, meaning a directory whose name begins with a
capital and which holds a file named after it (Card/Card.svelte, Card/Card.ts, Card/Card.svelte.ts). Use
it for a closed set that hangs off something a glob cannot reach, because units nest to arbitrary
depth.
An anyCaseUnitScopes key names a root the same way, but governs units of either case. It is the same
test without the letter requirement, so a .ts- or .svelte.ts-entry unit (formatDate/formatDate.ts,
useThing/useThing.svelte.ts) counts too. unitScopes’s letter test excludes a lowercase unit, so
without this option no generic unit-map declaration governs its children, though a scopes key naming
the parent directly can still reach one. On a real-world tree, 129 of 299 units (43%) are such any-case
units. Neither unit option is named with the bare word “unit”: with both predicates in play, that word
alone wouldn’t say which one applies (architecture/reserved-name-placement takes the same split for
its own options).
A scopes key is only worth writing where the children are entirely drawn from the names you
list. A route directory holds its reserved names beside its route segments, and route segments are
unbounded, one per page, so no declaration belongs there. Writing one anyway reports every segment.
The same applies wherever a position mixes reserved names with names the project invents freely. A
camelCase unit that keeps its own nested helpers beside a tests/ is such a position: the helper names
are as unbounded as route segments, so scopes: { 'src/**/functions/*': 'tests' } would report every
one of them. Between the two, the vocabulary is enforceable under component units and at positions
whose children really are a closed list, not everywhere a reserved name appears.
The names in one declaration need not be the names in another. Each declared position has its own closed set; there is no single table.
Which declaration wins
When more than one map matches one directory, the most specific key governs: more path segments
first, then fewer ** segments, then the longer key, then alphabetically first. That is what lets any
map narrow another.
Two identical globs are the only pair those steps cannot separate, and there a fixed priority
decides: scopes beats both unit maps, and unitScopes beats anyCaseUnitScopes.
scopes wins over either unit map because it applies to every directory its key matches, while a unit
map applies only to the units of its required case. Declaring the same glob in scopes and a unit map
is reported: scopes wins wherever both are declared, so the unit map’s entry does nothing there. (It
can still govern elsewhere: a unitScopes key declared globally and shadowed by a scopes key
added only inside an overrides entry still governs outside that override’s scope.)
unitScopes wins over anyCaseUnitScopes because unitScopes’s letter test is the narrower of the
two gates, since every capitalised unit is also an any-case unit and never the reverse, so the identical glob
in both maps partitions rather than collides: unitScopes governs at capitalised units,
anyCaseUnitScopes governs alone at the lowercase ones unitScopes never reaches. This is not
reported as a dead declaration, because both entries do real work:
options: {
// capitalised units get parts and styleGuide too; lowercase units do not
unitScopes: { 'src/**': 'parts|styleGuide|functions|stores|types|tests' },
anyCaseUnitScopes: { 'src/**': 'functions|stores|types|tests' }
}
A trailing /** means “everything under this directory” and never governs the directory itself.
exclude
exclude removes a directory and everything beneath it. Use it for a subtree whose names you do
not control:
options: {
unitScopes: { 'src/**': 'parts|functions|tests' },
exclude: ['**/generated']
}
Limitations
Only directories under src/ are considered. File names are not checked. Dot directories never
appear, so they need no excluding.
A directory beginning A–Z that holds no file named after it is not a unit here, and this rule
says nothing about its children. That directory is architecture/unit-entry-file’s finding,
reported once rather than once per child.
“Named after it” compares the directory name against the filename up to its first dot, which is
what lets Card/Card.svelte.ts count. One consequence: a directory whose only such file is a test,
Card/Card.test.ts, counts as a unit too, and its children are checked. The alternative, stripping a
single extension, would reject a real entry-file shape, and a finding a reader can dismiss is the
milder failure.
The cut applies only to the filename, not the directory name. The directory’s basename is compared
whole. So src/lib/Card.v2/ is not a unit here, even though it holds Card.v2.svelte: the stem of
that filename up to its first dot is Card, which does not equal the directory’s own uncut name
Card.v2. Its children go unchecked. architecture/unit-entry-file, configured with an explicit extension, asks a
different question, whether Card.v2 + .svelte exists, and answers yes for the same directory.
Both answers are consistent with each rule’s own definition.
The rule says “here, only these names”. It cannot say “this name, only here”: a parts/ in the wrong
place is invisible unless that place is itself declared.
A project that nests units directly inside units should not declare unitScopes or
anyCaseUnitScopes. The nested unit is a child not in the set, and would be reported.
A declaration that is not checking what it says is reported, so a typo cannot leave the rule silently doing nothing. These cases land in that finding, each named in the message:
- the glob matched no directory;
- every directory it matched is excluded;
- a
unitScopeskey matched directories but never a unit; - an
anyCaseUnitScopeskey matched directories but never a unit of either case, the stronger claim, since every capitalised unit is also an any-case unit; - the value lists no name at all;
- the same glob is declared in
scopesand a unit map, with both values naming at least one directory. If either value names nothing it is dropped before matching, so the other governs alone and the empty-value reason reports instead. The same glob declared in both unit maps is not this case; see “Which declaration wins” above.
Two things are never reported:
- A declaration written only inside an
overridesentry, since whether it matched anything depends on which paths the override applies to. One exception: the identical-glob collision check is not narrowed to globally declared keys, so a collision betweenscopesand a unit map assembled entirely fromoverridesentries is still reported. - A declared name no directory currently uses. The set says what may appear, not what must.
Mode differences
None. This rule reads the project’s source-file inventory, the src/** paths rather than file contents, and everywhere it runs that inventory is built the same way. The CLI, the Vite plugin’s build pass, and the live dashboard’s static baseline all report it identically, and the rendered-HTML pass never re-evaluates it. Scoping a run with --route skips it: no inventory is built, and a file finding has no route to attribute it to.
Disabling
Silence a single occurrence with <!-- svelte-vitals-disable-next-line architecture/reserved-directory-names --> on the line above it, or turn the rule off:
export default {
rules: {
'architecture/reserved-directory-names': 'off'
}
};