The parser and manifest
Reading the tree, transforming segments, catching conflicts.
The Parser
The BionicJS routing engine does not rely on the browser to discover routes. Instead, a build-time parser reads the app/ directory and constructs a pure data tree called the RouteManifestNode.
The parser takes a flat list of file paths (the output of fast-glob) and converts them into a nested tree that mirrors the application's URL structure. Every segment in the filesystem becomes a node: directories become intermediate nodes, and page.tsx / layout.tsx files set the page and layout properties on the node they live in.
1const files = [
2 "/app/layout.tsx",
3 "/app/page.tsx",
4 "/app/about/page.tsx",
5 "/app/users/[id]/page.tsx",
6];
7
8const manifest = parseRoutes(files);
9// manifest.segment = "" (root)
10// manifest.layout = "/app/layout.tsx"
11// manifest.children[0].segment = "about"
12// manifest.children[0].page = "/app/about/page.tsx"
The manifest
The parser produces a RouteManifestNode - a pure data structure describing the routes. There are no JavaScript strings and no code generation at this stage; each node records the file that supplied its page and layout:
1interface RouteManifestNode {
2 segment: string; // "users", "[id]", ""
3 path?: string; // "users", ":id", ""
4 page?: string; // absolute file path
5 layout?: string; // absolute file path
6 children: RouteManifestNode[];
7}
The parser does not generate code. It produces a data structure. This separation means the tree can be inspected, validated, and transformed independently of any output format - a key principle in the BionicJS architecture.
Segment Transformations
Directory and file names are translated to route path segments using a set of deterministic rules. The conventions module defines these transformations:
| Filesystem | Node Segment | Route Path |
|---|---|---|
app/page.tsx | "" | index: true |
app/about/page.tsx | "about" | "about" |
app/users/[id]/page.tsx | "[id]" | ":id" |
app/docs/[...slug]/page.tsx | "[...slug]" | "*" |
Dynamic segments ([param]) become colon-prefixed parameters for React Router. Catch-all segments ([...slug]) become wildcards that match the rest of the URL. Static segments pass through unchanged.
1export function formatSegmentToRoutePath(segment: string): string {
2 if (isCatchAllSegment(segment)) return "*";
3 if (isDynamicSegment(segment)) return `:${segment.slice(1, -1)}`;
4 return segment;
5}
Conflict Detection
Because parsing happens at build time, BionicJS can detect routing conflicts before the code ever reaches the browser. If a developer accidentally creates both app/[id]/page.tsx and app/[userId]/page.tsx in the same directory, the parser throws a hard error. React Router would otherwise silently swallow this and pick one nondeterministically.
1BionicJS Route Conflict: Conflicting segments "[id]" and "[userId]"
2at the same directory level.
The conflict check runs at two levels: two dynamic segments at the same directory level (like [id] and [userId]), and two catch-all segments (like [...slug] and [...path]). Both are errors because they would produce overlapping route matches.
app/users/page.tsx (the index) and app/users/[id]/page.tsx (the dynamic route) are fine together - they match different URL patterns.05 / 25