From f3182537c160fa7a0402e65dae6528dbe48d1fcf Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Thu, 21 May 2026 14:30:37 +0200 Subject: [PATCH] fix(boundaries): allow feature-to-feature imports of public exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit turbo.json's boundary config already allows `feature -> feature`, and the cross-feature event system depends on it — a consumer must import the publisher's event contract from `@repo/`. But the ESLint boundaries config, ADR-010, and AGENTS.md still declared `feature -> [core, tooling]`, contradicting turbo.json and the shipped code (marketing-pages imports @repo/auth). Align all three to turbo.json: a feature may import another feature's published public exports. Internals stay sealed by the `exports` map, and cross-feature behaviour still flows through IEventBus. --- AGENTS.md | 4 +-- docs/decisions/adr-010-turbo-boundaries.md | 41 +++++++++++++++++----- packages/core-eslint/base.js | 2 +- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d3a16ae..82cb1b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,7 @@ See `docs/guides/runbook.md` for the full workflow. | app | app, core, core-composition, feature, tooling | | core-composition | core, core-composition, feature, tooling | | core | core, core-composition, tooling | -| feature | core, tooling | +| feature | core, feature, tooling | | tooling | tooling | ### Composition exceptions @@ -81,7 +81,7 @@ No other cross-package boundary deviations are permitted. 2. **`exports` maps** — feature packages expose `.`, `./ui`, `./cms`, `./api`, `./di/bind-production`, `./di/bind-dev-seed` only; no deep source paths exist. 3. **ESLint `eslint-plugin-boundaries`** (lint-time) — configured in `packages/core-eslint/`: - Enforces the five-tag rules at linting - - Feature packages may import from `core` and tooling only. + - Feature packages may import from `core`, tooling, and other features' public exports (the `@repo/` contract barrel — e.g. an event contract a consumer subscribes to). They must not reach another feature's internals (the `exports` map seals those) or call its use cases directly — cross-feature behaviour flows through `IEventBus`. - `core-shared`, `core-ui` may not import any feature. - `core-api` restricted to `@repo//api` imports. - `core-cms` restricted to `@repo//cms` imports. diff --git a/docs/decisions/adr-010-turbo-boundaries.md b/docs/decisions/adr-010-turbo-boundaries.md index 648df86..6c7e9e6 100644 --- a/docs/decisions/adr-010-turbo-boundaries.md +++ b/docs/decisions/adr-010-turbo-boundaries.md @@ -38,6 +38,7 @@ ADR-006 mentioned three tags. This ADR refines the model to five, distinguishing - **tooling** — `packages/core-eslint`, `core-typescript` Why `core-trpc` is `core-composition`: + - `core-trpc` imports `@repo/core-api` (the tRPC app router) - `core-api` imports feature routers from `@repo//api` - Therefore, `core-trpc` transitively depends on features through the `AppRouter` type @@ -51,17 +52,40 @@ Why `core-trpc` is `core-composition`: { "boundaries": { "tags": { - "app": { "dependencies": { "allow": ["app", "core", "core-composition", "feature", "tooling"] } }, - "core-composition": { "dependencies": { "allow": ["core", "core-composition", "feature", "tooling"] } }, - "core": { "dependencies": { "allow": ["core", "core-composition", "tooling"] } }, - "feature": { "dependencies": { "allow": ["core", "tooling"] } }, + "app": { + "dependencies": { + "allow": ["app", "core", "core-composition", "feature", "tooling"] + } + }, + "core-composition": { + "dependencies": { + "allow": ["core", "core-composition", "feature", "tooling"] + } + }, + "core": { + "dependencies": { "allow": ["core", "core-composition", "tooling"] } + }, + "feature": { + "dependencies": { "allow": ["core", "feature", "tooling"] } + }, "tooling": { "dependencies": { "allow": ["tooling"] } } } } } ``` +> **Amendment (2026-05-21) — feature → feature.** The `feature` tag's allow-list +> includes `feature`. A feature package may depend on another feature's +> _published public exports_ — the `@repo/` contract barrel (types, +> schemas, errors, event contracts). This is required by the cross-feature +> event system (ADR-015): a consumer must import the publisher's event +> contract. Two guardrails remain: each feature's `exports` map seals its +> internals (only `.`, `./ui`, `./api`, `./cms`, `./di/bind-*` are reachable), +> and cross-feature _behaviour_ still flows through `IEventBus` — a feature +> never imports and invokes another feature's use cases directly. + 2. **Per-package `turbo.json`** declares the package's tag: + ```json // packages/blog/turbo.json { "extends": ["../../../turbo.json"], "tasks": { /* ... */ } } @@ -75,12 +99,13 @@ Why `core-trpc` is `core-composition`: Both ESLint and Turborepo enforce the same five-tag rules, but for different reasons: -| Layer | Runs | Sees | Exempts via | -|---|---|---|---| -| ESLint `eslint-plugin-boundaries` | lint-time | Direct imports per file | `// @boundaries-ignore` comments | -| Turborepo `boundaries` | build-graph time | Entire workspace dependency graph including transitives | None (graph-based, not per-import) | +| Layer | Runs | Sees | Exempts via | +| --------------------------------- | ---------------- | ------------------------------------------------------- | ---------------------------------- | +| ESLint `eslint-plugin-boundaries` | lint-time | Direct imports per file | `// @boundaries-ignore` comments | +| Turborepo `boundaries` | build-graph time | Entire workspace dependency graph including transitives | None (graph-based, not per-import) | **Why both?** + - **ESLint** provides fine-grained control (file-level exemptions) and immediate feedback during development - **Turborepo** catches transitive issues (e.g., feature reach through composition packages) and missing declarations diff --git a/packages/core-eslint/base.js b/packages/core-eslint/base.js index e1cd1cf..1c2fd36 100644 --- a/packages/core-eslint/base.js +++ b/packages/core-eslint/base.js @@ -96,7 +96,7 @@ export default [ from: "app", allow: ["app", "core", "core-composition", "feature", "tooling"], }, - { from: "feature", allow: ["core", "tooling"] }, + { from: "feature", allow: ["core", "feature", "tooling"] }, { from: "core", allow: ["core", "tooling"] }, { from: "core-composition", allow: ["core", "feature", "tooling"] }, { from: "tooling", allow: ["tooling"] },