fix(boundaries): allow feature-to-feature imports of public exports
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/<publisher>`. 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.
This commit is contained in:
@@ -64,7 +64,7 @@ See `docs/guides/runbook.md` for the full workflow.
|
|||||||
| app | app, core, core-composition, feature, tooling |
|
| app | app, core, core-composition, feature, tooling |
|
||||||
| core-composition | core, core-composition, feature, tooling |
|
| core-composition | core, core-composition, feature, tooling |
|
||||||
| core | core, core-composition, tooling |
|
| core | core, core-composition, tooling |
|
||||||
| feature | core, tooling |
|
| feature | core, feature, tooling |
|
||||||
| tooling | tooling |
|
| tooling | tooling |
|
||||||
|
|
||||||
### Composition exceptions
|
### 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.
|
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/`:
|
3. **ESLint `eslint-plugin-boundaries`** (lint-time) — configured in `packages/core-eslint/`:
|
||||||
- Enforces the five-tag rules at linting
|
- 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/<feature>` 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-shared`, `core-ui` may not import any feature.
|
||||||
- `core-api` restricted to `@repo/<feature>/api` imports.
|
- `core-api` restricted to `@repo/<feature>/api` imports.
|
||||||
- `core-cms` restricted to `@repo/<feature>/cms` imports.
|
- `core-cms` restricted to `@repo/<feature>/cms` imports.
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ ADR-006 mentioned three tags. This ADR refines the model to five, distinguishing
|
|||||||
- **tooling** — `packages/core-eslint`, `core-typescript`
|
- **tooling** — `packages/core-eslint`, `core-typescript`
|
||||||
|
|
||||||
Why `core-trpc` is `core-composition`:
|
Why `core-trpc` is `core-composition`:
|
||||||
|
|
||||||
- `core-trpc` imports `@repo/core-api` (the tRPC app router)
|
- `core-trpc` imports `@repo/core-api` (the tRPC app router)
|
||||||
- `core-api` imports feature routers from `@repo/<feature>/api`
|
- `core-api` imports feature routers from `@repo/<feature>/api`
|
||||||
- Therefore, `core-trpc` transitively depends on features through the `AppRouter` type
|
- Therefore, `core-trpc` transitively depends on features through the `AppRouter` type
|
||||||
@@ -51,17 +52,40 @@ Why `core-trpc` is `core-composition`:
|
|||||||
{
|
{
|
||||||
"boundaries": {
|
"boundaries": {
|
||||||
"tags": {
|
"tags": {
|
||||||
"app": { "dependencies": { "allow": ["app", "core", "core-composition", "feature", "tooling"] } },
|
"app": {
|
||||||
"core-composition": { "dependencies": { "allow": ["core", "core-composition", "feature", "tooling"] } },
|
"dependencies": {
|
||||||
"core": { "dependencies": { "allow": ["core", "core-composition", "tooling"] } },
|
"allow": ["app", "core", "core-composition", "feature", "tooling"]
|
||||||
"feature": { "dependencies": { "allow": ["core", "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"] } }
|
"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/<feature>` 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:
|
2. **Per-package `turbo.json`** declares the package's tag:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// packages/blog/turbo.json
|
// packages/blog/turbo.json
|
||||||
{ "extends": ["../../../turbo.json"], "tasks": { /* ... */ } }
|
{ "extends": ["../../../turbo.json"], "tasks": { /* ... */ } }
|
||||||
@@ -76,11 +100,12 @@ Why `core-trpc` is `core-composition`:
|
|||||||
Both ESLint and Turborepo enforce the same five-tag rules, but for different reasons:
|
Both ESLint and Turborepo enforce the same five-tag rules, but for different reasons:
|
||||||
|
|
||||||
| Layer | Runs | Sees | Exempts via |
|
| Layer | Runs | Sees | Exempts via |
|
||||||
|---|---|---|---|
|
| --------------------------------- | ---------------- | ------------------------------------------------------- | ---------------------------------- |
|
||||||
| ESLint `eslint-plugin-boundaries` | lint-time | Direct imports per file | `// @boundaries-ignore` comments |
|
| 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) |
|
| Turborepo `boundaries` | build-graph time | Entire workspace dependency graph including transitives | None (graph-based, not per-import) |
|
||||||
|
|
||||||
**Why both?**
|
**Why both?**
|
||||||
|
|
||||||
- **ESLint** provides fine-grained control (file-level exemptions) and immediate feedback during development
|
- **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
|
- **Turborepo** catches transitive issues (e.g., feature reach through composition packages) and missing declarations
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export default [
|
|||||||
from: "app",
|
from: "app",
|
||||||
allow: ["app", "core", "core-composition", "feature", "tooling"],
|
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", allow: ["core", "tooling"] },
|
||||||
{ from: "core-composition", allow: ["core", "feature", "tooling"] },
|
{ from: "core-composition", allow: ["core", "feature", "tooling"] },
|
||||||
{ from: "tooling", allow: ["tooling"] },
|
{ from: "tooling", allow: ["tooling"] },
|
||||||
|
|||||||
Reference in New Issue
Block a user