Skip to content

Exports

exports is the one family that is not about paths. It restricts which files may import a named symbol from a module.

exports:
- name: bus-factories-at-composition-roots
message: >-
makeCommandBus / makeQueryBus fold module dispatch tables into one routing
table. Building a bus anywhere but a composition root gives you a second
application with a different set of handlers.
module: "**/node_modules/@effect-server-utils/cqrs/**"
symbols: [makeCommandBus, makeQueryBus, mergeDispatchTables]
except: ["@/server.ts", "@/platform/cqrs/**", "@/test-utils/test-server.ts"]

It lives at the top level of the manifest rather than on a node, because the thing it governs is a symbol in some other package — there is no node in your tree to hang it on.

Every importer of a barrel resolves to the same file. If a module exports both a factory that only a composition root may call and a Tag that everything may import, no rule about the resolved path can separate them. The imported name is the only thing that does.

The same shape covers a second case with no name at all: banning an import form. Omit symbols and the rule means “any named import from this module”, which is how you insist a package be reached by subpath rather than by destructuring its barrel.

Field Meaning
name the rule id, reported on a violation
message why
module globs matched against the module’s resolved path
symbols exact exported names. Omit to mean any named import from that module
kinds binding forms the rule speaks to: named, default, namespace. Defaults to ["named"]
except importers the restriction does not apply to
fix "subpath-namespace-import" — offer an autofix
probe { source, symbol } — an import the rule must cover symbol out of

Names are read as they appear in the source module, not as the importer aliased them: import { makeCommandBus as make } is still makeCommandBus, and export { a as b } from is still a. Renaming at the import site is not an escape.

A rule is compared against bindings by kind, and speaks to named ones unless kinds says otherwise. That default is deliberate — it is the discriminating form for every rule of this shape — but it means a rule about a factory function says nothing about import makeBus from "m" until it lists "default":

- name: bus-factories
module: "**/@org/cqrs/**"
symbols: [makeCommandBus]
kinds: [named, default]

The namespace kind is the one binding form with no name of its own — its symbol is always * — and it is carried by every form that takes the whole module at once:

import * as ns from "m";
export * from "m";
export * as ns from "m";
import x = require("m");
await import("m");
require("m");

Each of those is the same way around a rule about a name: export * from "./internal" launders every symbol of internal through the barrel, and const { makeBus } = await import("m") names no binding. A rule with kinds: ["namespace"] and no symbols is how a policy forbids the form itself:

- name: no-barrel-laundering
module: "src/**/internal/**"
kinds: [namespace]
except: ["src/**/index.ts"]

Because a default binding is only ever named default and a namespace one *, a rule that lists symbols alongside either of those kinds could never fire on such a form — and is refused at load, by its own probe.

A side-effect import (import "m") carries no names, so the export family has nothing to judge there — the import family still sees the edge.

A rule is compared against bindings by kind, and covers named ones unless kinds says otherwise. An authored probe is how to show it sees the form you mean: the snippet is parsed at load, every edge in it is taken to reach module, and a binding named symbol must come out covered — "default" for a default import, "*" for a namespace one.

probe:
source: 'import { makeCommandBus } from "@org/cqrs";'
symbol: makeCommandBus

See Probes.

fix: "subpath-namespace-import" rewrites a named import into a namespace import from the matching subpath:

// before
import { Effect, Layer } from "effect";
// after
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

It applies to the no-symbols form of the rule — the one that bans a binding form rather than particular names — since that is the case where the correct rewrite is mechanical.