setup-ts-deep-modules
Wire dependency-cruiser into a TypeScript repo so each package is a deep module, with implementation hidden in subfolders and reachable only through its entry-point files. User-invoked.
Setup TS Deep Modules - TypeScript Deep Module Boundary Configuration
Skill Overview
Setup TS Deep Modules integrates dependency-cruiser into a TypeScript repository and turns each package into a “deep module”—hiding substantial behavior in subfolders, exposing only a few entry-point files at the package root, and ensuring that out-of-bounds imports immediately cause checks to fail.
Applicable Scenarios
- Package boundary governance in a TypeScript monorepo: Packages may deep-import one another, and application code may deep-import packages, so changing one internal file requires globally searching for all call sites. This skill turns “only enter through entry points” into a machine-enforceable rule instead of relying on verbal agreements and code review.
- Teams using AI coding agents: Agents tend to import the most specific implementation file directly because it looks “most straightforward.” The four error-level rules provided by this skill block such deep imports during linting, effectively giving agents an automatically detectable red line.
- Making the design quality of each package measurable: The standard for a deep module is “a small interface, lots of behavior.” Entry points are the interface, and
lib/is the implementation. Once packages are organized this way, refactoring internal implementations no longer requires changing external callers, and packages are easier to test and replace independently.
Core Features
-
Environment detection and dependency installation: First determine whether the repository uses pnpm, yarn, bun, or npm by checking the lockfile. Then determine the package root directory (
src/packagesifsrc/exists, otherwisepackages), and use the corresponding package manager to install dependency-cruiser as a devDependency. If the repository already contains a.dependency-cruiser.*configuration file, the skill will not overwrite it. Instead, it merges in the four rules and options and tells you what was added. -
Write four boundary rules and integrate them into the check command: The generated
.dependency-cruiser.cjscontains four rules, all at the error level:- Entry-point boundary: Code outside a package (application code or another package) may import only that package’s entry points (its root-level files), and may not touch anything in its subfolders.
- Freedom within a package: Files within a package may import one another freely, without restrictions.
- Tests use entry points: Files under
tests/may import any package’s entry points and fixtures within their owntests/, but may not import implementation details from any package’s subfolders, including those of their own package. Cross-package integration tests are fine; deep imports are not. - Cycles prohibited: Dependency cycles are not allowed.
It also adds a
lint:boundariesscript and incorporates it into the repository’s existing typecheck aggregate command (check/ci/validate, etc.), without modifyingtsconfigor adding path aliases. -
Scaffold an example package and prove that the rules actually work: The skill creates a copyable and removable
examplepackage as a template. Its root-levelindex.tsis the entry point and delegates tolib/impl.ts(so it is genuinely “deep,” rather than merely a one-level passthrough), whiletests/example.test.tsimports only../index. It then runs three verification passes: the clean state must pass; temporarily addingimport { thing } from "../lib/impl"must fail and reporttests-through-entrypoints; after reverting the change, it must pass again. The skill is considered complete only after visibly confirming “pass → fail → pass.” Finally, it writes aREADME.mdunder the package directory explaining the convention (and explicitly discouraging barrel files), then adds a link to it from the repository’sCLAUDE.md/AGENTS.md.
Frequently Asked Questions
What is a deep module? How is it different from ordinary package splitting?
A deep module is one where “a substantial amount of behavior is hidden behind a very small interface.” Ordinary package splitting often only addresses where files are located, while the package’s public interface remains scattered—external code can import any file inside the package, meaning the internal structure effectively becomes part of the public API and can no longer be refactored freely. A deep module draws a firm boundary: the package’s root-level files are its entry points and therefore its public interface; everything in subfolders is an implementation detail and is inaccessible from outside. The result is a small, stable interface and an implementation that can be freely rewritten.
Why is the public interface “all files in the package root” rather than a single index.ts?
A single index.ts can easily devolve into a barrel file that re-exports every submodule. In that case, there is nominally only one entry point, but the actual public interface is as large as the entire subtree, eliminating the benefits of deep modules. This skill defines the public surface as every file directly under the package root, allowing a package to expose several small entry points (index.ts, client.ts, server.ts), each focused and compact. Conversely, re-exporting the entire tree in barrel-file style is explicitly discouraged. This also means that adding an entry point means adding a new root-level file; no configuration changes are needed.
Do I need to change the configuration when adding a new implementation subfolder?
No. Whether something is public or private is determined by path depth: root-level files in a package are entry points, and anything in a subfolder is private. The configuration does not hard-code lib/ or tests/; they are simply conventional folders (lib/ for implementations and tests/ for tests). Adding an internal/ or adapters/ folder likewise makes it private automatically. The configuration can simultaneously enforce “freedom within a package” and “restrictions from outside” through dependency-cruiser’s $1 grouping and reverse references, so it should not be split into one rule per package.
Usage Limitations
- Must be invoked manually: This skill is user-invoked and will not trigger automatically.
- Designed for existing package structures: It assumes that the repository is already (or will soon be) organized in a flat
<packages-root>/<name>/structure—each package root has only one level of direct subdirectories, and packages may not be nested inside other packages. If the repository follows a clearly different convention, the skill will first confirm where the package root should be placed. - Does not handle layering: Which packages may depend on which other packages is a separate concern. The configuration contains only a commented placeholder for the repository to fill in.
- Commands vary with pnpm / yarn / bun: The skill detects the package manager automatically, but it can recognize one only if a lockfile is present.