mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-26 04:19:33 +08:00
fix(vue-router): admin-template route tables, children and layouts; Nuxt routes only in Nuxt (#2130)
vue-element-admin, vue-admin-template and vben had no routes: their tables
are named arrays (`export const constantRoutes = [...]`, `const routes:
RouteRecordRaw[] = [...]`) or per-module objects (`const tableRouter = {…}`)
handed to `new Router(...)`, and most of their screens are `children`.
The Vue Router reader now reads those tables in any file that builds a
router, imports vue-router or lives in a router/ directory; joins children
onto their parent's path (a parent is a screen only when no child claims
its address and it doesn't redirect); binds a lazy view by the FILE it
imports (all of vue-element-admin's are `…/index.vue`), through an alias the
resolver can't follow by the one file in the app with that path; links each
screen to its parents' components as layouts; and takes `this.$router.push`.
Nuxt's file routes move into their own `nuxt` resolver, detected only in a
Nuxt app: halo's plain-Vue console got 30 made-up screens from its `pages/`
folders. A Nuxt app at the repository root now gets its routes, and a
top-level `pages/index.vue` is `/`, not `/index`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
92e14eda40
commit
54a1698caa
@@ -18,7 +18,7 @@ import { vueRouterResolver } from './vue-router';
|
||||
import { angularRouterResolver } from './angular-router';
|
||||
import { svelteKitRouterResolver } from './sveltekit-router';
|
||||
import { svelteResolver } from './svelte';
|
||||
import { vueResolver } from './vue';
|
||||
import { vueResolver, nuxtResolver } from './vue';
|
||||
import { astroResolver } from './astro';
|
||||
import { djangoResolver, flaskResolver, fastapiResolver } from './python';
|
||||
import { railsResolver } from './ruby';
|
||||
@@ -58,6 +58,8 @@ const FRAMEWORK_RESOLVERS: FrameworkResolver[] = [
|
||||
// SvelteKit — `src/routes/**/+page.svelte` routes are `svelteResolver`'s; `goto('/x')` / `redirect(303, '/x')` → navigates edges
|
||||
svelteKitRouterResolver,
|
||||
vueResolver,
|
||||
// Nuxt — `pages/**` screens, `server/api/**` endpoints and `middleware/`, in a Nuxt app only
|
||||
nuxtResolver,
|
||||
// Vue Router — `createRouter({ routes })` → route nodes; `router.push({ name })` / `router.push('/x')` → navigates edges
|
||||
vueRouterResolver,
|
||||
angularRouterResolver,
|
||||
@@ -162,7 +164,7 @@ export { vueRouterResolver } from './vue-router';
|
||||
export { angularRouterResolver } from './angular-router';
|
||||
export { svelteKitRouterResolver } from './sveltekit-router';
|
||||
export { svelteResolver } from './svelte';
|
||||
export { vueResolver } from './vue';
|
||||
export { vueResolver, nuxtResolver } from './vue';
|
||||
export { astroResolver } from './astro';
|
||||
export { djangoResolver, flaskResolver, fastapiResolver } from './python';
|
||||
export { railsResolver } from './ruby';
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
type RouteTable,
|
||||
} from './expo-router';
|
||||
import { destinationsForHref } from './nextjs';
|
||||
import { resolveImportPath } from '../import-resolver';
|
||||
|
||||
const ROUTE_LANGUAGES: readonly Language[] = ['typescript', 'javascript', 'vue'];
|
||||
|
||||
@@ -72,75 +73,137 @@ const ROUTE_LANGUAGES: readonly Language[] = ['typescript', 'javascript', 'vue']
|
||||
// =============================================================================
|
||||
|
||||
export interface VueRouteEntry {
|
||||
/** `/profile/:username` — the path, in the form every other framework's routes use. */
|
||||
/** `/profile/:username` — the full path, a child's joined onto its parents'. */
|
||||
path: string;
|
||||
/** `profile` — what `router.push({ name })` names, when the entry has one. */
|
||||
name: string | null;
|
||||
/** The component the entry names, by identifier or by the tail of its lazy import. */
|
||||
component: string | null;
|
||||
/** `@/views/dashboard/index` — the file a lazy `() => import(…)` component names. */
|
||||
spec: string | null;
|
||||
/** The components of the parent routes it renders inside, outermost first. */
|
||||
layouts: VueLayout[];
|
||||
line: number;
|
||||
}
|
||||
|
||||
export interface VueLayout {
|
||||
component: string;
|
||||
spec: string | null;
|
||||
}
|
||||
|
||||
/** A file that builds a router — the cheap gate before parsing anything. */
|
||||
const ROUTER_FACTORY = /\b(?:createRouter|createWebHistory|createWebHashHistory|createMemoryHistory)\s*\(|\bnew\s+VueRouter\s*\(/;
|
||||
const ROUTER_FACTORY = /\b(?:createRouter|createWebHistory|createWebHashHistory|createMemoryHistory)\s*\(|\bnew\s+(?:VueRouter|Router)\s*\(/;
|
||||
|
||||
/** `routes: [` / `routes = [` — the array itself, for a file that only holds the table. */
|
||||
const ROUTES_ARRAY = /\broutes\s*[:=]\s*\[/;
|
||||
|
||||
/** A file that imports from `vue-router`. */
|
||||
const VUE_ROUTER_IMPORT = /\bfrom\s*['"]vue-router['"]/;
|
||||
|
||||
/** A file in a `router/` or `routes/` directory, or named `router.js` / `routes.ts`. */
|
||||
const ROUTE_FILE_PATH = /(?:^|\/)(?:router|routes)(?:\/|\.[cm]?[jt]s$)/;
|
||||
|
||||
/**
|
||||
* Every top-level entry of a `routes: [...]` array.
|
||||
* Where a routes table starts: a `routes: [` field, or a declaration named
|
||||
* for what it holds — `export const constantRoutes = [`, `const routes:
|
||||
* RouteRecordRaw[] = [`, vue-element-admin's per-module `const tableRouter = {`.
|
||||
*/
|
||||
const TABLE_OPENERS = /\broutes\s*[:=]\s*\[|\b(?:const|let|var)\s+(?:[A-Za-z_$][\w$]*)?(?:[Rr]outes?|[Rr]outers?)\s*(?::[^=;\n]*)?=\s*([[{])/g;
|
||||
|
||||
/**
|
||||
* Every route a file's routes tables declare.
|
||||
*
|
||||
* The array is walked, not pattern-matched: a `name` is written ABOVE the
|
||||
* The tables are walked, not pattern-matched: a `name` is written ABOVE the
|
||||
* `path` it belongs to, so reading fields out of a window around each `path`
|
||||
* hands an entry its PREDECESSOR's name — vue-realworld's `login` came out as
|
||||
* `/register`, silently, for every route in the file. So each top-level `{…}`
|
||||
* is matched as a unit and only its own depth-1 fields are read; a nested
|
||||
* `children:` array, a `meta: {…}` and a lazy `component: () => import(…)`
|
||||
* are stepped over rather than searched.
|
||||
* `/register`, silently, for every route in the file. So each `{…}` is
|
||||
* matched as a unit and only its own depth-1 fields are read; a `meta: {…}`
|
||||
* and a lazy `component: () => import(…)` are stepped over rather than
|
||||
* searched, and `children` are walked in turn with the parent's path in front
|
||||
* of theirs.
|
||||
*
|
||||
* An entry whose path does not start with `/` is a child route, relative to a
|
||||
* parent this does not compose, and is not a destination on its own.
|
||||
* A route with children is a layout: its component renders the outlet they
|
||||
* fill. It is a screen of its own only when no child claims its address and
|
||||
* it does not `redirect` elsewhere — vue-element-admin's `{ path: '/table',
|
||||
* component: Layout, redirect: '/table/complex-table', children }` is the
|
||||
* frame around four tables, not a fifth page.
|
||||
*
|
||||
* A top-level entry whose path does not start with `/` (`*`, a catch-all) is
|
||||
* not an address. A table is read from a file that builds a router, imports
|
||||
* `vue-router`, or lives in a `router/` directory.
|
||||
*/
|
||||
export function parseVueRoutes(content: string): VueRouteEntry[] {
|
||||
if (!ROUTER_FACTORY.test(content) && !ROUTES_ARRAY.test(content)) return [];
|
||||
export function parseVueRoutes(content: string, filePath = ''): VueRouteEntry[] {
|
||||
const routeFile = ROUTER_FACTORY.test(content) || ROUTES_ARRAY.test(content) || VUE_ROUTER_IMPORT.test(content) || ROUTE_FILE_PATH.test(filePath);
|
||||
if (!routeFile) return [];
|
||||
const safe = stripCommentsForRegex(content, 'typescript');
|
||||
const out: VueRouteEntry[] = [];
|
||||
const seen = new Set<string>();
|
||||
const arrays = /\broutes\s*[:=]\s*\[/g;
|
||||
let a: RegExpExecArray | null;
|
||||
while ((a = arrays.exec(safe)) !== null) {
|
||||
const open = a.index + a[0].length - 1;
|
||||
const close = matchBracket(safe, open);
|
||||
if (close < 0) continue;
|
||||
for (const obj of topLevelObjects(safe, open + 1, close)) {
|
||||
const fields = readFields(safe, obj.start, obj.end);
|
||||
const pathField = fields.get('path');
|
||||
if (!pathField) continue;
|
||||
const path = readStringAt(pathField.text.trimStart(), 0);
|
||||
if (path === null || !path.startsWith('/')) continue;
|
||||
const componentField = fields.get('component') ?? fields.get('components');
|
||||
if (!componentField) continue; // no component in the entry → not a route object
|
||||
const component = componentName(componentField.text);
|
||||
if (!component) continue;
|
||||
const nameField = fields.get('name');
|
||||
const name = nameField ? readStringAt(nameField.text.trimStart(), 0) : null;
|
||||
const line = safe.slice(0, pathField.at).split('\n').length;
|
||||
const key = `${path} ${name ?? ''}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push({ path, name, component, line });
|
||||
const walked = new Set<number>();
|
||||
const lineOf = (at: number) => safe.slice(0, at).split('\n').length;
|
||||
|
||||
const visit = (obj: { start: number; end: number }, prefix: string | null, layouts: VueLayout[], depth: number): void => {
|
||||
const fields = readFields(safe, obj.start, obj.end);
|
||||
const pathField = fields.get('path');
|
||||
if (!pathField) return;
|
||||
const own = readStringAt(pathField.text.trimStart(), 0);
|
||||
if (own === null) return;
|
||||
let path: string;
|
||||
if (own.startsWith('/')) path = own;
|
||||
else if (prefix === null) return; // a top-level `*` or relative path is no address
|
||||
else path = own === '' ? prefix : `${prefix === '/' ? '' : prefix}/${own}`;
|
||||
if (path.length > 1) path = path.replace(/\/+$/, '');
|
||||
|
||||
const componentField = fields.get('component') ?? fields.get('components');
|
||||
const component = componentField ? componentOf(componentField.text) : null;
|
||||
|
||||
const children = fields.get('children');
|
||||
let childClaimsAddress = false;
|
||||
if (children && depth < 12) {
|
||||
const open = safe.indexOf('[', children.at);
|
||||
const close = open < 0 ? -1 : matchBracket(safe, open);
|
||||
if (open >= 0 && close > open) {
|
||||
const inner = component ? [...layouts, component] : layouts;
|
||||
for (const child of topLevelObjects(safe, open + 1, close)) {
|
||||
const childPath = readFields(safe, child.start, child.end).get('path');
|
||||
const childOwn = childPath ? readStringAt(childPath.text.trimStart(), 0) : null;
|
||||
if (childOwn === '' || childOwn === path) childClaimsAddress = true;
|
||||
visit(child, path, inner, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
arrays.lastIndex = close;
|
||||
if (!component) return;
|
||||
if (children && (childClaimsAddress || fields.has('redirect'))) return;
|
||||
const nameField = fields.get('name');
|
||||
const name = nameField ? readStringAt(nameField.text.trimStart(), 0) : null;
|
||||
const key = `${path} ${name ?? ''}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
out.push({ path, name, component: component.component, spec: component.spec, layouts: [...layouts], line: lineOf(pathField.at) });
|
||||
};
|
||||
|
||||
TABLE_OPENERS.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = TABLE_OPENERS.exec(safe)) !== null) {
|
||||
const open = m.index + m[0].length - 1;
|
||||
const close = matchBracket(safe, open);
|
||||
if (close < 0 || walked.has(open)) continue;
|
||||
walked.add(open);
|
||||
if (safe[open] === '{') visit({ start: open, end: close }, null, [], 0);
|
||||
else for (const obj of topLevelObjects(safe, open + 1, close)) visit(obj, null, [], 0);
|
||||
TABLE_OPENERS.lastIndex = close;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The component an entry names: an identifier, or the file a lazy import names. */
|
||||
function componentName(value: string): string | null {
|
||||
function componentOf(value: string): VueLayout | null {
|
||||
const lazy = /\bimport\s*\(\s*['"`]([^'"`]+)['"`]/.exec(value);
|
||||
if (lazy) return (lazy[1]!.split('/').pop() ?? '').replace(/\.\w+$/, '') || null;
|
||||
if (lazy) {
|
||||
const tail = (lazy[1]!.split('/').pop() ?? '').replace(/\.\w+$/, '');
|
||||
return tail ? { component: tail, spec: lazy[1]! } : null;
|
||||
}
|
||||
const ident = /^\s*([A-Z][A-Za-z0-9_]*)\s*$/.exec(value);
|
||||
return ident?.[1] ?? null;
|
||||
return ident ? { component: ident[1]!, spec: null } : null;
|
||||
}
|
||||
|
||||
function languageForFile(filePath: string): Language {
|
||||
@@ -175,7 +238,7 @@ function isVueConfigRoute(node: Node): boolean {
|
||||
function isNuxtPage(node: Node): boolean {
|
||||
return (
|
||||
node.language === 'vue' &&
|
||||
node.filePath.includes('/pages/') &&
|
||||
`/${node.filePath}`.includes('/pages/') &&
|
||||
node.id === `route:${node.filePath}:${node.name}:1`
|
||||
);
|
||||
}
|
||||
@@ -212,7 +275,7 @@ export function vueRouteTable(context: ResolutionContext): VueRouteTable {
|
||||
if (!content) continue;
|
||||
const byName = tableAt(group.root).byName;
|
||||
const byPath = new Map(group.nodes.map((n) => [n.name, n]));
|
||||
for (const entry of parseVueRoutes(content)) {
|
||||
for (const entry of parseVueRoutes(content, filePath)) {
|
||||
if (!entry.name) continue;
|
||||
const node = byPath.get(entry.path);
|
||||
if (node && !byName.has(entry.name)) byName.set(entry.name, node);
|
||||
@@ -228,13 +291,13 @@ export function vueRouteTable(context: ResolutionContext): VueRouteTable {
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* `router.push` / `.replace` (the Composition API), `$router.push` /
|
||||
* `.replace` (the Options API and templates), and Nuxt's `navigateTo`.
|
||||
* `router.push` / `.replace` (the Composition API), `this.$router.push` /
|
||||
* `$router.push` (the Options API and templates), and Nuxt's `navigateTo`.
|
||||
*
|
||||
* As everywhere else, `push` and `replace` need a receiver that names a
|
||||
* router: an unqualified `push` is an array's.
|
||||
*/
|
||||
const NAV_CALL = /^\$?router\.(?:push|replace)$|^navigateTo$/;
|
||||
const NAV_CALL = /^(?:this\.)?\$?router\.(?:push|replace)$|^navigateTo$/;
|
||||
|
||||
/** The verb a navigation call name stands for, or null. */
|
||||
export function vueNavVerb(name: string): string | null {
|
||||
@@ -282,6 +345,56 @@ function vueComponentNamed(name: string, fromFile: string, context: ResolutionCo
|
||||
return near.length === 1 ? near[0]! : null;
|
||||
}
|
||||
|
||||
/** `import:@/views/Login#Login` for a lazy component, the bare name for an identifier. */
|
||||
function componentRefName(component: string, spec: string | null): string {
|
||||
return spec === null ? component : `import:${spec}#${component}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The component a route's reference names. A lazy import names a FILE, and
|
||||
* the component is that file's: vue-element-admin's views are all
|
||||
* `…/index.vue`, so the name alone (`index`) fits dozens. The import is
|
||||
* resolved as the router file would resolve it, or — through an alias the
|
||||
* resolver doesn't know, like vben's `#/views/…` — by the one file in the
|
||||
* same app whose path ends the way the specifier does.
|
||||
*/
|
||||
function routeComponent(encoded: string, fromFile: string, context: ResolutionContext): Node | null {
|
||||
const lazy = /^import:(.+)#([^#]+)$/.exec(encoded);
|
||||
// `component: Layout` — the file's own import of it says which file that is
|
||||
// (`import Layout from '@/layout'`, a `layout/index.vue`).
|
||||
const spec = lazy ? lazy[1]! : (context.getImportMappings(fromFile, languageForFile(fromFile)).find((m) => m.localName === encoded)?.source ?? null);
|
||||
const name = lazy ? lazy[2]! : encoded;
|
||||
if (spec !== null) {
|
||||
const file = resolveImportPath(spec, fromFile, 'vue', context) ?? fileBySuffix(spec, fromFile, context);
|
||||
const component = file ? componentInFile(file, context) : null;
|
||||
if (component) return component;
|
||||
}
|
||||
return name === 'index' ? null : vueComponentNamed(name, fromFile, context);
|
||||
}
|
||||
|
||||
function componentInFile(file: string, context: ResolutionContext): Node | null {
|
||||
const nodes = context.getNodesInFile(file);
|
||||
return nodes.find((n) => n.kind === 'component') ?? nodes.find((n) => n.kind === 'function' && /^[A-Z]/.test(n.name)) ?? null;
|
||||
}
|
||||
|
||||
const COMPONENT_EXTENSIONS = ['', '.vue', '.tsx', '.jsx', '.ts', '.js', '/index.vue', '/index.tsx', '/index.ts', '/index.js'];
|
||||
|
||||
/** The one file under `fromFile`'s app whose path ends like `spec` minus its alias (`#/`, `@/`, `~/`). */
|
||||
function fileBySuffix(spec: string, fromFile: string, context: ResolutionContext): string | null {
|
||||
const rest = spec.replace(/^(?:[@#~$][\w-]*\/|\.{1,2}\/)+/, '');
|
||||
if (rest === spec || rest.length === 0) return null;
|
||||
const root = appRootFor(fromFile);
|
||||
const hits = new Set<string>();
|
||||
for (const file of context.getAllFiles()) {
|
||||
if (!file.startsWith(root)) continue;
|
||||
for (const ext of COMPONENT_EXTENSIONS) {
|
||||
const tail = rest + ext;
|
||||
if (file === tail || file.endsWith('/' + tail)) hits.add(file);
|
||||
}
|
||||
}
|
||||
return hits.size === 1 ? [...hits][0]! : null;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// The resolver
|
||||
// =============================================================================
|
||||
@@ -289,17 +402,18 @@ function vueComponentNamed(name: string, fromFile: string, context: ResolutionCo
|
||||
export const vueRouterResolver: FrameworkResolver = {
|
||||
name: 'vue-router',
|
||||
languages: [...ROUTE_LANGUAGES],
|
||||
appDependencies: ['vue-router', 'nuxt', 'nuxt3'],
|
||||
|
||||
detect(context: ResolutionContext): boolean {
|
||||
return dependsOn(context, 'vue-router', 'nuxt', 'nuxt3');
|
||||
},
|
||||
|
||||
claimsReference(name: string): boolean {
|
||||
return NAV_CALL.test(name);
|
||||
return NAV_CALL.test(name) || name.startsWith('import:') || name.startsWith('layout:');
|
||||
},
|
||||
|
||||
extract(filePath: string, content: string): FrameworkExtractionResult {
|
||||
const entries = parseVueRoutes(content);
|
||||
const entries = parseVueRoutes(content, filePath);
|
||||
if (entries.length === 0) return { nodes: [], references: [] };
|
||||
const language = languageForFile(filePath);
|
||||
const now = Date.now();
|
||||
@@ -329,7 +443,7 @@ export const vueRouterResolver: FrameworkResolver = {
|
||||
// a `calls` edge to a component as the page a screen renders.
|
||||
references.push({
|
||||
fromNodeId: node.id,
|
||||
referenceName: entry.component,
|
||||
referenceName: componentRefName(entry.component, entry.spec),
|
||||
referenceKind: 'calls',
|
||||
line: entry.line,
|
||||
column: 0,
|
||||
@@ -338,11 +452,30 @@ export const vueRouterResolver: FrameworkResolver = {
|
||||
candidates: [entry.component],
|
||||
});
|
||||
}
|
||||
// The layouts around it: what they render — a sidebar, a navbar — is on
|
||||
// this screen too.
|
||||
for (const layout of entry.layouts) {
|
||||
references.push({
|
||||
fromNodeId: node.id,
|
||||
referenceName: `layout:${componentRefName(layout.component, layout.spec)}`,
|
||||
referenceKind: 'references',
|
||||
line: entry.line,
|
||||
column: 0,
|
||||
filePath,
|
||||
language,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { nodes, references };
|
||||
},
|
||||
|
||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
if (ref.referenceKind === 'references' && isVueRouteRef(ref) && ref.referenceName.startsWith('layout:')) {
|
||||
const layout = routeComponent(ref.referenceName.slice('layout:'.length), ref.filePath, context);
|
||||
return layout
|
||||
? { original: ref, targetNodeId: layout.id, confidence: 0.95, resolvedBy: 'framework', metadata: { layout: true } }
|
||||
: null;
|
||||
}
|
||||
if (ref.referenceKind !== 'calls') return null;
|
||||
|
||||
// A route naming the component it renders — this resolver's own reference,
|
||||
@@ -350,7 +483,7 @@ export const vueRouterResolver: FrameworkResolver = {
|
||||
// `Login` view AND a `login` action in a store, and only one of them is
|
||||
// the screen.
|
||||
if (isVueRouteRef(ref)) {
|
||||
const component = vueComponentNamed(ref.referenceName, ref.filePath, context);
|
||||
const component = routeComponent(ref.referenceName, ref.filePath, context);
|
||||
return component
|
||||
? { original: ref, targetNodeId: component.id, confidence: 0.95, resolvedBy: 'framework' }
|
||||
: null;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* Vue / Nuxt Framework Resolver
|
||||
*
|
||||
* Handles Vue component references, compiler macros (defineProps, etc.),
|
||||
* Nuxt auto-imports, and Nuxt file-based routing patterns.
|
||||
* Handles Vue component references, compiler macros (defineProps, etc.) and
|
||||
* Nuxt auto-imports; `nuxtResolver` reads Nuxt's file-based routes.
|
||||
*/
|
||||
|
||||
import { Node } from '../../types';
|
||||
import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
|
||||
import { dependsOn } from './package-deps';
|
||||
|
||||
/**
|
||||
* Vue 3 compiler macros — compiler-provided, not user code
|
||||
@@ -186,13 +187,34 @@ export const vueResolver: FrameworkResolver = {
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Nuxt's file-based routes: `pages/` screens, `server/api/` endpoints and
|
||||
* `middleware/`. Its own resolver, detected only in a Nuxt app: a plain Vue
|
||||
* app keeps its views in a `pages/` folder just as often (halo's console
|
||||
* does), and those are components a router config names, not addresses.
|
||||
*/
|
||||
export const nuxtResolver: FrameworkResolver = {
|
||||
name: 'nuxt',
|
||||
appDependencies: ['nuxt', 'nuxt3', '@nuxt/kit'],
|
||||
|
||||
detect(context: ResolutionContext): boolean {
|
||||
if (dependsOn(context, 'nuxt', 'nuxt3', '@nuxt/kit')) return true;
|
||||
return context.getAllFiles().some((f) => /(?:^|\/)nuxt\.config\.(?:[cm]?[jt]s)$/.test(f));
|
||||
},
|
||||
|
||||
resolve(): ResolvedRef | null {
|
||||
return null;
|
||||
},
|
||||
|
||||
extract(filePath: string, _content: string) {
|
||||
const nodes: Node[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
// Normalize to forward slashes
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
// Forward slashes, and a leading `/` so an app at the repository root
|
||||
// (`pages/index.vue`) is found by the same `/pages/` search as a nested one.
|
||||
const normalized = '/' + filePath.replace(/\\/g, '/');
|
||||
|
||||
// Detect Nuxt page routes (pages/ directory)
|
||||
const pagesIndex = normalized.indexOf('/pages/');
|
||||
@@ -221,8 +243,10 @@ export const vueResolver: FrameworkResolver = {
|
||||
const afterApi = normalized.substring(apiIndex + '/server/api/'.length);
|
||||
const routeName = afterApi
|
||||
.replace(/\.[^/.]+$/, '') // Remove extension
|
||||
.replace(/\/index$/, ''); // index -> parent path
|
||||
const apiRoute = '/api/' + routeName;
|
||||
.replace(/(?:^|\/)index$/, '') // index -> parent path
|
||||
.replace(/\[\.\.\.([^\]]+)\]/g, '*$1') // [...slug] -> *slug
|
||||
.replace(/\[([^\]]+)\]/g, ':$1'); // [id] -> :id, as a page's params are
|
||||
const apiRoute = routeName === '' ? '/api' : '/api/' + routeName;
|
||||
|
||||
nodes.push({
|
||||
id: `route:${filePath}:${apiRoute}:1`,
|
||||
@@ -317,7 +341,7 @@ function filePathToNuxtRoute(normalized: string, afterPagesStart: number): strin
|
||||
const withoutExt = afterPages.replace(/\.vue$/, '');
|
||||
|
||||
// Remove /index suffix (index.vue -> parent route)
|
||||
const withoutIndex = withoutExt.replace(/\/index$/, '');
|
||||
const withoutIndex = withoutExt.replace(/(?:^|\/)index$/, '');
|
||||
|
||||
// Convert Nuxt param syntax [param] to :param
|
||||
let route = '/' + withoutIndex
|
||||
|
||||
@@ -218,7 +218,7 @@ function writtenHere(edge: Edge, holder: Node): boolean {
|
||||
* Entry points, which is the list of what a request or a user can arrive at.
|
||||
*/
|
||||
function isScreenRoute(route: Node): boolean {
|
||||
return route.name.startsWith('/') && !route.filePath.includes('/server/api/');
|
||||
return route.name.startsWith('/') && !`/${route.filePath}`.includes('/server/api/');
|
||||
}
|
||||
|
||||
export async function buildScreens(cg: CodeGraph, projectRoot: string): Promise<WireScreensPayload> {
|
||||
|
||||
Reference in New Issue
Block a user