graftapp 0.2.0
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/dist/auth-QTBHG3BR.js +10 -0
- package/dist/chunk-FGCUN3GX.js +137 -0
- package/dist/chunk-IFQ6DFWT.js +30 -0
- package/dist/chunk-JAQFRDWK.js +30 -0
- package/dist/chunk-JKWWL7J3.js +182 -0
- package/dist/chunk-OSSL5LKZ.js +204 -0
- package/dist/chunk-SYWI4LAM.js +99 -0
- package/dist/chunk-XCGWPUXN.js +182 -0
- package/dist/config-DQLGHOXM.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +658 -0
- package/dist/next-7BRUNHDY.js +6 -0
- package/dist/next-SA36X7D6.js +6 -0
- package/dist/react-router-FHUEMDP5.js +6 -0
- package/dist/remix-BFPHQLFL.js +6 -0
- package/dist/vue-router-XRVQX7PZ.js +6 -0
- package/package.json +44 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/scanners/react-router.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
var ReactRouterScanner = class {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.cwd = process.cwd();
|
|
8
|
+
}
|
|
9
|
+
async scan() {
|
|
10
|
+
const routes = [];
|
|
11
|
+
const routerFiles = [
|
|
12
|
+
"src/router.ts",
|
|
13
|
+
"src/router.tsx",
|
|
14
|
+
"src/routes.ts",
|
|
15
|
+
"src/routes.tsx",
|
|
16
|
+
"src/router/index.ts",
|
|
17
|
+
"src/router/index.tsx",
|
|
18
|
+
"src/App.tsx",
|
|
19
|
+
"src/App.ts",
|
|
20
|
+
"src/app.tsx",
|
|
21
|
+
"src/app.ts"
|
|
22
|
+
];
|
|
23
|
+
for (const file of routerFiles) {
|
|
24
|
+
const filePath = join(this.cwd, file);
|
|
25
|
+
if (existsSync(filePath)) {
|
|
26
|
+
try {
|
|
27
|
+
const content = await readFile(filePath, "utf-8");
|
|
28
|
+
const extractedRoutes = this.extractRoutes(content);
|
|
29
|
+
routes.push(...extractedRoutes);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const uniqueRoutes = this.deduplicateRoutes(routes);
|
|
36
|
+
return {
|
|
37
|
+
routes: uniqueRoutes,
|
|
38
|
+
framework: "react-router"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extract routes from file content using regex patterns
|
|
43
|
+
* Looks for:
|
|
44
|
+
* - path: "/some/path"
|
|
45
|
+
* - path="/some/path"
|
|
46
|
+
* - path='/some/path'
|
|
47
|
+
* - <Route path="/some/path" />
|
|
48
|
+
*/
|
|
49
|
+
extractRoutes(content) {
|
|
50
|
+
const routes = [];
|
|
51
|
+
if (!this.usesReactRouter(content)) {
|
|
52
|
+
return routes;
|
|
53
|
+
}
|
|
54
|
+
const colonPattern = /path:\s*["']([^"']+)["']/g;
|
|
55
|
+
let match;
|
|
56
|
+
while ((match = colonPattern.exec(content)) !== null) {
|
|
57
|
+
const path = match[1];
|
|
58
|
+
if (this.isValidRoutePath(path)) {
|
|
59
|
+
routes.push({
|
|
60
|
+
path: this.normalizePath(path),
|
|
61
|
+
isDynamic: this.isDynamicRoute(path),
|
|
62
|
+
framework: "react-router"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const equalsPattern = /path\s*=\s*["']([^"']+)["']/g;
|
|
67
|
+
while ((match = equalsPattern.exec(content)) !== null) {
|
|
68
|
+
const path = match[1];
|
|
69
|
+
if (this.isValidRoutePath(path)) {
|
|
70
|
+
routes.push({
|
|
71
|
+
path: this.normalizePath(path),
|
|
72
|
+
isDynamic: this.isDynamicRoute(path),
|
|
73
|
+
framework: "react-router"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return routes;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Check if the file content uses react-router
|
|
81
|
+
*/
|
|
82
|
+
usesReactRouter(content) {
|
|
83
|
+
return content.includes("react-router-dom") || content.includes("react-router") || content.includes("createBrowserRouter") || content.includes("createHashRouter") || content.includes("BrowserRouter") || content.includes("HashRouter") || content.includes("<Route");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check if a path is a valid route path (not a variable or complex expression)
|
|
87
|
+
*/
|
|
88
|
+
isValidRoutePath(path) {
|
|
89
|
+
if (!path.startsWith("/")) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (path.includes("${") || path.includes("`")) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (path.includes(" ")) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Normalize the path to ensure consistency
|
|
102
|
+
*/
|
|
103
|
+
normalizePath(path) {
|
|
104
|
+
if (!path.startsWith("/")) {
|
|
105
|
+
path = "/" + path;
|
|
106
|
+
}
|
|
107
|
+
if (path.length > 1 && path.endsWith("/")) {
|
|
108
|
+
path = path.slice(0, -1);
|
|
109
|
+
}
|
|
110
|
+
return path;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Check if a route path contains dynamic segments
|
|
114
|
+
* React Router uses :param for dynamic segments
|
|
115
|
+
*/
|
|
116
|
+
isDynamicRoute(path) {
|
|
117
|
+
return path.includes(":") || path.includes("*");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Remove duplicate routes
|
|
121
|
+
*/
|
|
122
|
+
deduplicateRoutes(routes) {
|
|
123
|
+
const seen = /* @__PURE__ */ new Set();
|
|
124
|
+
const unique = [];
|
|
125
|
+
for (const route of routes) {
|
|
126
|
+
if (!seen.has(route.path)) {
|
|
127
|
+
seen.add(route.path);
|
|
128
|
+
unique.push(route);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return unique;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export {
|
|
136
|
+
ReactRouterScanner
|
|
137
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/utils/auth.ts
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { readFile, writeFile, unlink, mkdir } from "fs/promises";
|
|
5
|
+
var CONFIG_DIR = join(homedir(), ".config", "graft");
|
|
6
|
+
var AUTH_FILE = join(CONFIG_DIR, "auth.json");
|
|
7
|
+
async function saveAuth(auth) {
|
|
8
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
9
|
+
await writeFile(AUTH_FILE, JSON.stringify(auth, null, 2));
|
|
10
|
+
}
|
|
11
|
+
async function loadAuth() {
|
|
12
|
+
try {
|
|
13
|
+
const content = await readFile(AUTH_FILE, "utf-8");
|
|
14
|
+
return JSON.parse(content);
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function clearAuth() {
|
|
20
|
+
try {
|
|
21
|
+
await unlink(AUTH_FILE);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
saveAuth,
|
|
28
|
+
loadAuth,
|
|
29
|
+
clearAuth
|
|
30
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/utils/config.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
var CONFIG_FILE = "graft.config.json";
|
|
6
|
+
async function loadConfig() {
|
|
7
|
+
const configPath = join(process.cwd(), CONFIG_FILE);
|
|
8
|
+
if (!existsSync(configPath)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const content = await readFile(configPath, "utf-8");
|
|
13
|
+
return JSON.parse(content);
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function saveConfig(config) {
|
|
19
|
+
const configPath = join(process.cwd(), CONFIG_FILE);
|
|
20
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
21
|
+
}
|
|
22
|
+
function configExists() {
|
|
23
|
+
return existsSync(join(process.cwd(), CONFIG_FILE));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
loadConfig,
|
|
28
|
+
saveConfig,
|
|
29
|
+
configExists
|
|
30
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// src/scanners/next.ts
|
|
2
|
+
import { readdir } from "fs/promises";
|
|
3
|
+
import { join, sep } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
var NextScanner = class {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.cwd = process.cwd();
|
|
8
|
+
}
|
|
9
|
+
async scan() {
|
|
10
|
+
const routes = [];
|
|
11
|
+
const appDir = join(this.cwd, "app");
|
|
12
|
+
if (existsSync(appDir)) {
|
|
13
|
+
const appRoutes = await this.scanAppRouter(appDir);
|
|
14
|
+
routes.push(...appRoutes);
|
|
15
|
+
}
|
|
16
|
+
const pagesDir = join(this.cwd, "pages");
|
|
17
|
+
if (existsSync(pagesDir)) {
|
|
18
|
+
const pageRoutes = await this.scanPagesRouter(pagesDir);
|
|
19
|
+
routes.push(...pageRoutes);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
routes,
|
|
23
|
+
framework: "next"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Scan Next.js App Router (app directory)
|
|
28
|
+
* Looks for page.tsx, page.ts, page.jsx, page.js files
|
|
29
|
+
*/
|
|
30
|
+
async scanAppRouter(appDir) {
|
|
31
|
+
const routes = [];
|
|
32
|
+
await this.walkDirectory(appDir, async (filePath, relativePath) => {
|
|
33
|
+
const fileName = filePath.split(sep).pop() || "";
|
|
34
|
+
if (!fileName.match(/^page\.(tsx?|jsx?)$/)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const routePath = this.convertAppRouterPath(relativePath);
|
|
38
|
+
if (routePath) {
|
|
39
|
+
routes.push({
|
|
40
|
+
path: routePath,
|
|
41
|
+
isDynamic: this.isDynamicRoute(routePath),
|
|
42
|
+
framework: "next"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return routes;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Scan Next.js Pages Router (pages directory)
|
|
50
|
+
* Looks for .tsx, .ts, .jsx, .js files
|
|
51
|
+
*/
|
|
52
|
+
async scanPagesRouter(pagesDir) {
|
|
53
|
+
const routes = [];
|
|
54
|
+
await this.walkDirectory(pagesDir, async (filePath, relativePath) => {
|
|
55
|
+
const fileName = filePath.split(sep).pop() || "";
|
|
56
|
+
if (fileName.match(/^(_app|_document|_error)\.(tsx?|jsx?)$/)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (relativePath.startsWith("api" + sep) || relativePath.startsWith("api/")) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!fileName.match(/\.(tsx?|jsx?)$/)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const routePath = this.convertPagesRouterPath(relativePath);
|
|
66
|
+
if (routePath) {
|
|
67
|
+
routes.push({
|
|
68
|
+
path: routePath,
|
|
69
|
+
isDynamic: this.isDynamicRoute(routePath),
|
|
70
|
+
framework: "next"
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return routes;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Convert App Router file path to route path
|
|
78
|
+
* Examples:
|
|
79
|
+
* - "page.tsx" → "/"
|
|
80
|
+
* - "dashboard/page.tsx" → "/dashboard"
|
|
81
|
+
* - "users/[id]/page.tsx" → "/users/:id"
|
|
82
|
+
* - "blog/[...slug]/page.tsx" → "/blog/*"
|
|
83
|
+
* - "shop/[[...slug]]/page.tsx" → "/shop/*?"
|
|
84
|
+
* - "(auth)/login/page.tsx" → "/login" (route groups removed)
|
|
85
|
+
* - "@modal/photo/page.tsx" → skipped (parallel routes)
|
|
86
|
+
* - "_components/card.tsx" → skipped (private folders)
|
|
87
|
+
*/
|
|
88
|
+
convertAppRouterPath(relativePath) {
|
|
89
|
+
let path = relativePath.replace(/\/?page\.(tsx?|jsx?)$/, "");
|
|
90
|
+
const segments = path ? path.split(sep) : [];
|
|
91
|
+
const processedSegments = [];
|
|
92
|
+
for (const segment of segments) {
|
|
93
|
+
if (segment.startsWith("_")) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (segment.startsWith("@")) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
103
|
+
const param = segment.slice(1, -1);
|
|
104
|
+
if (param.startsWith("...")) {
|
|
105
|
+
processedSegments.push("*");
|
|
106
|
+
} else if (param.startsWith("[...") && param.endsWith("]")) {
|
|
107
|
+
processedSegments.push("*?");
|
|
108
|
+
} else {
|
|
109
|
+
processedSegments.push(":" + param);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
processedSegments.push(segment);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return "/" + processedSegments.join("/");
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Convert Pages Router file path to route path
|
|
119
|
+
* Examples:
|
|
120
|
+
* - "index.tsx" → "/"
|
|
121
|
+
* - "about.tsx" → "/about"
|
|
122
|
+
* - "blog/index.tsx" → "/blog"
|
|
123
|
+
* - "blog/[slug].tsx" → "/blog/:slug"
|
|
124
|
+
* - "blog/[...slug].tsx" → "/blog/*"
|
|
125
|
+
*/
|
|
126
|
+
convertPagesRouterPath(relativePath) {
|
|
127
|
+
let path = relativePath.replace(/\.(tsx?|jsx?)$/, "");
|
|
128
|
+
if (path === "index") {
|
|
129
|
+
return "/";
|
|
130
|
+
}
|
|
131
|
+
path = path.replace(/\/index$/, "");
|
|
132
|
+
const segments = path.split(sep);
|
|
133
|
+
const processedSegments = [];
|
|
134
|
+
for (const segment of segments) {
|
|
135
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
136
|
+
const param = segment.slice(1, -1);
|
|
137
|
+
if (param.startsWith("...")) {
|
|
138
|
+
processedSegments.push("*");
|
|
139
|
+
} else {
|
|
140
|
+
processedSegments.push(":" + param);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
processedSegments.push(segment);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return "/" + processedSegments.join("/");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if a route path contains dynamic segments
|
|
150
|
+
*/
|
|
151
|
+
isDynamicRoute(path) {
|
|
152
|
+
return path.includes(":") || path.includes("*");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Recursively walk a directory and call the callback for each file
|
|
156
|
+
*/
|
|
157
|
+
async walkDirectory(dir, callback) {
|
|
158
|
+
const walk = async (currentDir, basePath = "") => {
|
|
159
|
+
try {
|
|
160
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
const fullPath = join(currentDir, entry.name);
|
|
163
|
+
const relativePath = basePath ? join(basePath, entry.name) : entry.name;
|
|
164
|
+
if (entry.isDirectory()) {
|
|
165
|
+
await walk(fullPath, relativePath);
|
|
166
|
+
} else if (entry.isFile()) {
|
|
167
|
+
await callback(fullPath, relativePath);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof Error && "code" in error && error.code !== "ENOENT" && error.code !== "EACCES") {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
await walk(dir);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
NextScanner
|
|
182
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// src/scanners/vue-router.ts
|
|
2
|
+
import { readFile, readdir } from "fs/promises";
|
|
3
|
+
import { join, sep } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
var VueRouterScanner = class {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.cwd = process.cwd();
|
|
8
|
+
}
|
|
9
|
+
async scan() {
|
|
10
|
+
const routes = [];
|
|
11
|
+
const routerFiles = [
|
|
12
|
+
"src/router/index.ts",
|
|
13
|
+
"src/router/index.js",
|
|
14
|
+
"src/router.ts",
|
|
15
|
+
"src/router.js",
|
|
16
|
+
"router/index.ts",
|
|
17
|
+
"router/index.js"
|
|
18
|
+
];
|
|
19
|
+
let foundRouterConfig = false;
|
|
20
|
+
for (const file of routerFiles) {
|
|
21
|
+
const filePath = join(this.cwd, file);
|
|
22
|
+
if (existsSync(filePath)) {
|
|
23
|
+
try {
|
|
24
|
+
const content = await readFile(filePath, "utf-8");
|
|
25
|
+
const extractedRoutes = this.extractRoutesFromConfig(content);
|
|
26
|
+
routes.push(...extractedRoutes);
|
|
27
|
+
foundRouterConfig = true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!foundRouterConfig) {
|
|
34
|
+
const pagesRoutes = await this.scanPagesDirectory();
|
|
35
|
+
routes.push(...pagesRoutes);
|
|
36
|
+
}
|
|
37
|
+
const uniqueRoutes = this.deduplicateRoutes(routes);
|
|
38
|
+
return {
|
|
39
|
+
routes: uniqueRoutes,
|
|
40
|
+
framework: "vue"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract routes from Vue Router configuration file
|
|
45
|
+
* Looks for path: '/some/path' patterns
|
|
46
|
+
*/
|
|
47
|
+
extractRoutesFromConfig(content) {
|
|
48
|
+
const routes = [];
|
|
49
|
+
if (!this.usesVueRouter(content)) {
|
|
50
|
+
return routes;
|
|
51
|
+
}
|
|
52
|
+
const pathPattern = /path:\s*["'`]([^"'`]+)["'`]/g;
|
|
53
|
+
let match;
|
|
54
|
+
while ((match = pathPattern.exec(content)) !== null) {
|
|
55
|
+
const path = match[1];
|
|
56
|
+
if (this.isValidRoutePath(path)) {
|
|
57
|
+
routes.push({
|
|
58
|
+
path: this.normalizePath(path),
|
|
59
|
+
isDynamic: this.isDynamicRoute(path),
|
|
60
|
+
framework: "vue"
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return routes;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Scan Nuxt-style pages directory for routes
|
|
68
|
+
*/
|
|
69
|
+
async scanPagesDirectory() {
|
|
70
|
+
const routes = [];
|
|
71
|
+
const pagesDir = join(this.cwd, "pages");
|
|
72
|
+
if (!existsSync(pagesDir)) {
|
|
73
|
+
return routes;
|
|
74
|
+
}
|
|
75
|
+
await this.walkDirectory(pagesDir, async (filePath, relativePath) => {
|
|
76
|
+
const fileName = filePath.split(sep).pop() || "";
|
|
77
|
+
if (!fileName.match(/\.vue$/)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const routePath = this.convertNuxtPagePath(relativePath);
|
|
81
|
+
if (routePath) {
|
|
82
|
+
routes.push({
|
|
83
|
+
path: routePath,
|
|
84
|
+
isDynamic: this.isDynamicRoute(routePath),
|
|
85
|
+
framework: "vue"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return routes;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Convert Nuxt pages file path to route path
|
|
93
|
+
* Examples:
|
|
94
|
+
* - "index.vue" → "/"
|
|
95
|
+
* - "about.vue" → "/about"
|
|
96
|
+
* - "users/index.vue" → "/users"
|
|
97
|
+
* - "users/[id].vue" → "/users/:id"
|
|
98
|
+
* - "blog/[...slug].vue" → "/blog/*"
|
|
99
|
+
*/
|
|
100
|
+
convertNuxtPagePath(relativePath) {
|
|
101
|
+
let path = relativePath.replace(/\.vue$/, "");
|
|
102
|
+
if (path === "index") {
|
|
103
|
+
return "/";
|
|
104
|
+
}
|
|
105
|
+
path = path.replace(/\/index$/, "");
|
|
106
|
+
const segments = path.split(sep);
|
|
107
|
+
const processedSegments = [];
|
|
108
|
+
for (const segment of segments) {
|
|
109
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
110
|
+
const param = segment.slice(1, -1);
|
|
111
|
+
if (param.startsWith("...")) {
|
|
112
|
+
processedSegments.push("*");
|
|
113
|
+
} else {
|
|
114
|
+
processedSegments.push(":" + param);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
processedSegments.push(segment);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return "/" + processedSegments.join("/");
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if the file content uses vue-router
|
|
124
|
+
*/
|
|
125
|
+
usesVueRouter(content) {
|
|
126
|
+
return content.includes("vue-router") || content.includes("createRouter") || content.includes("VueRouter") || content.includes("RouteRecordRaw");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check if a path is a valid route path
|
|
130
|
+
*/
|
|
131
|
+
isValidRoutePath(path) {
|
|
132
|
+
if (!path.startsWith("/")) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (path.includes("${") || path.includes("`")) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (path.includes(" ")) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Normalize the path to ensure consistency
|
|
145
|
+
*/
|
|
146
|
+
normalizePath(path) {
|
|
147
|
+
if (!path.startsWith("/")) {
|
|
148
|
+
path = "/" + path;
|
|
149
|
+
}
|
|
150
|
+
if (path.length > 1 && path.endsWith("/")) {
|
|
151
|
+
path = path.slice(0, -1);
|
|
152
|
+
}
|
|
153
|
+
return path;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if a route path contains dynamic segments
|
|
157
|
+
* Vue Router uses :param for dynamic segments
|
|
158
|
+
*/
|
|
159
|
+
isDynamicRoute(path) {
|
|
160
|
+
return path.includes(":") || path.includes("*");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Remove duplicate routes
|
|
164
|
+
*/
|
|
165
|
+
deduplicateRoutes(routes) {
|
|
166
|
+
const seen = /* @__PURE__ */ new Set();
|
|
167
|
+
const unique = [];
|
|
168
|
+
for (const route of routes) {
|
|
169
|
+
if (!seen.has(route.path)) {
|
|
170
|
+
seen.add(route.path);
|
|
171
|
+
unique.push(route);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return unique;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Recursively walk a directory and call the callback for each file
|
|
178
|
+
*/
|
|
179
|
+
async walkDirectory(dir, callback) {
|
|
180
|
+
const walk = async (currentDir, basePath = "") => {
|
|
181
|
+
try {
|
|
182
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const fullPath = join(currentDir, entry.name);
|
|
185
|
+
const relativePath = basePath ? join(basePath, entry.name) : entry.name;
|
|
186
|
+
if (entry.isDirectory()) {
|
|
187
|
+
await walk(fullPath, relativePath);
|
|
188
|
+
} else if (entry.isFile()) {
|
|
189
|
+
await callback(fullPath, relativePath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof Error && "code" in error && error.code !== "ENOENT" && error.code !== "EACCES") {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
await walk(dir);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
VueRouterScanner
|
|
204
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/scanners/remix.ts
|
|
2
|
+
import { readdir } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
var RemixScanner = class {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.cwd = process.cwd();
|
|
8
|
+
}
|
|
9
|
+
async scan() {
|
|
10
|
+
const routes = [];
|
|
11
|
+
const routesDir = join(this.cwd, "app", "routes");
|
|
12
|
+
if (!existsSync(routesDir)) {
|
|
13
|
+
return {
|
|
14
|
+
routes,
|
|
15
|
+
framework: "remix"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const entries = await readdir(routesDir, { withFileTypes: true });
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (!entry.isFile()) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const fileName = entry.name;
|
|
25
|
+
if (!fileName.match(/\.(tsx?|jsx?)$/)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const routePath = this.convertRemixRoute(fileName);
|
|
29
|
+
if (routePath) {
|
|
30
|
+
routes.push({
|
|
31
|
+
path: routePath,
|
|
32
|
+
isDynamic: this.isDynamicRoute(routePath),
|
|
33
|
+
framework: "remix"
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return {
|
|
39
|
+
routes,
|
|
40
|
+
framework: "remix"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
routes,
|
|
45
|
+
framework: "remix"
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Convert Remix flat file route convention to route path
|
|
50
|
+
* Examples:
|
|
51
|
+
* - "_index.tsx" → "/"
|
|
52
|
+
* - "about.tsx" → "/about"
|
|
53
|
+
* - "dashboard.users.tsx" → "/dashboard/users"
|
|
54
|
+
* - "$userId.tsx" → "/:userId"
|
|
55
|
+
* - "blog.$slug.tsx" → "/blog/:slug"
|
|
56
|
+
* - "$.tsx" → "/*" (splat route)
|
|
57
|
+
* - "_layout.tsx" → null (layout file, not a route)
|
|
58
|
+
* - "posts._index.tsx" → "/posts"
|
|
59
|
+
*/
|
|
60
|
+
convertRemixRoute(fileName) {
|
|
61
|
+
let name = fileName.replace(/\.(tsx?|jsx?)$/, "");
|
|
62
|
+
if (name.startsWith("_") && name !== "_index") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (name === "_index") {
|
|
66
|
+
return "/";
|
|
67
|
+
}
|
|
68
|
+
const segments = name.split(".");
|
|
69
|
+
const processedSegments = [];
|
|
70
|
+
for (let i = 0; i < segments.length; i++) {
|
|
71
|
+
const segment = segments[i];
|
|
72
|
+
if (segment === "_index") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (segment === "$") {
|
|
76
|
+
processedSegments.push("*");
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (segment.startsWith("$")) {
|
|
80
|
+
const param = segment.slice(1);
|
|
81
|
+
processedSegments.push(":" + param);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
processedSegments.push(segment);
|
|
85
|
+
}
|
|
86
|
+
const path = "/" + processedSegments.join("/");
|
|
87
|
+
return path;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if a route path contains dynamic segments
|
|
91
|
+
*/
|
|
92
|
+
isDynamicRoute(path) {
|
|
93
|
+
return path.includes(":") || path.includes("*");
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
RemixScanner
|
|
99
|
+
};
|