@bromscandium/vite-plugin 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/README.md +71 -71
- package/dist/codegen.js +21 -21
- package/package.json +43 -43
- package/src/codegen.ts +127 -127
- package/src/index.ts +9 -9
- package/src/plugin.ts +151 -151
- package/src/scanner.ts +283 -283
package/src/scanner.ts
CHANGED
|
@@ -1,283 +1,283 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File system scanner for discovering page routes.
|
|
3
|
-
* @module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Information about a scanned route from the file system.
|
|
11
|
-
*/
|
|
12
|
-
export interface ScannedRoute {
|
|
13
|
-
/** Absolute path to the page file */
|
|
14
|
-
filePath: string;
|
|
15
|
-
/** URL path for the route (e.g., '/users/:id') */
|
|
16
|
-
routePath: string;
|
|
17
|
-
/** Whether the route contains dynamic segments */
|
|
18
|
-
isDynamic: boolean;
|
|
19
|
-
/** Names of dynamic parameters in the route */
|
|
20
|
-
params: string[];
|
|
21
|
-
/** Whether this is a page component (page.tsx) */
|
|
22
|
-
isPage: boolean;
|
|
23
|
-
/** Whether this is a layout component */
|
|
24
|
-
isLayout: boolean;
|
|
25
|
-
/** Whether this is a catch-all route ([...slug]) */
|
|
26
|
-
isCatchAll: boolean;
|
|
27
|
-
/** URL path segments */
|
|
28
|
-
segments: string[];
|
|
29
|
-
/** Path to the layout file if one exists for this route */
|
|
30
|
-
layoutPath?: string;
|
|
31
|
-
/** File system relative path (includes route groups) */
|
|
32
|
-
fsPath: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const VALID_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
|
|
36
|
-
const IGNORED_FILES = ['_app', '_document', '_error', '404', '500'];
|
|
37
|
-
const IGNORED_PREFIXES = ['_', '.'];
|
|
38
|
-
|
|
39
|
-
const layoutsMap = new Map<string, string>();
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Scans a pages directory and returns all discovered routes.
|
|
43
|
-
* Supports Next.js-style file-based routing conventions including:
|
|
44
|
-
* - `page.tsx` files for page components
|
|
45
|
-
* - `layout.tsx` files for layout wrappers
|
|
46
|
-
* - `[param]` for dynamic route segments
|
|
47
|
-
* - `[...slug]` for catch-all routes
|
|
48
|
-
* - `(group)` folders for route grouping without URL segments
|
|
49
|
-
*
|
|
50
|
-
* @param pagesDir - The directory to scan for pages
|
|
51
|
-
* @returns An array of scanned route information, sorted by priority
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* ```ts
|
|
55
|
-
* const routes = scanPages('./src/pages');
|
|
56
|
-
* // Returns routes sorted: static > dynamic > catch-all
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
export function scanPages(pagesDir: string): ScannedRoute[] {
|
|
60
|
-
if (!fs.existsSync(pagesDir)) {
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
layoutsMap.clear();
|
|
65
|
-
|
|
66
|
-
scanForLayouts(pagesDir, '');
|
|
67
|
-
|
|
68
|
-
const routes: ScannedRoute[] = [];
|
|
69
|
-
scanDirectory(pagesDir, '', '', routes);
|
|
70
|
-
|
|
71
|
-
for (const route of routes) {
|
|
72
|
-
const layout = findLayoutForRoute(route.fsPath);
|
|
73
|
-
if (layout) {
|
|
74
|
-
route.layoutPath = layout;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return routes.sort((a, b) => {
|
|
79
|
-
if (a.isCatchAll !== b.isCatchAll) {
|
|
80
|
-
return a.isCatchAll ? 1 : -1;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (a.isDynamic !== b.isDynamic) {
|
|
84
|
-
return a.isDynamic ? 1 : -1;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const aSegments = a.routePath.split('/').filter(Boolean).length;
|
|
88
|
-
const bSegments = b.routePath.split('/').filter(Boolean).length;
|
|
89
|
-
if (aSegments !== bSegments) {
|
|
90
|
-
return aSegments - bSegments;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return a.routePath.localeCompare(b.routePath);
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function scanForLayouts(dir: string, fsRelativePath: string): void {
|
|
98
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
99
|
-
|
|
100
|
-
for (const entry of entries) {
|
|
101
|
-
const name = entry.name;
|
|
102
|
-
|
|
103
|
-
if (IGNORED_PREFIXES.some(prefix => name.startsWith(prefix))) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const fullPath = path.join(dir, name);
|
|
108
|
-
|
|
109
|
-
if (entry.isDirectory()) {
|
|
110
|
-
const newFsPath = fsRelativePath ? `${fsRelativePath}/${name}` : name;
|
|
111
|
-
scanForLayouts(fullPath, newFsPath);
|
|
112
|
-
} else if (entry.isFile()) {
|
|
113
|
-
const ext = path.extname(name);
|
|
114
|
-
if (!VALID_EXTENSIONS.includes(ext)) continue;
|
|
115
|
-
|
|
116
|
-
const baseName = path.basename(name, ext);
|
|
117
|
-
if (baseName === 'layout' || baseName === '_layout') {
|
|
118
|
-
const fsPath = '/' + fsRelativePath.replace(/\\/g, '/');
|
|
119
|
-
layoutsMap.set(fsPath.replace(/\/+/g, '/') || '/', fullPath.replace(/\\/g, '/'));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function findLayoutForRoute(fsPath: string): string | undefined {
|
|
126
|
-
let current = fsPath;
|
|
127
|
-
|
|
128
|
-
while (current) {
|
|
129
|
-
const layout = layoutsMap.get(current);
|
|
130
|
-
if (layout) return layout;
|
|
131
|
-
|
|
132
|
-
const lastSlash = current.lastIndexOf('/');
|
|
133
|
-
if (lastSlash <= 0) break;
|
|
134
|
-
current = current.substring(0, lastSlash);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return layoutsMap.get('/');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function isRouteGroup(name: string): boolean {
|
|
141
|
-
return name.startsWith('(') && name.endsWith(')');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function scanDirectory(
|
|
145
|
-
dir: string,
|
|
146
|
-
routeRelativePath: string,
|
|
147
|
-
fsRelativePath: string,
|
|
148
|
-
routes: ScannedRoute[]
|
|
149
|
-
): void {
|
|
150
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
151
|
-
|
|
152
|
-
for (const entry of entries) {
|
|
153
|
-
const name = entry.name;
|
|
154
|
-
|
|
155
|
-
if (IGNORED_PREFIXES.some(prefix => name.startsWith(prefix))) {
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const fullPath = path.join(dir, name);
|
|
160
|
-
|
|
161
|
-
if (entry.isDirectory()) {
|
|
162
|
-
const routeSegment = isRouteGroup(name) ? '' : name;
|
|
163
|
-
const newRoutePath = routeSegment
|
|
164
|
-
? (routeRelativePath ? `${routeRelativePath}/${routeSegment}` : routeSegment)
|
|
165
|
-
: routeRelativePath;
|
|
166
|
-
const newFsPath = fsRelativePath ? `${fsRelativePath}/${name}` : name;
|
|
167
|
-
scanDirectory(fullPath, newRoutePath, newFsPath, routes);
|
|
168
|
-
} else if (entry.isFile()) {
|
|
169
|
-
const ext = path.extname(name);
|
|
170
|
-
if (!VALID_EXTENSIONS.includes(ext)) {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const baseName = path.basename(name, ext);
|
|
175
|
-
|
|
176
|
-
if (IGNORED_FILES.includes(baseName)) {
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (baseName === '_layout' || baseName === 'layout') {
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const route = parseRoute(fullPath, routeRelativePath, fsRelativePath, baseName);
|
|
185
|
-
if (route) {
|
|
186
|
-
routes.push(route);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function parseRoute(
|
|
193
|
-
filePath: string,
|
|
194
|
-
routeRelativePath: string,
|
|
195
|
-
fsRelativePath: string,
|
|
196
|
-
baseName: string
|
|
197
|
-
): ScannedRoute | null {
|
|
198
|
-
const isPage = baseName === 'page';
|
|
199
|
-
const isLayout = baseName === '_layout' || baseName === 'layout';
|
|
200
|
-
|
|
201
|
-
let routePath = '/' + routeRelativePath.replace(/\\/g, '/');
|
|
202
|
-
|
|
203
|
-
if (!isPage) {
|
|
204
|
-
routePath = routePath === '/'
|
|
205
|
-
? `/${baseName}`
|
|
206
|
-
: `${routePath}/${baseName}`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const fsPath = '/' + fsRelativePath.replace(/\\/g, '/');
|
|
210
|
-
|
|
211
|
-
const params: string[] = [];
|
|
212
|
-
let isDynamic = false;
|
|
213
|
-
let isCatchAll = false;
|
|
214
|
-
|
|
215
|
-
routePath = routePath.replace(/\[\.\.\.(\w+)]/g, (_, paramName) => {
|
|
216
|
-
params.push(paramName);
|
|
217
|
-
isDynamic = true;
|
|
218
|
-
isCatchAll = true;
|
|
219
|
-
return `:${paramName}*`;
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
routePath = routePath.replace(/\[(\w+)]/g, (_, paramName) => {
|
|
223
|
-
params.push(paramName);
|
|
224
|
-
isDynamic = true;
|
|
225
|
-
return `:${paramName}`;
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
routePath = routePath.replace(/\/+/g, '/');
|
|
229
|
-
if (routePath.length > 1 && routePath.endsWith('/')) {
|
|
230
|
-
routePath = routePath.slice(0, -1);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const segments = routePath.split('/').filter(Boolean);
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
filePath: filePath.replace(/\\/g, '/'),
|
|
237
|
-
routePath,
|
|
238
|
-
isDynamic,
|
|
239
|
-
params,
|
|
240
|
-
isPage,
|
|
241
|
-
isLayout,
|
|
242
|
-
isCatchAll,
|
|
243
|
-
segments,
|
|
244
|
-
fsPath: fsPath.replace(/\/+/g, '/') || '/',
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Watches a pages directory for changes and invokes a callback when files change.
|
|
250
|
-
*
|
|
251
|
-
* @param pagesDir - The directory to watch
|
|
252
|
-
* @param onChange - Callback invoked when a page file changes
|
|
253
|
-
* @returns A function to stop watching
|
|
254
|
-
*
|
|
255
|
-
* @example
|
|
256
|
-
* ```ts
|
|
257
|
-
* const stop = watchPages('./src/pages', () => {
|
|
258
|
-
* console.log('Pages changed, regenerating routes...');
|
|
259
|
-
* });
|
|
260
|
-
*
|
|
261
|
-
* // Later: stop watching
|
|
262
|
-
* stop();
|
|
263
|
-
* ```
|
|
264
|
-
*/
|
|
265
|
-
export function watchPages(
|
|
266
|
-
pagesDir: string,
|
|
267
|
-
onChange: () => void
|
|
268
|
-
): () => void {
|
|
269
|
-
if (!fs.existsSync(pagesDir)) {
|
|
270
|
-
fs.mkdirSync(pagesDir, { recursive: true });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const watcher = fs.watch(pagesDir, { recursive: true }, (_eventType, filename) => {
|
|
274
|
-
if (!filename) return;
|
|
275
|
-
|
|
276
|
-
const ext = path.extname(filename);
|
|
277
|
-
if (VALID_EXTENSIONS.includes(ext)) {
|
|
278
|
-
onChange();
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
return () => watcher.close();
|
|
283
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* File system scanner for discovering page routes.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Information about a scanned route from the file system.
|
|
11
|
+
*/
|
|
12
|
+
export interface ScannedRoute {
|
|
13
|
+
/** Absolute path to the page file */
|
|
14
|
+
filePath: string;
|
|
15
|
+
/** URL path for the route (e.g., '/users/:id') */
|
|
16
|
+
routePath: string;
|
|
17
|
+
/** Whether the route contains dynamic segments */
|
|
18
|
+
isDynamic: boolean;
|
|
19
|
+
/** Names of dynamic parameters in the route */
|
|
20
|
+
params: string[];
|
|
21
|
+
/** Whether this is a page component (page.tsx) */
|
|
22
|
+
isPage: boolean;
|
|
23
|
+
/** Whether this is a layout component */
|
|
24
|
+
isLayout: boolean;
|
|
25
|
+
/** Whether this is a catch-all route ([...slug]) */
|
|
26
|
+
isCatchAll: boolean;
|
|
27
|
+
/** URL path segments */
|
|
28
|
+
segments: string[];
|
|
29
|
+
/** Path to the layout file if one exists for this route */
|
|
30
|
+
layoutPath?: string;
|
|
31
|
+
/** File system relative path (includes route groups) */
|
|
32
|
+
fsPath: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const VALID_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
|
|
36
|
+
const IGNORED_FILES = ['_app', '_document', '_error', '404', '500'];
|
|
37
|
+
const IGNORED_PREFIXES = ['_', '.'];
|
|
38
|
+
|
|
39
|
+
const layoutsMap = new Map<string, string>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Scans a pages directory and returns all discovered routes.
|
|
43
|
+
* Supports Next.js-style file-based routing conventions including:
|
|
44
|
+
* - `page.tsx` files for page components
|
|
45
|
+
* - `layout.tsx` files for layout wrappers
|
|
46
|
+
* - `[param]` for dynamic route segments
|
|
47
|
+
* - `[...slug]` for catch-all routes
|
|
48
|
+
* - `(group)` folders for route grouping without URL segments
|
|
49
|
+
*
|
|
50
|
+
* @param pagesDir - The directory to scan for pages
|
|
51
|
+
* @returns An array of scanned route information, sorted by priority
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const routes = scanPages('./src/pages');
|
|
56
|
+
* // Returns routes sorted: static > dynamic > catch-all
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function scanPages(pagesDir: string): ScannedRoute[] {
|
|
60
|
+
if (!fs.existsSync(pagesDir)) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
layoutsMap.clear();
|
|
65
|
+
|
|
66
|
+
scanForLayouts(pagesDir, '');
|
|
67
|
+
|
|
68
|
+
const routes: ScannedRoute[] = [];
|
|
69
|
+
scanDirectory(pagesDir, '', '', routes);
|
|
70
|
+
|
|
71
|
+
for (const route of routes) {
|
|
72
|
+
const layout = findLayoutForRoute(route.fsPath);
|
|
73
|
+
if (layout) {
|
|
74
|
+
route.layoutPath = layout;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return routes.sort((a, b) => {
|
|
79
|
+
if (a.isCatchAll !== b.isCatchAll) {
|
|
80
|
+
return a.isCatchAll ? 1 : -1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (a.isDynamic !== b.isDynamic) {
|
|
84
|
+
return a.isDynamic ? 1 : -1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const aSegments = a.routePath.split('/').filter(Boolean).length;
|
|
88
|
+
const bSegments = b.routePath.split('/').filter(Boolean).length;
|
|
89
|
+
if (aSegments !== bSegments) {
|
|
90
|
+
return aSegments - bSegments;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return a.routePath.localeCompare(b.routePath);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function scanForLayouts(dir: string, fsRelativePath: string): void {
|
|
98
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
99
|
+
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const name = entry.name;
|
|
102
|
+
|
|
103
|
+
if (IGNORED_PREFIXES.some(prefix => name.startsWith(prefix))) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const fullPath = path.join(dir, name);
|
|
108
|
+
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
const newFsPath = fsRelativePath ? `${fsRelativePath}/${name}` : name;
|
|
111
|
+
scanForLayouts(fullPath, newFsPath);
|
|
112
|
+
} else if (entry.isFile()) {
|
|
113
|
+
const ext = path.extname(name);
|
|
114
|
+
if (!VALID_EXTENSIONS.includes(ext)) continue;
|
|
115
|
+
|
|
116
|
+
const baseName = path.basename(name, ext);
|
|
117
|
+
if (baseName === 'layout' || baseName === '_layout') {
|
|
118
|
+
const fsPath = '/' + fsRelativePath.replace(/\\/g, '/');
|
|
119
|
+
layoutsMap.set(fsPath.replace(/\/+/g, '/') || '/', fullPath.replace(/\\/g, '/'));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function findLayoutForRoute(fsPath: string): string | undefined {
|
|
126
|
+
let current = fsPath;
|
|
127
|
+
|
|
128
|
+
while (current) {
|
|
129
|
+
const layout = layoutsMap.get(current);
|
|
130
|
+
if (layout) return layout;
|
|
131
|
+
|
|
132
|
+
const lastSlash = current.lastIndexOf('/');
|
|
133
|
+
if (lastSlash <= 0) break;
|
|
134
|
+
current = current.substring(0, lastSlash);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return layoutsMap.get('/');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isRouteGroup(name: string): boolean {
|
|
141
|
+
return name.startsWith('(') && name.endsWith(')');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function scanDirectory(
|
|
145
|
+
dir: string,
|
|
146
|
+
routeRelativePath: string,
|
|
147
|
+
fsRelativePath: string,
|
|
148
|
+
routes: ScannedRoute[]
|
|
149
|
+
): void {
|
|
150
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
151
|
+
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const name = entry.name;
|
|
154
|
+
|
|
155
|
+
if (IGNORED_PREFIXES.some(prefix => name.startsWith(prefix))) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fullPath = path.join(dir, name);
|
|
160
|
+
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
const routeSegment = isRouteGroup(name) ? '' : name;
|
|
163
|
+
const newRoutePath = routeSegment
|
|
164
|
+
? (routeRelativePath ? `${routeRelativePath}/${routeSegment}` : routeSegment)
|
|
165
|
+
: routeRelativePath;
|
|
166
|
+
const newFsPath = fsRelativePath ? `${fsRelativePath}/${name}` : name;
|
|
167
|
+
scanDirectory(fullPath, newRoutePath, newFsPath, routes);
|
|
168
|
+
} else if (entry.isFile()) {
|
|
169
|
+
const ext = path.extname(name);
|
|
170
|
+
if (!VALID_EXTENSIONS.includes(ext)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const baseName = path.basename(name, ext);
|
|
175
|
+
|
|
176
|
+
if (IGNORED_FILES.includes(baseName)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (baseName === '_layout' || baseName === 'layout') {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const route = parseRoute(fullPath, routeRelativePath, fsRelativePath, baseName);
|
|
185
|
+
if (route) {
|
|
186
|
+
routes.push(route);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseRoute(
|
|
193
|
+
filePath: string,
|
|
194
|
+
routeRelativePath: string,
|
|
195
|
+
fsRelativePath: string,
|
|
196
|
+
baseName: string
|
|
197
|
+
): ScannedRoute | null {
|
|
198
|
+
const isPage = baseName === 'page';
|
|
199
|
+
const isLayout = baseName === '_layout' || baseName === 'layout';
|
|
200
|
+
|
|
201
|
+
let routePath = '/' + routeRelativePath.replace(/\\/g, '/');
|
|
202
|
+
|
|
203
|
+
if (!isPage) {
|
|
204
|
+
routePath = routePath === '/'
|
|
205
|
+
? `/${baseName}`
|
|
206
|
+
: `${routePath}/${baseName}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const fsPath = '/' + fsRelativePath.replace(/\\/g, '/');
|
|
210
|
+
|
|
211
|
+
const params: string[] = [];
|
|
212
|
+
let isDynamic = false;
|
|
213
|
+
let isCatchAll = false;
|
|
214
|
+
|
|
215
|
+
routePath = routePath.replace(/\[\.\.\.(\w+)]/g, (_, paramName) => {
|
|
216
|
+
params.push(paramName);
|
|
217
|
+
isDynamic = true;
|
|
218
|
+
isCatchAll = true;
|
|
219
|
+
return `:${paramName}*`;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
routePath = routePath.replace(/\[(\w+)]/g, (_, paramName) => {
|
|
223
|
+
params.push(paramName);
|
|
224
|
+
isDynamic = true;
|
|
225
|
+
return `:${paramName}`;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
routePath = routePath.replace(/\/+/g, '/');
|
|
229
|
+
if (routePath.length > 1 && routePath.endsWith('/')) {
|
|
230
|
+
routePath = routePath.slice(0, -1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
filePath: filePath.replace(/\\/g, '/'),
|
|
237
|
+
routePath,
|
|
238
|
+
isDynamic,
|
|
239
|
+
params,
|
|
240
|
+
isPage,
|
|
241
|
+
isLayout,
|
|
242
|
+
isCatchAll,
|
|
243
|
+
segments,
|
|
244
|
+
fsPath: fsPath.replace(/\/+/g, '/') || '/',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Watches a pages directory for changes and invokes a callback when files change.
|
|
250
|
+
*
|
|
251
|
+
* @param pagesDir - The directory to watch
|
|
252
|
+
* @param onChange - Callback invoked when a page file changes
|
|
253
|
+
* @returns A function to stop watching
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* const stop = watchPages('./src/pages', () => {
|
|
258
|
+
* console.log('Pages changed, regenerating routes...');
|
|
259
|
+
* });
|
|
260
|
+
*
|
|
261
|
+
* // Later: stop watching
|
|
262
|
+
* stop();
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
export function watchPages(
|
|
266
|
+
pagesDir: string,
|
|
267
|
+
onChange: () => void
|
|
268
|
+
): () => void {
|
|
269
|
+
if (!fs.existsSync(pagesDir)) {
|
|
270
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const watcher = fs.watch(pagesDir, { recursive: true }, (_eventType, filename) => {
|
|
274
|
+
if (!filename) return;
|
|
275
|
+
|
|
276
|
+
const ext = path.extname(filename);
|
|
277
|
+
if (VALID_EXTENSIONS.includes(ext)) {
|
|
278
|
+
onChange();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return () => watcher.close();
|
|
283
|
+
}
|