@dcoder-x/plugin-shared 0.1.15 → 0.1.16
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.
|
@@ -12,6 +12,24 @@ const InteractionGraphExtractor_1 = require("./InteractionGraphExtractor");
|
|
|
12
12
|
const ComponentContextResolver_1 = require("./ComponentContextResolver");
|
|
13
13
|
const HtmlTagGuards_1 = require("../injection/HtmlTagGuards");
|
|
14
14
|
const IdStrategy_1 = require("../injection/IdStrategy");
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Content-based structural file detection (Options A and B)
|
|
17
|
+
// These mirror the helpers in RouteExtractor but live here so ComponentExtractor
|
|
18
|
+
// can run them independently without importing across extractor boundaries.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
function looksLikeRouterConfigContent(content) {
|
|
21
|
+
return /createBrowserRouter|createHashRouter|createMemoryRouter/.test(content) ||
|
|
22
|
+
/<Routes[\s>]/.test(content);
|
|
23
|
+
}
|
|
24
|
+
function looksLikeOutletOnlyWrapper(content) {
|
|
25
|
+
if (!/<Outlet[\s/>]/.test(content))
|
|
26
|
+
return false;
|
|
27
|
+
const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
28
|
+
const componentTags = stripped.match(/<([A-Z][A-Za-z0-9]*)/g) ?? [];
|
|
29
|
+
const uniqueTags = new Set(componentTags.map((t) => t.slice(1)));
|
|
30
|
+
const structural = new Set(['Outlet', 'Suspense', 'Fragment', 'ErrorBoundary', 'StrictMode']);
|
|
31
|
+
return [...uniqueTags].filter((t) => !structural.has(t)).length === 0;
|
|
32
|
+
}
|
|
15
33
|
/**
|
|
16
34
|
* A route is a usable module-graph entry point only when its filePath
|
|
17
35
|
* points to an actual page component, not a layout/guard/wrapper/router file.
|
|
@@ -30,6 +48,18 @@ function isPageRoute(route) {
|
|
|
30
48
|
if (/(provider|context|guard|layout|redirect|catchall|catch-all)/.test(base) ||
|
|
31
49
|
/^(router|routes|routerconfig|approuter|routesconfig|routingconfig|routeconfig|approviders|appwrapper)$/i.test(base))
|
|
32
50
|
return false;
|
|
51
|
+
// Option A: content-based — file defines routes, it is not a page
|
|
52
|
+
// Option B: file only renders <Outlet>, it is a pure layout wrapper
|
|
53
|
+
try {
|
|
54
|
+
const content = fs_1.default.readFileSync(route.filePath, 'utf-8');
|
|
55
|
+
if (looksLikeRouterConfigContent(content))
|
|
56
|
+
return false;
|
|
57
|
+
if (looksLikeOutletOnlyWrapper(content))
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// file unreadable — fall through to default true
|
|
62
|
+
}
|
|
33
63
|
return true;
|
|
34
64
|
}
|
|
35
65
|
class ComponentExtractor {
|
|
@@ -20,8 +20,11 @@ const traverse = typeof traverse_1.default === 'function' ? traverse_1.default :
|
|
|
20
20
|
* 3 = pages/views/screens directory → definitely a page
|
|
21
21
|
* 1 = unknown / ambiguous
|
|
22
22
|
* 0 = structural file (provider, guard, layout, context, router config, …)
|
|
23
|
+
*
|
|
24
|
+
* routerFiles: optional set of files known to contain createBrowserRouter/<Route
|
|
25
|
+
* (Option A — content-based router config detection)
|
|
23
26
|
*/
|
|
24
|
-
function scoreFilePath(filePath) {
|
|
27
|
+
function scoreFilePath(filePath, routerFiles) {
|
|
25
28
|
const p = filePath.replace(/\\/g, '/').toLowerCase();
|
|
26
29
|
const base = path_1.default.basename(p).replace(/\.(tsx?|jsx?)$/, '');
|
|
27
30
|
if (p.includes('/pages/') || p.includes('/views/') || p.includes('/screens/'))
|
|
@@ -33,8 +36,35 @@ function scoreFilePath(filePath) {
|
|
|
33
36
|
if (/(provider|context|guard|layout|redirect|catchall|catch-all)/.test(base) ||
|
|
34
37
|
/^(router|routes|routerconfig|approuter|routesconfig|routingconfig|routeconfig|approviders|appwrapper)$/i.test(base))
|
|
35
38
|
return 0;
|
|
39
|
+
// Option A: content-based — this file defines routes, it is not a page
|
|
40
|
+
if (routerFiles?.has(path_1.default.normalize(filePath)))
|
|
41
|
+
return 0;
|
|
36
42
|
return 1;
|
|
37
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Option A: returns true if the file content contains router definition patterns.
|
|
46
|
+
* Used to populate the routerFiles set passed to scoreFilePath().
|
|
47
|
+
*/
|
|
48
|
+
function looksLikeRouterConfigContent(content) {
|
|
49
|
+
return /createBrowserRouter|createHashRouter|createMemoryRouter/.test(content) ||
|
|
50
|
+
/<Routes[\s>]/.test(content);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Option B: returns true if the file renders only <Outlet> with no other
|
|
54
|
+
* substantive JSX component tags. These are pure layout wrappers.
|
|
55
|
+
*/
|
|
56
|
+
function looksLikeOutletOnlyWrapper(content) {
|
|
57
|
+
if (!/<Outlet[\s/>]/.test(content))
|
|
58
|
+
return false;
|
|
59
|
+
// Strip comments and string literals (rough), then count distinct JSX component tags
|
|
60
|
+
const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
61
|
+
const componentTags = stripped.match(/<([A-Z][A-Za-z0-9]*)/g) ?? [];
|
|
62
|
+
const uniqueTags = new Set(componentTags.map((t) => t.slice(1)));
|
|
63
|
+
// Allow Outlet plus a small set of structural primitives (Suspense, Fragment, ErrorBoundary)
|
|
64
|
+
const structural = new Set(['Outlet', 'Suspense', 'Fragment', 'ErrorBoundary', 'StrictMode']);
|
|
65
|
+
const nonStructural = [...uniqueTags].filter((t) => !structural.has(t));
|
|
66
|
+
return nonStructural.length === 0;
|
|
67
|
+
}
|
|
38
68
|
// ---------------------------------------------------------------------------
|
|
39
69
|
// RouteExtractor
|
|
40
70
|
// ---------------------------------------------------------------------------
|
|
@@ -150,6 +180,10 @@ class RouteExtractor {
|
|
|
150
180
|
return [];
|
|
151
181
|
const results = [];
|
|
152
182
|
const routerFiles = this.findFilesContaining(srcDir, /createBrowserRouter|<Route/);
|
|
183
|
+
// Option A: build a normalised set of all files that contain router-definition
|
|
184
|
+
// patterns. scoreFilePath() uses this to score any such file as 0 regardless
|
|
185
|
+
// of its filename, so arbitrarily-named router config files are excluded.
|
|
186
|
+
const routerFileSet = new Set(routerFiles.map((f) => path_1.default.normalize(f)));
|
|
153
187
|
for (const filePath of routerFiles) {
|
|
154
188
|
const source = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
155
189
|
let ast;
|
|
@@ -162,7 +196,7 @@ class RouteExtractor {
|
|
|
162
196
|
const importMap = this.buildImportMap(ast, filePath);
|
|
163
197
|
// Walk the entire AST looking for JSX Route trees and
|
|
164
198
|
// createBrowserRouter([…]) / createHashRouter([…]) call expressions.
|
|
165
|
-
this.extractFromAst(ast, filePath, importMap, results);
|
|
199
|
+
this.extractFromAst(ast, filePath, importMap, results, routerFileSet);
|
|
166
200
|
}
|
|
167
201
|
// Deduplicate: same (path + filePath) pair can appear when multiple router
|
|
168
202
|
// files re-export or re-nest the same routes.
|
|
@@ -182,7 +216,7 @@ class RouteExtractor {
|
|
|
182
216
|
* 2. createBrowserRouter / createHashRouter call arguments — delegates to
|
|
183
217
|
* walkObjectRouteConfig which handles the object-literal route config style.
|
|
184
218
|
*/
|
|
185
|
-
extractFromAst(ast, filePath, importMap, results) {
|
|
219
|
+
extractFromAst(ast, filePath, importMap, results, routerFileSet) {
|
|
186
220
|
// Track which JSX Route nodes we have already processed as children of a
|
|
187
221
|
// parent Route so we don't double-emit them from the flat visitor pass.
|
|
188
222
|
const processedNodes = new WeakSet();
|
|
@@ -202,7 +236,7 @@ class RouteExtractor {
|
|
|
202
236
|
const routeObjects = firstArg.type === 'ArrayExpression' ? firstArg.elements : [];
|
|
203
237
|
for (const el of routeObjects) {
|
|
204
238
|
if (el)
|
|
205
|
-
this.walkObjectRouteConfig(el, '', filePath, importMap, results);
|
|
239
|
+
this.walkObjectRouteConfig(el, '', filePath, importMap, results, routerFileSet);
|
|
206
240
|
}
|
|
207
241
|
},
|
|
208
242
|
// JSX style: <Routes><Route path="…" element={…}>…</Route></Routes>
|
|
@@ -221,7 +255,7 @@ class RouteExtractor {
|
|
|
221
255
|
const grandparentOpening = parentJsx?.openingElement?.name?.name;
|
|
222
256
|
if (grandparentOpening === 'Route')
|
|
223
257
|
return;
|
|
224
|
-
this.walkJsxRouteNode(jsxElement, '', filePath, importMap, results, processedNodes);
|
|
258
|
+
this.walkJsxRouteNode(jsxElement, '', filePath, importMap, results, processedNodes, routerFileSet);
|
|
225
259
|
},
|
|
226
260
|
});
|
|
227
261
|
}
|
|
@@ -229,7 +263,7 @@ class RouteExtractor {
|
|
|
229
263
|
* Recursively walk a JSX <Route> element and its JSX children.
|
|
230
264
|
* pathPrefix is the accumulated path from ancestor <Route path> segments.
|
|
231
265
|
*/
|
|
232
|
-
walkJsxRouteNode(jsxElement, pathPrefix, filePath, importMap, results, processedNodes) {
|
|
266
|
+
walkJsxRouteNode(jsxElement, pathPrefix, filePath, importMap, results, processedNodes, routerFileSet) {
|
|
233
267
|
if (!jsxElement)
|
|
234
268
|
return;
|
|
235
269
|
processedNodes.add(jsxElement);
|
|
@@ -251,11 +285,21 @@ class RouteExtractor {
|
|
|
251
285
|
// Resolve the component file for this route's element prop.
|
|
252
286
|
const resolvedFile = this.resolveRouteComponentFile(openingEl, importMap);
|
|
253
287
|
const effectiveFile = resolvedFile ?? filePath;
|
|
254
|
-
|
|
288
|
+
// Option A+B: pass routerFileSet for content-based router config detection;
|
|
289
|
+
// also check if the resolved file is an outlet-only wrapper (Option B).
|
|
290
|
+
const fileScore = scoreFilePath(effectiveFile, routerFileSet);
|
|
291
|
+
const isOutletOnly = resolvedFile !== null && (() => {
|
|
292
|
+
try {
|
|
293
|
+
return looksLikeOutletOnlyWrapper(fs_1.default.readFileSync(resolvedFile, 'utf-8'));
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
})();
|
|
255
299
|
// A route is a layout container when:
|
|
256
300
|
// - it has child routes (it is a branch, not a leaf), AND
|
|
257
|
-
// - its resolved file is
|
|
258
|
-
const isLayoutRoute = hasChildRoutes && (fileScore === 0 || resolvedFile === null);
|
|
301
|
+
// - its resolved file scores 0, is an outlet-only wrapper, or fell back to the router file
|
|
302
|
+
const isLayoutRoute = hasChildRoutes && (fileScore === 0 || isOutletOnly || resolvedFile === null);
|
|
259
303
|
// Only emit routes that have an explicit path segment — index routes with
|
|
260
304
|
// no path are structural placeholders.
|
|
261
305
|
if (routeSegment || (!hasChildRoutes && fullPath)) {
|
|
@@ -272,14 +316,14 @@ class RouteExtractor {
|
|
|
272
316
|
}
|
|
273
317
|
// Recurse into child <Route> elements, passing the accumulated path.
|
|
274
318
|
for (const child of childRouteElements) {
|
|
275
|
-
this.walkJsxRouteNode(child, fullPath, filePath, importMap, results, processedNodes);
|
|
319
|
+
this.walkJsxRouteNode(child, fullPath, filePath, importMap, results, processedNodes, routerFileSet);
|
|
276
320
|
}
|
|
277
321
|
}
|
|
278
322
|
/**
|
|
279
323
|
* Walk an object-literal route config node as produced by createBrowserRouter.
|
|
280
324
|
* Handles: { path, element, Component, children: [...] }
|
|
281
325
|
*/
|
|
282
|
-
walkObjectRouteConfig(node, pathPrefix, filePath, importMap, results) {
|
|
326
|
+
walkObjectRouteConfig(node, pathPrefix, filePath, importMap, results, routerFileSet) {
|
|
283
327
|
if (!node || node.type !== 'ObjectExpression')
|
|
284
328
|
return;
|
|
285
329
|
let routeSegment = '';
|
|
@@ -316,8 +360,17 @@ class RouteExtractor {
|
|
|
316
360
|
? this.resolveElementNodeToFile(elementNode, importMap)
|
|
317
361
|
: null;
|
|
318
362
|
const effectiveFile = resolvedFile ?? filePath;
|
|
319
|
-
|
|
320
|
-
const
|
|
363
|
+
// Option A+B: content-based router config detection + outlet-only wrapper check.
|
|
364
|
+
const fileScore = scoreFilePath(effectiveFile, routerFileSet);
|
|
365
|
+
const isOutletOnly = resolvedFile !== null && (() => {
|
|
366
|
+
try {
|
|
367
|
+
return looksLikeOutletOnlyWrapper(fs_1.default.readFileSync(resolvedFile, 'utf-8'));
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
})();
|
|
373
|
+
const isLayoutRoute = hasChildRoutes && (fileScore === 0 || isOutletOnly || resolvedFile === null);
|
|
321
374
|
if (routeSegment || (!hasChildRoutes && fullPath)) {
|
|
322
375
|
results.push({
|
|
323
376
|
path: fullPath,
|
|
@@ -332,7 +385,7 @@ class RouteExtractor {
|
|
|
332
385
|
}
|
|
333
386
|
for (const child of childElements) {
|
|
334
387
|
if (child)
|
|
335
|
-
this.walkObjectRouteConfig(child, fullPath, filePath, importMap, results);
|
|
388
|
+
this.walkObjectRouteConfig(child, fullPath, filePath, importMap, results, routerFileSet);
|
|
336
389
|
}
|
|
337
390
|
}
|
|
338
391
|
// ---------------------------------------------------------------------------
|