@dcoder-x/plugin-shared 0.1.8 → 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;
@@ -131,6 +131,7 @@ class RouteExtractor {
131
131
  catch {
132
132
  continue;
133
133
  }
134
+ const importMap = this.buildImportMap(ast, filePath);
134
135
  traverse(ast, {
135
136
  JSXOpeningElement: (nodePath) => {
136
137
  const name = nodePath.node.name?.name;
@@ -138,22 +139,102 @@ class RouteExtractor {
138
139
  return;
139
140
  const pathAttr = nodePath.node.attributes.find((a) => a.name?.name === 'path');
140
141
  const routePath = pathAttr?.value?.value;
141
- if (routePath) {
142
- results.push({
143
- path: routePath,
144
- filePath,
145
- isDynamic: routePath.includes(':'),
146
- params: (routePath.match(/:(\w+)/g) || []).map((p) => p.slice(1)),
147
- layout: null,
148
- routerType: 'react-router',
149
- semantic: this.deriveSemanticMeaning(routePath),
150
- });
151
- }
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
+ });
152
154
  },
153
155
  });
154
156
  }
155
157
  return results;
156
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
+ }
157
238
  findFilesContaining(dir, pattern) {
158
239
  const results = [];
159
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
- const closeCharIndex = source.lastIndexOf('>', end - 1);
161
- if (closeCharIndex === -1)
162
- return null;
163
- return closeCharIndex;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcoder-x/plugin-shared",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",