The routing engine/The parser and manifest

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.

parseRoutes input/output
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:

RouteManifestNode
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:

FilesystemNode SegmentRoute 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.

Segment formatting
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.

Conflict thrown
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.

ⓘ
Static files alongside dynamic segments are always allowed. app/users/page.tsx (the index) and app/users/[id]/page.tsx (the dynamic route) are fine together - they match different URL patterns.