architecture/directory-naming · Directory naming
A directory should be named in the casing its location declares.
Severity: info · Category: architecture
What it checks
Flags a directory whose name does not match the casing you have declared for its location:
UserProfile/ where the features root is camelCase, setCookie/ where endpoint segments are
kebab-case.
This rule is off until you configure it. It has no default idea of what your directory names should look like, because that is your project’s convention, not ours.
Why it matters
A directory name is the cheapest signal a tree has. When the convention holds, parts/ and Card/
tell a reader, human or agent, what they are without opening anything. One directory that breaks it
costs nothing today and makes the signal unreliable forever after, because a reader who has met one
exception has to check every case from then on.
How to fix
Rename the directory, or narrow the declaration that swept it in.
Configuration
| Option | Type | Default |
|---|---|---|
directories |
map of directory glob → casing set | {} |
exclude |
list of directory globs | [] |
export default {
rules: {
'architecture/directory-naming': {
options: {
directories: {
'src/routes/**': 'camelCase|PascalCase',
'src/routes/internalApi/*': 'kebab-case',
'src/lib/features/*': 'camelCase',
'src/lib/api/*': 'camelCase'
}
}
}
}
};
The casing names
Four are recognised, and each tests the whole name rather than its first character:
| Name | Accepts | Example |
|---|---|---|
camelCase |
^[a-z][a-zA-Z0-9]*$ |
itemList |
PascalCase |
^[A-Z][a-zA-Z0-9]*$ |
PageHeader |
kebab-case |
^[a-z0-9]+(-[a-z0-9]+)*$ |
clear-cache |
snake_case |
^[a-z0-9]+(_[a-z0-9]+)*$ |
price_table |
A value may name several, joined by |, for a location that legitimately holds more than one kind of
directory. A route’s components/ holds PascalCase component units and camelCase groupings side by
side.
These rules mean different things by “PascalCase”, on purpose:
- This rule checks that the whole name conforms.
architecture/unit-entry-fileasks only whether the first character is A–Z. It is asking whether a directory looks like a unit, not whether its name conforms.architecture/reserved-directory-namesuses that same first-character test, but also requires a same-named file.
So a directory can pass one rule’s PascalCase gate and fail another’s.
One lowercase word satisfies camelCase, kebab-case and snake_case at once. dialog matches
all three, because there is nothing in the name to disagree with. This rule only fires on a name that
carries the evidence of a casing it fails: a capital, a hyphen, an underscore, a leading digit, or a
character none of the four admits.
A name with no letter in it is never reported. 2024 and 404 carry no casing, and a
year-archive route cannot be renamed without changing its URL.
Route directories
A directory whose name is SvelteKit route syntax is decoded before the casing test, so a declaration
reaching into src/routes/ is usable:
| Directory | Checked as |
|---|---|
[itemId] |
itemId |
[itemId=integer] |
itemId |
[...rest] |
rest |
[[optional]] |
optional |
(app) |
app |
A compound segment such as [foo]-[bar] names no single identifier and is skipped.
One consequence: a declaration covering src/routes/ governs parameter and group names under the
same casing as static segments. A project wanting kebab-case URL segments but camelCase parameters in
one subtree should declare the narrower static-segment globs instead.
Which declaration wins
When several globs match one directory, the most specific wins: more path segments first, then fewer
** segments, then the longer key, then the alphabetically first. That is what lets
'src/routes/internalApi/*' narrow 'src/routes/**'.
Segment count includes wildcards, so a key made only of wildcards can outrank one naming a real directory if it is deeper. Write the depth you mean.
A trailing /** means “everything under this directory” and never governs the directory itself,
which matters here, because the containers those keys name are src/routes, src/lib and src, and
SvelteKit chooses those names, not you.
exclude
exclude removes a directory and everything beneath it. Use it for a subtree whose names you do
not control, such as generated code or a vendored tree:
options: {
directories: { 'src/lib/**': 'camelCase' },
exclude: ['src/lib/generated']
}
If a broad declaration sweeps in a subtree that you do control, narrow the glob instead of excluding it: an exclusion takes everything below it out of the check as well.
Limitations
Only directories under src/ are considered, so anything outside it is never checked and does not
need excluding. File names are not checked at all.
A violation’s route is the directory it names; its location points at a file inside that
directory, because --diff filters on location and git can only tell a file changed, never a
directory.
The two are kept apart on purpose: a directory nested inside another violating directory is still reported separately, and each can be suppressed on its own.
A declaration that is not checking what it says is reported, so a typo cannot leave the rule silently doing nothing. Five cases land in that finding, each named in the message:
| The declaration | Reported as |
|---|---|
| matched no directory | matched no directory |
had every match removed by exclude |
matched only excluded directories |
names no casing at all (e.g. '', '|') |
the value names no casing at all, so it checks nothing |
| names no casing this rule knows | unknown casing name '…', so it checks nothing |
| names some casing this rule knows | unknown casing name '…'; the rest of the value still applies |
The last is the one worth watching for: the declaration keeps working under its valid names and quietly enforces less than you wrote.
A declaration naming no known casing is dropped before matching, so it cannot shadow a broader valid declaration that would otherwise govern the same directory.
Two things are deliberately never reported:
- A declaration written only inside an
overridesentry, since whether it matched anything depends on which paths the override applies to. - An
excludeglob that matches nothing, since removing nothing changes no report. A mistypedexcludeis therefore silent when the subtree it meant to remove had no findings anyway.
A mis-cased directory that is also a declared unit missing its entry file draws a finding from
architecture/unit-entry-file as well. Neither suppresses the other. They are different claims and
both are true.
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/directory-naming --> on the line above it, or turn the rule off:
export default {
rules: {
'architecture/directory-naming': 'off'
}
};