Files
vac-optimizer/.agents/skills/setup-ts-deep-modules/SKILL.md
T
2026-09-25 10:00:00 +02:00

7.3 KiB

name, description
name description
setup-ts-deep-modules Wire dependency-cruiser into a TypeScript repo so each package is a deep module — implementation hidden in subfolders, reachable only through its entry-point files. User-invoked.

Setup TS Deep Modules

Configure dependency-cruiser to enforce the requested package boundaries, then prove the rules detect a violation. Start from existing package layout and public exports. The bundled configuration is for flat packages whose root files are entry points and whose subfolders are private; adapt it when the repository uses another contract rather than restructuring the application to fit the template.

For the vocabulary (deep module, interface, seam, depth), run the /codebase-design skill — use its language throughout.

Bundled template's shape

src/packages/
  <name>/
    index.ts        ← an entry point (public). Import this from outside.
    client.ts       ← another entry point. Packages may expose SEVERAL.
    lib/            ← implementation: hidden from outside, free to import each other.
    tests/          ← co-located tests + fixtures (a subfolder, so private).

The public surface is the package's root files — not one designated index.ts. By convention implementation lives in lib/ and tests in tests/, giving every package the same two-folder shape. The rule itself is general, though: anything in any subfolder is private, so you never extend the config to add a folder.

Four rules, all error:

  1. Entry-point boundary — code outside a package (app code or another package) may import only that package's entry points (its root files), never anything in its subfolders.
  2. Intra-package freedom — a package's own files import each other freely.
  3. Tests through the entry points — files under <pkg>/tests/ may import any package's entry points and their own tests/ fixtures, but never any package's subfolder internals (not even their own). Integration tests across packages are fine; deep imports are not.
  4. No cycles — no dependency cycles.

Entry points, not a barrel. Because the public surface is every root file, a package can expose several small entry points (index.ts, client.ts, server.ts) instead of funnelling everything through one giant index.ts. Barrel files that re-export a whole subtree are discouraged — keep entry points small and hide implementation in subfolders.

Layering (which packages may depend on which) is a separate concern. Add those rules only for an actual requested architectural constraint.

Steps

1. Detect the environment

  • Package manager — inspect the owning package's packageManager and lockfile, including bun.lock or bun.lockb; preserve its tooling.
  • Packages root and contract — use existing package locations and exports. If establishing a new convention, propose the smallest suitable boundary and ask only when the choice materially affects the architecture.
  • Existing config — check for a .dependency-cruiser.* file. If one exists, do not overwrite it: merge the four rules and the options in, and tell the user what you added.

Done when: package manager, packages root, and existing-config status are all known.

2. Install dependency-cruiser

Reuse dependency-cruiser if installed; otherwise add it to the owning package's devDependencies with the detected package manager. Check its installed documentation for supported configuration before wiring rules.

Done when: dependency-cruiser is in devDependencies.

3. Write the config

Use dependency-cruiser.config.cjs as a starting point, merging existing configuration in place. Adapt package paths, public-entry rules, test locations, and tsConfig to the selected project. Do not assume changing PACKAGES_ROOT alone covers another layout.

Done when: .dependency-cruiser.cjs exists with the correct PACKAGES_ROOT, and the four forbidden rules are present.

4. Wire it into the checks

  • Add a lint:boundaries script: depcruise <packages-root> (or depcruise src).
  • Fold it into the repo's umbrella check command — the one that already runs typecheck (e.g. a check / ci / validate script). Do not touch tsconfig or add path aliases.
  • If there is no umbrella script, add lint:boundaries and tell the user to include it in CI.

Done when: the boundary check runs through the requested existing check command, or its standalone invocation and missing umbrella integration are documented.

5. Select a validation fixture

Use an existing representative package or a temporary task-owned fixture with:

  • index.ts — an entry point. Export one function that delegates to an internal file (so the package is visibly deep, not a pass-through).
  • lib/impl.ts — an internal file in a subfolder, imported by index.ts, not reachable from outside.
  • tests/example.test.ts — imports only ../index (an entry point), and asserts against the public function.

Keep the fixture isolated from user work. A permanent example package is optional and should be created only when requested.

Done when: the fixture exercises allowed entry-point access and a forbidden private import under the actual configured rules.

6. Prove the rules bite

This is the completion criterion for the whole skill — a config that doesn't fail on a violation is worthless.

  1. Run the boundary check on the selected fixture and record the allowed result.
  2. Add an intentional forbidden import in the isolated fixture; verify that the relevant rule reports it. Also exercise cross-package boundaries when those rules changed.
  3. Remove only the task's intentional violation and rerun. Remove a disposable fixture after validation; retain a regression check only where it adds ongoing value.

Done when: you have observed a pass, then a fail on the deep import, then a pass again. If step 2 does not fail, the rules are not wired correctly — fix before finishing.

7. Document the convention

Update the nearest existing package/contributor guidance with the selected public-entry convention and check command. Add a short local document only when no suitable one exists; do not require another README or a fixed two-folder layout.

Link that guidance from the applicable agent instructions when it is not already discoverable. Preserve the user's chosen agent product and file.

Done when: the actual convention and check command are discoverable, with no temporary violation or unrequested example package left behind.

Notes

  • The config's $1 back-references (dependency-cruiser's group matching) are what let a package reach its own internals while outsiders can't — don't flatten them into separate per-package rules.
  • Public vs private is decided by depth: a package's root files are entry points; anything in a subfolder is private. The conventional subfolders are lib/ (implementation) and tests/, but the rule doesn't hardcode them — any subfolder is private, so a new folder never needs a config change. Adding an entry point is just adding a root file — no barrel.
  • Packages are flat: one tier of immediate children under the root. A package's internals may nest as deep as you like; a package may not contain another package.
  • Use .cjs (not .js) so the config's module.exports works even in "type": "module" repos.