Skip to content

Commit 73e0637

Browse files
committed
build manifest function
1 parent f342341 commit 73e0637

File tree

1 file changed

+199
-0
lines changed

1 file changed

+199
-0
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
4+
export type RouteInfo = {
5+
path: string;
6+
dynamic: boolean;
7+
pattern?: string;
8+
paramNames?: string[];
9+
};
10+
11+
export type RouteManifest = {
12+
dynamic: RouteInfo[];
13+
static: RouteInfo[];
14+
};
15+
16+
export type CreateRouteManifestOptions = {
17+
// For starters we only support app router
18+
appDirPath?: string;
19+
};
20+
21+
let manifestCache: RouteManifest | null = null;
22+
let lastAppDirPath: string | null = null;
23+
24+
function isPageFile(filename: string): boolean {
25+
return filename === 'page.tsx' || filename === 'page.jsx' || filename === 'page.ts' || filename === 'page.js';
26+
}
27+
28+
function isRouteGroup(name: string): boolean {
29+
return name.startsWith('(') && name.endsWith(')');
30+
}
31+
32+
function getDynamicRouteSegment(name: string): string {
33+
if (name.startsWith('[[...') && name.endsWith(']]')) {
34+
// Optional catchall: [[...param]]
35+
const paramName = name.slice(5, -2); // Remove [[... and ]]
36+
return `:${paramName}*?`; // Mark with ? as optional
37+
} else if (name.startsWith('[...') && name.endsWith(']')) {
38+
// Required catchall: [...param]
39+
const paramName = name.slice(4, -1); // Remove [... and ]
40+
return `:${paramName}*`;
41+
} else {
42+
// Regular dynamic: [param]
43+
return `:${name.slice(1, -1)}`;
44+
}
45+
}
46+
47+
function buildRegexForDynamicRoute(routePath: string): { pattern: string; paramNames: string[] } {
48+
const segments = routePath.split('/').filter(Boolean);
49+
const regexSegments: string[] = [];
50+
const paramNames: string[] = [];
51+
52+
for (const segment of segments) {
53+
if (segment.startsWith(':')) {
54+
const paramName = segment.substring(1);
55+
56+
if (paramName.endsWith('*?')) {
57+
// Optional catchall: matches zero or more segments
58+
const cleanParamName = paramName.slice(0, -2);
59+
paramNames.push(cleanParamName);
60+
regexSegments.push('(.*)');
61+
} else if (paramName.endsWith('*')) {
62+
// Required catchall: matches one or more segments
63+
const cleanParamName = paramName.slice(0, -1);
64+
paramNames.push(cleanParamName);
65+
regexSegments.push('(.+)');
66+
} else {
67+
// Regular dynamic segment
68+
paramNames.push(paramName);
69+
regexSegments.push('([^/]+)');
70+
}
71+
} else {
72+
// Static segment
73+
regexSegments.push(segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
74+
}
75+
}
76+
77+
const pattern = `^/${regexSegments.join('/')}$`;
78+
return { pattern, paramNames };
79+
}
80+
81+
function scanAppDirectory(dir: string, basePath: string = ''): RouteInfo[] {
82+
const routes: RouteInfo[] = [];
83+
84+
try {
85+
const entries = fs.readdirSync(dir, { withFileTypes: true });
86+
const pageFile = getHighestPriorityPageFile(entries);
87+
88+
if (pageFile) {
89+
const routePath = basePath || '/';
90+
const isDynamic = routePath.includes(':');
91+
92+
if (isDynamic) {
93+
const { pattern, paramNames } = buildRegexForDynamicRoute(routePath);
94+
routes.push({
95+
path: routePath,
96+
dynamic: true,
97+
pattern,
98+
paramNames,
99+
});
100+
} else {
101+
routes.push({
102+
path: routePath,
103+
dynamic: false,
104+
});
105+
}
106+
}
107+
108+
for (const entry of entries) {
109+
if (entry.isDirectory()) {
110+
const fullPath = path.join(dir, entry.name);
111+
112+
if (isRouteGroup(entry.name)) {
113+
// Route groups don't affect the URL, just scan them
114+
const subRoutes = scanAppDirectory(fullPath, basePath);
115+
routes.push(...subRoutes);
116+
continue;
117+
}
118+
119+
const isDynamic = entry.name.startsWith('[') && entry.name.endsWith(']');
120+
let routeSegment: string;
121+
122+
if (isDynamic) {
123+
routeSegment = getDynamicRouteSegment(entry.name);
124+
} else {
125+
routeSegment = entry.name;
126+
}
127+
128+
const newBasePath = `${basePath}/${routeSegment}`;
129+
const subRoutes = scanAppDirectory(fullPath, newBasePath);
130+
routes.push(...subRoutes);
131+
}
132+
}
133+
} catch (error) {
134+
// eslint-disable-next-line no-console
135+
console.warn('Error building route manifest:', error);
136+
}
137+
138+
return routes;
139+
}
140+
141+
function getHighestPriorityPageFile(entries: fs.Dirent[]): string | null {
142+
// Next.js precedence order: .tsx > .ts > .jsx > .js
143+
const pageFiles = entries.filter(entry => entry.isFile() && isPageFile(entry.name)).map(entry => entry.name);
144+
145+
if (pageFiles.length === 0) return null;
146+
147+
if (pageFiles.includes('page.tsx')) return 'page.tsx';
148+
if (pageFiles.includes('page.ts')) return 'page.ts';
149+
if (pageFiles.includes('page.jsx')) return 'page.jsx';
150+
if (pageFiles.includes('page.js')) return 'page.js';
151+
152+
return null;
153+
}
154+
155+
/**
156+
* Returns a route manifest for the given app directory
157+
*/
158+
export function createRouteManifest(options?: CreateRouteManifestOptions): RouteManifest {
159+
let targetDir: string | undefined;
160+
161+
if (options?.appDirPath) {
162+
targetDir = options.appDirPath;
163+
} else {
164+
const projectDir = process.cwd();
165+
const maybeAppDirPath = path.join(projectDir, 'app');
166+
const maybeSrcAppDirPath = path.join(projectDir, 'src', 'app');
167+
168+
if (fs.existsSync(maybeAppDirPath) && fs.lstatSync(maybeAppDirPath).isDirectory()) {
169+
targetDir = maybeAppDirPath;
170+
} else if (fs.existsSync(maybeSrcAppDirPath) && fs.lstatSync(maybeSrcAppDirPath).isDirectory()) {
171+
targetDir = maybeSrcAppDirPath;
172+
}
173+
}
174+
175+
if (!targetDir) {
176+
return {
177+
dynamic: [],
178+
static: [],
179+
};
180+
}
181+
182+
// Check if we can use cached version
183+
if (manifestCache && lastAppDirPath === targetDir) {
184+
return manifestCache;
185+
}
186+
187+
const routes = scanAppDirectory(targetDir);
188+
189+
const manifest: RouteManifest = {
190+
dynamic: routes.filter(route => route.dynamic),
191+
static: routes.filter(route => !route.dynamic),
192+
};
193+
194+
// set cache
195+
manifestCache = manifest;
196+
lastAppDirPath = targetDir;
197+
198+
return manifest;
199+
}

0 commit comments

Comments
 (0)