Skip to content

fix(guardrails): exempt typed resources+/actions+ data endpoints from no-restricted-paths#31

Merged
stevensacks merged 1 commit into
mainfrom
fix/route-import-boundary-data-endpoints
Jun 25, 2026
Merged

fix(guardrails): exempt typed resources+/actions+ data endpoints from no-restricted-paths#31
stevensacks merged 1 commit into
mainfrom
fix/route-import-boundary-data-endpoints

Conversation

@stevensacks

Copy link
Copy Markdown
Contributor

What

Exempt typed routes/resources+ and routes/actions+ data endpoints from the import-x/no-restricted-paths architecture boundary, for the UI layers only.

Why

import-x/no-restricted-paths cannot distinguish a type-only import. resources+ and actions+ are no-UI, typed data endpoints (direct children of app/routes/, siblings of _public+/_session+/_legal+) that the UI layer is explicitly meant to consume — the useFetcher<typeof action> pattern requires a UI component to import type {action} from a resources+/actions+ endpoint. Without a carve-out the rule flags that type-only edge as a boundary violation.

except resolves relative to each zone's from. Only routes contains actions+/resources+ subfolders, so listing them in except scopes the exemption to route imports of those endpoints. Real UI routes under _public+/_session+/_legal+ stay fully restricted.

Zones touched

except: ['actions+', 'resources+'] is added to exactly three zones — the UI layers:

  • pages zone — from: [./app/routes], target ./app/pages
  • components zone — from: [./app/routes, ./app/pages], target ./app/components
  • hooks/state zone — from: [./app/routes, ./app/pages, ./app/components], target [./app/hooks, ./app/state]

The same three messages gain Typed \resources+`/`actions+` data endpoints are exempt.`, and the function JSDoc documents the carve-out.

The services, utils, and types zones deliberately get no exemption, so the carve-out cannot leak past the UI layer into the data/leaf layers.

Verification

The repo has no test suite, so I ran a functional rule check: drove the built guardrails config (pulled live from the dist bundle, not a hand-copied replica) through ESLint's flat-config Linter against a fixture tree mirroring the real app/ layout, using the no-restricted-paths rule from eslint-plugin-import-x@4.16.2 with createNodeResolver and basePath set to the fixture root.

All assertions pass (5/5):

Edge Expected Result
component → routes/resources+ CLEAN PASS
component → routes/actions+ CLEAN PASS
component → routes/_public+ (real UI route) FLAGGED PASS
component → page FLAGGED PASS
service → routes/resources+ (no leak) FLAGGED PASS

pnpm build (tsup + tsc) and pnpm lint both pass clean.

🤖 Generated with Claude Code

… no-restricted-paths

import-x/no-restricted-paths cannot distinguish a type-only import, so it
flagged a UI component's `import type {action}` from a typed data endpoint
(the `useFetcher<typeof action>` pattern). routes/resources+ and
routes/actions+ are no-UI, typed data endpoints the UI layer is meant to
consume.

Add `except: ['actions+', 'resources+']` to the three UI zones (pages,
components, hooks/state). `except` resolves relative to each zone's `from`;
only `routes` contains those subfolders, so the exemption is scoped to route
imports of the endpoints. The services, utils, and types zones get no
exemption, so the carve-out stays within the UI layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@stevensacks stevensacks merged commit 178157f into main Jun 25, 2026
1 check passed
@stevensacks stevensacks deleted the fix/route-import-boundary-data-endpoints branch June 25, 2026 07:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant