@devfellowship/ux-paths-routes
v0.1.0
Published
The DFL UX Paths route extractor: read an app repo's routing (a React-Router <Routes> tree, an Expo Router or Next app-router file tree) and emit the routes it serves, each with the source file that renders it.
Maintainers
Readme
@devfellowship/ux-paths-routes
Read an app repo's routing and print the routes it serves — each with the source file that renders it. No schema, no validator, no flows tooling: the route extractor and what the extractor needs.
npm install @devfellowship/ux-paths-routes
npx ux-paths-routes . --convention react-router --format lines/
/admin
/admin/completion
…What it reads
Three conventions, auto-detected in this order:
| Convention | How the route is found |
|---|---|
| Expo Router | the file path — app/profile/settings.tsx → /profile/settings |
| Next.js app router | the file path of a page.* file — app/studio/[id]/page.tsx → /studio/:id |
| React Router | the SOURCE is parsed — the route lives in a JSX expression, not in the path |
The React-Router adapter uses the TypeScript compiler's own parser, never a
regex and never a model. It reads nested and layout routes,
element={<RequireAuth><Page/></RequireAuth>} (the page is the innermost
component; the wrappers become guards[]), lazy(() => import('./P')) as well
as static imports, <Route index>, :param, the * catch-all,
path={SOME_CONST} where the constant is a string literal in this file or
exported by another module, <Navigate> (reported separately — a redirect
renders no screen), and barrel re-exports (import { Auth } from '../pages' is
followed to the file that defines Auth, because that is the file the screen
must point at).
A regex gets most of this wrong in ways that produce a WRONG answer rather than
a missing one. Children of <Route path="/admin"> are declared with RELATIVE
paths, so a grep for path="…" reports roster where the app serves
/admin/roster. And path={GUEST_ROUTE_PATH} is an identifier, so a grep skips
it silently and never notices the route disappearing.
What it never does: guess
- A route whose COMPONENT cannot be resolved comes back with
component_file: nulland anunresolved_reason. It is never dropped: the app serves that URL either way, and the route list stays correct. - A route whose PATH cannot be read is worse —
routethen holds the parent's path, which is wrong rather than incomplete. That entry carriespath_unreadable: true, and--format linesrefuses on it (exit 2) rather than print it. --format linesalso refuses an empty list. A consumer that compares this output against a declared route set reads "no routes" as "every declared route is stale".
API
import { resolveRoutes } from '@devfellowship/ux-paths-routes';
const result = resolveRoutes('/path/to/app', { convention: 'react-router' });
// result.convention 'react-router' | 'expo-router' | 'next-app-router' | 'unknown'
// result.router_file the file the <Routes> tree was read from
// result.routes[] { route, component_file, guards?, index?, layout?, … }
// result.redirects[] <Navigate> routes — declared, but they render no screen
// result.unresolved[] the subset of routes[] carrying an unresolved_reasonThe package is ESM-only. Do not reach for it with
createRequire(…).resolve('@devfellowship/ux-paths-routes') — an ESM-only
package with an exports map has no require condition, and Node answers
ERR_PACKAGE_PATH_NOT_EXPORTED, which reads like a broken install. Use the bin,
or await import(…). ./package.json IS exported, so a caller that needs to
locate the install can read the manifest.
CLI
ux-paths-routes [repoPath] [options]
--convention <name> auto | expo-router | next-app-router | react-router
--router-file <path> React Router only: repo-relative file holding <Routes>
--format <name> json (default) | lines
--pretty JSON only: indent
--full JSON only: the whole result, not just routes[]Exit codes: 0 ok, 1 the arguments or the repo could not be read, 2
(--format lines) the routing was read but the answer would be wrong or empty.
Notes, warnings and the unresolved list go to stderr, so stdout stays exactly the routes.
In CI
This is what a ux-paths guard calls to learn the routes the code actually serves:
- name: Extract the routes the code actually serves
run: |
set -euo pipefail
mkdir -p /tmp/ux-paths-tools && cd /tmp/ux-paths-tools
npm init -y >/dev/null 2>&1
npm install --no-save --no-audit --no-fund @devfellowship/ux-paths-routes@^0.1.0
cd "$GITHUB_WORKSPACE"
/tmp/ux-paths-tools/node_modules/.bin/ux-paths-routes . \
--convention react-router --format lines > "$RUNNER_TEMP/routes.txt"A directory of its own, rather than the repo's node_modules, is deliberate: a
blocking gate that needs a full application install (and the token that install
needs) ends up permanently red for reasons that have nothing to do with the map.
Dependencies
One: typescript, the JSX parser. It is loaded on first use, not at import
time, so a caller that only resolves an Expo Router tree never pays for it.
Where it lives
Source: devfellowship/dfl-ux-paths,
directory routes/. The dfl-ux-paths CLI re-exports this package rather than
keeping a copy, so there is one implementation of the adapter and one test suite
behind it.
Its sibling @devfellowship/ux-paths-spec carries the other half — the v1 JSON
Schema, the types generated from it, and a zero-dependency validate().
MIT.
