Skip to main content

Version 0.21

Behavior Change: String encapsulationPattern Matches at Any Depth

A string encapsulationPattern (default: internal) now also encapsulates pattern folders at any depth of a barrel-less module. Previously only paths starting with the pattern were checked, so a nested folder such as data/foo/internal/secret.ts was silently public despite its name.

A file is now encapsulated if

  • its module-relative path starts with the pattern (previous behavior, kept: internals/x.ts stays encapsulated for the pattern internal), or
  • any directory segment of its module-relative path equals the pattern exactly. The filename itself does not count as a segment, so data/internal.ts stays public.

The change is strictly tightening: no file that was encapsulated before becomes public. However, existing builds can turn red where files inside nested internal folders are imported from outside their module. To migrate, either import a public file instead, or rename the folder if it was never meant to be private. Regular-expression patterns and the precedence of a module's exports are unchanged. The deprecated encapsulatedFolderNameForBarrelLess option is an alias for encapsulationPattern and inherits the new any-depth behavior.

New Configuration Options

Sheriff now documents the new configuration options for stricter dependency control, external library checks, explicit barrel-less exports, barrel policy enforcement, and multi-config workspaces.

denyRules

Use denyRules to forbid imports even when depRules would otherwise allow them. A matching deny rule wins after depRules grants clearance.

For more details, see the Dependency Rules and Configuration Reference.

externalRules

Use externalRules to restrict imports from packages in node_modules by the importing module's tags. Declared packages are now discovered from the nearest package.json when TypeScript cannot resolve them, so uninstalled runtime, peer, and optional dependencies remain subject to externalRules. Undeclared unresolvable imports remain outside external-rule checks.

For more details, see the Dependency Rules and Configuration Reference.

exports

Use exports on an explicit module definition to define which files of a barrel-less module are public outside that module.

Export wildcards are path-segment local. For example, *.port.ts exports a file in the module root, while sub/*.ts exports files one level below sub. When present, exports defines the public API and takes precedence over the default internal folder convention.

For more details, see the Configuration Reference.

configs

Use configs to map workspace subdirectories to additional Sheriff config files. The deepest matching directory mapping wins.

Directory keys must be workspace-relative and stay inside the workspace root. Invalid mappings and missing selected config files now surface as Sheriff configuration errors instead of being ignored or reported as raw filesystem errors.

For more details, see the Configuration Reference.

barrelPolicy and allowBarrelsIn

In barrel-less mode the absence of a barrel file is load-bearing configuration: a single stray index.ts silently turns a barrel-less module into a barrel module and changes its encapsulation semantics — without any diagnostic.

Use barrelPolicy ('allow' | 'warn' | 'forbid', default 'allow') to make stray barrels loud: 'warn' is an observation phase surfaced by sheriff verify only (warning lines, successful exit), while 'forbid' turns every barrel module into a violation — sheriff verify fails and the new barrel-policy ESLint rule reports directly on the barrel file (even an empty one), so the finding shows up in the editor and in CI lint runs. For editor visibility during an observation phase, combine barrelPolicy: 'forbid' with a downgraded ESLint rule severity ('@lambda-solutions/sheriff/barrel-policy': 'warn').

Intentional bucket-level barrels (e.g. an api folder whose index.ts acts as a port) stay legal via allowBarrelsIn glob patterns:

export const config: SheriffConfig = {
enableBarrelLess: true,
barrelPolicy: 'forbid',
allowBarrelsIn: ['**/api'],
// ... other configuration
};

Setting barrelPolicy without enableBarrelLess: true, or allowBarrelsIn without a restrictive barrelPolicy, is rejected as a configuration error — both would otherwise be silently dead configuration.

For more details, see the Configuration Reference.

moduleIdentity

barrelPolicy makes a stray barrel loud; moduleIdentity makes it harmless.

By default (moduleIdentity: 'auto') a directory becomes a module either by matching a modules pattern or by containing a barrel file. The second way derives module identity from a file existing: a stray index.ts in a directory no modules pattern covers creates a new, untagged (noTag) module and re-routes which module — and which tags — an import is attributed to.

With moduleIdentity: 'config', only the modules configuration creates modules. A barrel file never does; files inside such a directory belong to their nearest enclosing configured module.

export const config: SheriffConfig = {
enableBarrelLess: true,
moduleIdentity: 'config',
// ... other configuration
};

Barrels still decide exposure inside a configured module: a configured module containing a barrel file exposes only that barrel file, and the barrel takes precedence over the module's exports. Under 'config', barrelPolicy additionally reports barrel files outside every configured module, so sheriff verify does not go blind on them.

moduleIdentity: 'config' requires enableBarrelLess: true and is otherwise rejected as a configuration error (SH-021).

The default is unchanged, so existing projects are unaffected. Switching to 'config' makes every module that existed only because of a barrel file disappear — see the migration notes in the Configuration Reference.

sheriff doctor

The new builtin command sheriff doctor [main.ts] bundles the diagnostics for silent enforcement gaps — configurations that are correct but whose enforcement is not what the author thinks — into one CI-suitable run:

  1. modules that resolve to no tags (noTag),
  2. folders matching the string encapsulationPattern that are not enforced (inside a barrel module, or anywhere without barrel-less mode),
  3. barrel files in barrel-less module trees (hints at 'allow', findings at 'warn'/'forbid' of barrelPolicy; allowBarrelsIn matches are only counted),
  4. entry points whose tsconfig.json cannot be found,
  5. workspace-shaping options which a sub-config referenced via configs silently inherits from the defaults instead of from the root config.

Check 5 addresses a failure mode that had no signal at all: a sub-config is merged with Sheriff's defaults, never with the root config, so a root config declaring barrelPolicy: 'forbid' or moduleIdentity: 'config' governs only the directories the root config itself covers. Every other directory runs on defaults while sheriff verify reports success — see A sub-config is standalone.

The command exits with 1 on findings of checks 1, 2, 4, and 5, and on check-3 findings under 'warn'/'forbid'. --json emits a machine-readable report with a stable key order for CI pipelines.

For more details, see the CLI documentation.

Migration Notes

An object with only tags, or with tags and exports, is an explicit module definition. If a leaf folder is literally named tags, prefer a path key such as 'src/app/tags': ['type:tags-folder']. Objects that contain tags plus another child key are still treated as nested module configs.

Dependency Rule Semantics

The dependency rules documentation now explains that every source tag of an importing module must have clearance, while multiple matching depRules keys for one source tag are OR-combined. This clarifies why a permissive wildcard can make noDependencies ineffective unless the restriction is expressed with denyRules.