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.tsstays encapsulated for the patterninternal), 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.tsstays 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:
- modules that resolve to no tags (
noTag), - folders matching the string
encapsulationPatternthat are not enforced (inside a barrel module, or anywhere without barrel-less mode), - barrel files in barrel-less module trees (hints at
'allow', findings at'warn'/'forbid'ofbarrelPolicy;allowBarrelsInmatches are only counted), - entry points whose
tsconfig.jsoncannot be found, - workspace-shaping options which a sub-config referenced via
configssilently 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.