@dcoder-x/plugin-shared 0.1.7 → 0.1.9
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.
|
@@ -7,6 +7,10 @@ export declare class RouteExtractor {
|
|
|
7
7
|
private walkPagesDir;
|
|
8
8
|
private walkTanStackDir;
|
|
9
9
|
private extractReactRouterRoutes;
|
|
10
|
+
private buildImportMap;
|
|
11
|
+
private resolveRouteComponentFile;
|
|
12
|
+
private collectJSXComponentNames;
|
|
13
|
+
private resolveImportToFile;
|
|
10
14
|
private findFilesContaining;
|
|
11
15
|
private extractParams;
|
|
12
16
|
private findNearestLayout;
|
|
@@ -6,6 +6,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.RouteExtractor = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const parser_1 = require("@babel/parser");
|
|
10
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
11
|
+
// @babel/traverse ships as CJS but may be loaded by an ESM host (e.g. Vite).
|
|
12
|
+
// The CJS-in-ESM import wraps exports in a namespace object, so `.default`
|
|
13
|
+
// may be the module object rather than the function. This guard handles both.
|
|
14
|
+
const traverse = typeof traverse_1.default === 'function' ? traverse_1.default : traverse_1.default.default;
|
|
9
15
|
class RouteExtractor {
|
|
10
16
|
constructor(dir) {
|
|
11
17
|
this.dir = dir;
|
|
@@ -114,42 +120,121 @@ class RouteExtractor {
|
|
|
114
120
|
async extractReactRouterRoutes(srcDir) {
|
|
115
121
|
if (!fs_1.default.existsSync(srcDir))
|
|
116
122
|
return [];
|
|
117
|
-
const { parse } = await import('@babel/parser');
|
|
118
|
-
const traverse = (await import('@babel/traverse')).default;
|
|
119
123
|
const results = [];
|
|
120
124
|
const routerFiles = this.findFilesContaining(srcDir, /createBrowserRouter|<Route/);
|
|
121
125
|
for (const filePath of routerFiles) {
|
|
122
126
|
const source = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
123
127
|
let ast;
|
|
124
128
|
try {
|
|
125
|
-
ast = parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
|
|
129
|
+
ast = (0, parser_1.parse)(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
|
|
126
130
|
}
|
|
127
131
|
catch {
|
|
128
132
|
continue;
|
|
129
133
|
}
|
|
134
|
+
const importMap = this.buildImportMap(ast, filePath);
|
|
130
135
|
traverse(ast, {
|
|
131
|
-
JSXOpeningElement(nodePath) {
|
|
136
|
+
JSXOpeningElement: (nodePath) => {
|
|
132
137
|
const name = nodePath.node.name?.name;
|
|
133
138
|
if (name !== 'Route')
|
|
134
139
|
return;
|
|
135
140
|
const pathAttr = nodePath.node.attributes.find((a) => a.name?.name === 'path');
|
|
136
141
|
const routePath = pathAttr?.value?.value;
|
|
137
|
-
if (routePath)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
if (!routePath)
|
|
143
|
+
return;
|
|
144
|
+
const resolvedFilePath = this.resolveRouteComponentFile(nodePath, importMap) ?? filePath;
|
|
145
|
+
results.push({
|
|
146
|
+
path: routePath,
|
|
147
|
+
filePath: resolvedFilePath,
|
|
148
|
+
isDynamic: routePath.includes(':'),
|
|
149
|
+
params: (routePath.match(/:(\w+)/g) || []).map((p) => p.slice(1)),
|
|
150
|
+
layout: null,
|
|
151
|
+
routerType: 'react-router',
|
|
152
|
+
semantic: this.deriveSemanticMeaning(routePath),
|
|
153
|
+
});
|
|
148
154
|
},
|
|
149
155
|
});
|
|
150
156
|
}
|
|
151
157
|
return results;
|
|
152
158
|
}
|
|
159
|
+
buildImportMap(ast, fromFile) {
|
|
160
|
+
const map = new Map();
|
|
161
|
+
for (const node of ast.program.body) {
|
|
162
|
+
if (node.type !== 'ImportDeclaration')
|
|
163
|
+
continue;
|
|
164
|
+
const importSource = node.source?.value;
|
|
165
|
+
if (!importSource)
|
|
166
|
+
continue;
|
|
167
|
+
const resolved = this.resolveImportToFile(importSource, fromFile);
|
|
168
|
+
if (!resolved)
|
|
169
|
+
continue;
|
|
170
|
+
for (const specifier of node.specifiers ?? []) {
|
|
171
|
+
if (specifier.type === 'ImportDefaultSpecifier' ||
|
|
172
|
+
specifier.type === 'ImportSpecifier') {
|
|
173
|
+
const localName = specifier.local?.name;
|
|
174
|
+
if (localName)
|
|
175
|
+
map.set(localName, resolved);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return map;
|
|
180
|
+
}
|
|
181
|
+
resolveRouteComponentFile(routeOpeningPath, importMap) {
|
|
182
|
+
const elementAttr = routeOpeningPath.node.attributes.find((a) => a.name?.name === 'element');
|
|
183
|
+
if (!elementAttr)
|
|
184
|
+
return null;
|
|
185
|
+
const expr = elementAttr.value?.expression;
|
|
186
|
+
if (!expr)
|
|
187
|
+
return null;
|
|
188
|
+
// Try each JSX component name in document order; first match in the import map wins.
|
|
189
|
+
// Wrapper components like <Can> or <RouteGuard> won't be in the map, so we fall
|
|
190
|
+
// through to the actual page component nested inside them.
|
|
191
|
+
const componentNames = this.collectJSXComponentNames(expr);
|
|
192
|
+
for (const componentName of componentNames) {
|
|
193
|
+
const resolved = importMap.get(componentName);
|
|
194
|
+
if (resolved)
|
|
195
|
+
return resolved;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
collectJSXComponentNames(node) {
|
|
200
|
+
if (!node)
|
|
201
|
+
return [];
|
|
202
|
+
if (node.type === 'JSXElement') {
|
|
203
|
+
const tagName = node.openingElement?.name?.name ?? '';
|
|
204
|
+
const names = [];
|
|
205
|
+
if (tagName && /^[A-Z]/.test(tagName))
|
|
206
|
+
names.push(tagName);
|
|
207
|
+
for (const child of node.children ?? []) {
|
|
208
|
+
names.push(...this.collectJSXComponentNames(child));
|
|
209
|
+
}
|
|
210
|
+
return names;
|
|
211
|
+
}
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
resolveImportToFile(importSource, fromFile) {
|
|
215
|
+
if (!importSource.startsWith('.') && !importSource.startsWith('/'))
|
|
216
|
+
return null;
|
|
217
|
+
const base = importSource.startsWith('/')
|
|
218
|
+
? path_1.default.normalize(importSource)
|
|
219
|
+
: path_1.default.resolve(path_1.default.dirname(fromFile), importSource);
|
|
220
|
+
const candidates = [
|
|
221
|
+
base,
|
|
222
|
+
`${base}.ts`,
|
|
223
|
+
`${base}.tsx`,
|
|
224
|
+
`${base}.js`,
|
|
225
|
+
`${base}.jsx`,
|
|
226
|
+
path_1.default.join(base, 'index.ts'),
|
|
227
|
+
path_1.default.join(base, 'index.tsx'),
|
|
228
|
+
path_1.default.join(base, 'index.js'),
|
|
229
|
+
path_1.default.join(base, 'index.jsx'),
|
|
230
|
+
];
|
|
231
|
+
for (const candidate of candidates) {
|
|
232
|
+
if (fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile()) {
|
|
233
|
+
return path_1.default.normalize(candidate);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
153
238
|
findFilesContaining(dir, pattern) {
|
|
154
239
|
const results = [];
|
|
155
240
|
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
@@ -157,10 +157,10 @@ function findAttributeInsertIndex(source, range) {
|
|
|
157
157
|
if (!range)
|
|
158
158
|
return null;
|
|
159
159
|
const end = range[1];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return
|
|
160
|
+
// Avoid searching backwards with lastIndexOf('>') — it hits '>' inside JSX expression
|
|
161
|
+
// containers (e.g. onClick={() => navigate('/')}). Use the AST range end directly.
|
|
162
|
+
const isSelfClosing = source[end - 2] === '/';
|
|
163
|
+
return isSelfClosing ? end - 2 : end - 1;
|
|
164
164
|
}
|
|
165
165
|
function applyEdits(source, edits) {
|
|
166
166
|
const sorted = edits.slice().sort((a, b) => b.index - a.index);
|