Skip to content
svelte-vitals
English
Esc
navigateopen⌘Jpreview
On this page

architecture/unit-entry-file · Unit entry file

A directory declared to be a unit should contain a file named after it.

Severity: info · Category: architecture

What it checks

Flags a directory you have declared to be a “unit” that contains no file named after it: Card/ without Card.svelte, getFoo/ without getFoo.ts.

This rule is off until you configure it. It has no default idea of what a unit is, because that is your project’s convention, not ours.

Why it matters

A directory named after a unit but missing that unit’s entry file is either an incomplete unit or a grouping wearing the wrong name. Either way the tree stops saying what it means, and anyone, or anything, resolving by convention starts guessing.

A filename-pattern check cannot catch this. Given a path it can ask whether that filename matches its parent directory, but a file that does not exist has no path to check.

How to fix

Add the entry file, or stop declaring the directory a unit. Rename it to camelCase if it is really a grouping, or narrow the declaration that swept it in.

Configuration

Option Type Default
units map of directory glob → extension {}
pascalCaseUnits map of root glob → extension {}
exclude list of directory globs []

In the two map options, units and pascalCaseUnits, each value is the entry file’s extension with its leading dot: '.ts', not 'ts'. Validation accepts any non-empty string, so a missing dot passes config validation and then misfires: the rule looks for getFoots instead of getFoo.ts, never finds it, and reports a missing entry file for a directory that has one.

exclude is a list, not a map, and its values are directory globs rather than extensions.

export default {
  rules: {
    'architecture/unit-entry-file': {
      options: {
        units: {
          'src/lib/api/**/*': '.ts',
          'src/**/functions/*': '.ts',
          'src/**/functions/*/*': '.ts',
          'src/**/stores/*': '.svelte.ts'
        },
        pascalCaseUnits: { 'src/**': '.svelte' },
        exclude: ['**/tests', '**/styleGuide', '**/types', '**/e2e']
      }
    }
  }
};

pascalCaseUnits’s root glob covers every capitalized directory beneath it, not only the ones that are component units. A route segment or an asset tree mirrored inside src/ can be PascalCase too, and neither is a unit. Narrow the root, or add those trees to exclude, rather than letting the sweep catch them. This matters most for a route directory: renaming it to satisfy this rule changes the site’s URL, so for a route segment the fix is to narrow the declaration, never to rename the directory.

units identifies a unit by where it sits. pascalCaseUnits identifies one by its name: every directory under a matching root whose name begins with an uppercase letter. Both are needed, because a camelCase directory may legitimately be a unit or a grouping, and only its position can tell them apart, while a PascalCase unit nests to arbitrary depth, where no path glob can find it.

A directory matched by units takes that declaration; pascalCaseUnits applies only to the rest. When several globs match one directory, the most specific wins, in this order:

  1. more path segments;
  2. fewer ** segments;
  3. the longer key;
  4. the alphabetically first.

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.

exclude

exclude removes a directory and everything beneath it. So it is only for directories that are neither units themselves nor hold units:

Directory In exclude?
test, style-guide, e2e, type folders Yes
a folder whose children are units No: excluding it removes those units too

If a broad units glob sweeps in a folder that holds units, narrow the glob instead of excluding it.

Glob depth

* matches within one path segment and ** across segments, but the two star forms are not symmetric: a ** between two segments matches one segment or more, never zero. So src/lib/api/**/* requires at least two levels below api/, which is what keeps an intermediate grouping level from being treated as a unit. A trailing /** is safe to write: it means “everything under this directory,” and the rule will not treat the directory itself as a unit.

Limitations

Only files under src/ are considered, so a directory outside it is never checked and does not need excluding.

A units or pascalCaseUnits declaration that checks no directory is reported, so a glob typo cannot leave the rule silently checking nothing. “Checks no directory” is stricter than “matches no path”: a pascalCaseUnits key that matched only lowercase directories has identified no unit, so it is reported too. That is what surfaces a key missing the trailing /** it was meant to have.

A declaration whose every match is removed by exclude is reported the same way, and says so: matched only excluded directories rather than matched no directory. The two have different remedies: one is a typo in the glob, the other a contradiction between two options you can both see.

Two things are deliberately left out:

  • A declaration written only inside an overrides entry, since whether it matched anything depends on which paths the override applies to.
  • An exclude glob, which is never checked at all, since an exclusion matching nothing changes no report. A mistyped exclude is therefore silent when the subtree it meant to remove had no findings anyway.

When more than one declaration checks no directory, they are all reported together as a single finding rather than one each, so suppressing that finding suppresses the check for every inert declaration at once.

A declared unit missing its entry file that is also named in the wrong casing draws a finding from architecture/directory-naming as well, when that rule is configured for the same location. 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/unit-entry-file --> on the line above it, or turn the rule off:

export default {
  rules: {
    'architecture/unit-entry-file': 'off'
  }
};