Skip to content

Commit 84b5188

Browse files
committed
feat: better log routes tree
1 parent 93bab73 commit 84b5188

File tree

1 file changed

+172
-37
lines changed

1 file changed

+172
-37
lines changed
Lines changed: 172 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,190 @@
1+
/** biome-ignore-all lint/style/noNonNullAssertion: <explanation> */
12
import kleur from "kleur";
23
import type { ServerManifestRoute } from "types";
34
import { logger } from "./logger";
45

6+
type LogRouteKind = "static" | "static-dynamic" | "server-side";
7+
8+
type FlatRoute = Pick<ServerManifestRoute, "path"> & { kind: LogRouteKind };
9+
10+
type TrieNode = {
11+
name: string; // segment name ('' at root)
12+
fullPath: string; // accumulated path
13+
kind?: LogRouteKind; // only set on leaf route nodes
14+
children: Map<string, TrieNode>;
15+
};
16+
17+
const ICONS = {
18+
server: () => kleur.yellow("λ"),
19+
static: () => kleur.white("○"),
20+
staticDyn: () => kleur.white("●"),
21+
folder: () => kleur.white("┬"),
22+
};
23+
24+
const getKindIcon = (kind: LogRouteKind) =>
25+
kind === "server-side" ? ICONS.server() : kind === "static" ? ICONS.static() : ICONS.staticDyn();
26+
527
export function log_routes_tree(input_routes: ServerManifestRoute[]) {
6-
// Loggin Routes
28+
const flat = flattenRoutes(input_routes);
29+
30+
// Sort lexicographically for stable grouping.
31+
flat.sort((a, b) => a.path.localeCompare(b.path));
32+
33+
// Build trie.
34+
const root: TrieNode = { name: "", fullPath: "", children: new Map() };
35+
for (const r of flat) insertIntoTrie(root, r);
36+
37+
// Header.
738
logger.log(kleur.bold().blue().underline("Routes"));
39+
logger.log(".");
40+
41+
// Render.
42+
renderTrie(root, []);
843

9-
function buildTree(routes: ServerManifestRoute[]) {
10-
const tree: any = {};
11-
for (const route of routes) {
12-
const parts = route.path === "/" ? [] : route.path.split("/").filter(Boolean);
13-
let node = tree;
14-
for (const part of parts) {
15-
node.children = node.children || {};
16-
node.children[part] = node.children[part] || {};
17-
node = node.children[part];
44+
// Legend.
45+
logger.log("");
46+
logger.log(`${ICONS.server()} Server-side`);
47+
logger.log(`${ICONS.static()} Static`);
48+
logger.log(`${ICONS.staticDyn()} Static (pre-expanded variants)`);
49+
}
50+
51+
function flattenRoutes(input: ServerManifestRoute[]): FlatRoute[] {
52+
const out: FlatRoute[] = [];
53+
for (const r of input) {
54+
if (r.rendering_kind === "static" && r.static_generated_routes.length > 0) {
55+
for (const sr of r.static_generated_routes) {
56+
out.push({ path: sr.path, kind: "static-dynamic" });
1857
}
19-
node.route = route;
58+
} else {
59+
out.push({ path: r.path, kind: r.rendering_kind as LogRouteKind });
2060
}
21-
return tree;
2261
}
62+
return out;
63+
}
2364

24-
function printTree(node: any, prefix = "", isLast = true) {
25-
if (node.route) {
26-
const icon = node.route.rendering_kind === "static" ? "●" : kleur.yellow("λ");
27-
let extra = "";
28-
if (
29-
node.route.rendering_kind === "static" &&
30-
node.route.static_generated_routes &&
31-
node.route.static_generated_routes.length > 0
32-
) {
33-
extra = ` (${kleur.cyan(`${node.route.static_generated_routes.length}`)})`;
34-
}
35-
logger.log(
36-
`${prefix}${isLast ? "└-" : "├-"} ${icon} ${kleur.white(node.route.path)}${extra}`
37-
);
65+
function insertIntoTrie(root: TrieNode, r: FlatRoute) {
66+
const segments = r.path.split("/").filter(Boolean); // ignore leading '/'
67+
let node = root;
68+
let acc = "";
69+
for (let i = 0; i < segments.length; i++) {
70+
const seg = segments[i];
71+
acc = acc === "" ? `/${seg}` : `${acc}/${seg}`;
72+
if (!seg) {
73+
continue;
3874
}
39-
if (node.children) {
40-
const keys = Object.keys(node.children);
41-
keys.forEach((key, idx) => {
42-
printTree(node.children[key], prefix + (node.route ? "| " : ""), idx === keys.length - 1);
43-
});
75+
if (!node.children.has(seg)) {
76+
node.children.set(seg, { name: seg, fullPath: acc, children: new Map() });
4477
}
78+
node = node.children.get(seg)!;
79+
if (i === segments.length - 1) node.kind = r.kind;
4580
}
81+
}
4682

47-
const tree = buildTree(input_routes);
83+
/**
84+
* Render the trie using box-drawing characters.
85+
* Additionally, group numeric leaf siblings into compact ranges.
86+
*/
87+
function renderTrie(node: TrieNode, prefixBits: boolean[]) {
88+
// Collect children into an array for deterministic index access.
89+
const children = Array.from(node.children.values());
4890

49-
logger.log(".");
50-
printTree(tree, "", true);
91+
// Optional numeric range grouping applied at this directory level.
92+
const [numericLeaves, others] = partition(children, (c) => isNumericLeaf(c));
93+
const numericRanges = compressNumericLeaves(numericLeaves);
94+
95+
// Prepare final render list with synthetic "range nodes".
96+
type RenderItem = { label: string; icon: string; isLeaf: boolean; child?: TrieNode };
97+
const renderItems: RenderItem[] = [];
98+
99+
// Folders and non-numeric leaves first, sorted by type then name.
100+
others.sort(byFolderThenName).forEach((child) => {
101+
const isLeaf = child.kind != null && child.children.size === 0;
102+
renderItems.push({
103+
label: isLeaf ? kleur.white(child.name) : kleur.cyan(child.name),
104+
icon: isLeaf ? getKindIcon(child.kind!) : ICONS.folder(),
105+
isLeaf,
106+
child,
107+
});
108+
});
109+
110+
// Then numeric ranges, already compressed.
111+
for (const rng of numericRanges) {
112+
const label = rng.single ? `${rng.base}/${rng.single}` : `${rng.base}/${rng.start}${rng.end}`;
113+
renderItems.push({
114+
label: kleur.white(label.replace(node.fullPath || "", "").replace(/^\//, "")),
115+
icon: ICONS.staticDyn(), // numeric expansions typically come from static-dynamic
116+
isLeaf: true,
117+
child: undefined,
118+
});
119+
}
51120

52-
logger.log(`\n${kleur.yellow("λ")} Server-side Page`);
53-
logger.log("● Static Page");
54-
logger.log(`${kleur.cyan("(#)")} Count of Static Generated Routes\n`);
121+
// Emit lines.
122+
for (let i = 0; i < renderItems.length; i++) {
123+
const it = renderItems[i];
124+
const last = i === renderItems.length - 1;
125+
126+
if (!it) {
127+
continue;
128+
}
129+
130+
const branch = last ? "└─" : "├─";
131+
const guide = prefixBits.map((b) => (b ? "│ " : " ")).join("");
132+
133+
logger.log(`${guide}${branch}${it.icon} ${it.label}`);
134+
135+
// Recurse into directories.
136+
if (!it.isLeaf && it.child) {
137+
renderTrie(it.child, [...prefixBits, !last]);
138+
}
139+
}
140+
}
141+
142+
function byFolderThenName(a: TrieNode, b: TrieNode) {
143+
const aIsFolder = a.children.size > 0 && a.kind == null;
144+
const bIsFolder = b.children.size > 0 && b.kind == null;
145+
if (aIsFolder !== bIsFolder) return aIsFolder ? -1 : 1;
146+
return a.name.localeCompare(b.name);
147+
}
148+
149+
function partition<T>(arr: T[], pred: (x: T) => boolean): [T[], T[]] {
150+
const t: T[] = [],
151+
f: T[] = [];
152+
for (const x of arr) (pred(x) ? t : f).push(x);
153+
return [t, f];
154+
}
155+
156+
function isNumericLeaf(n: TrieNode): boolean {
157+
if (n.children.size !== 0) return false;
158+
if (n.kind == null) return false;
159+
return /^\d+$/.test(n.name);
160+
}
161+
162+
type NumericRange = { base: string; start: number; end: number; single?: number };
163+
164+
/**
165+
* Group numeric leaves like /posts/1, /posts/2, /posts/3 into compact ranges.
166+
* We assume all nodes share the same base = parent.fullPath.
167+
*/
168+
function compressNumericLeaves(nodes: TrieNode[]): NumericRange[] {
169+
if (nodes.length === 0) return [];
170+
// Base is parent path. All siblings share it.
171+
//@ts-expect-error
172+
const base = nodes[0].fullPath.replace(/\/\d+$/, "");
173+
const nums = nodes.map((n) => parseInt(n.name, 10)).sort((a, b) => a - b);
174+
175+
const ranges: NumericRange[] = [];
176+
let s = nums[0]!;
177+
let prev = nums[0]!;
178+
179+
for (let i = 1; i <= nums.length; i++) {
180+
const cur = nums[i]!;
181+
const contiguous = cur === prev + 1;
182+
if (!contiguous) {
183+
if (s === prev) ranges.push({ base, start: s, end: s, single: s });
184+
else ranges.push({ base, start: s, end: prev });
185+
s = cur;
186+
}
187+
prev = cur!;
188+
}
189+
return ranges;
55190
}

0 commit comments

Comments
 (0)