/**
 * Source-grounded route extraction — strategy 2 of the candidate UI page ladder.
 *
 * Given a repo path, walks router/App files with the TypeScript Compiler API
 * to find route declarations in three forms:
 *
 *   1. React-style JSX route elements:
 *      <Route path="/cart" element={<Cart/>} />
 *      <SentryRoute path={WORKSPACE_URL} component={Workspaces} />
 *
 *   2. Vue Router object literals:
 *      const routes = [{ path: '/login', component: Login }, …]
 *
 *   3. Directus `defineModule({ id, routes: [...] })`:
 *      defineModule({ id: 'notifications', routes: [{ path: '', component: X }] })
 *      → URL = "/notifications" (module id prefix + nested path)
 *
 * Path attribute values are resolved through:
 *   - String literals (`path="/cart"`)
 *   - Local const declarations (`const X = "/cart"; path={X}`)
 *   - Cross-file imports + named exports (`import { X } from './constants'`)
 *   - Template literals composed of resolvable parts (`` `${BASE}/foo` ``)
 *
 * Why TS Compiler API rather than regex: route declarations span multiple
 * lines with attributes interleaved, paths reference constants that must be
 * cross-file resolved, and tag names vary (Route, SentryRoute, PrivateRoute,
 * …). The AST gives us the structure for free; regex would have to be
 * deliberately permissive at the cost of false matches.
 */
import * as fs from "fs";
import * as path from "path";
import * as ts from "typescript";
function makeParseCache() {
    return { files: new Map(), tsconfigBaseUrl: new Map() };
}
/** Recursion cap for cross-file constant resolution. */
const MAX_RESOLVE_DEPTH = 4;
/**
 * JSX tag names recognized as route declarations. Default covers React Router
 * and SentryRoute (Sentry-instrumented wrapper used by appsmith). Wrapper
 * components like PrivateRoute / AuthRoute / RoleRoute can be added here as
 * we encounter them — the heuristic stays the same: a JSX element with `path`
 * and `element`/`component` attrs is a route declaration.
 */
const ROUTE_TAG_NAMES = new Set([
    "Route",
    "SentryRoute",
    "PrivateRoute",
    "PublicRoute",
    "AuthRoute",
    "ProtectedRoute",
]);
/** Files we look for as router declaration sites. */
const ROUTER_FILE_GLOBS = [
    "App.tsx",
    "App.jsx",
    "AppRouter.tsx",
    "AppRouter.jsx",
    "router.ts",
    "router.tsx",
    "router.js",
    "router.jsx",
    "routes.ts",
    "routes.tsx",
    "routes.js",
    "routes.jsx",
    "Routes.tsx",
    "Routes.jsx",
];
/** Cap on candidate files to avoid pathological repos. */
const MAX_CANDIDATE_FILES = 50;
/** Recursion cap on directory walking. */
const MAX_WALK_DEPTH = 8;
/** Directories we skip when walking the repo. */
const SKIP_DIRS = new Set([
    "node_modules",
    ".git",
    "dist",
    "build",
    "out",
    ".next",
    ".nuxt",
    ".svelte-kit",
    "coverage",
    "__tests__",
]);
/**
 * Walk the repo, parse candidate router files, return all route declarations.
 *
 * Best-effort by design: a parse error on one file shouldn't block discovery
 * across the rest. Returns empty when no router files match — callers should
 * treat that as "strategy 2 inapplicable" and fall through to the next one.
 */
export function extractSourceRoutes(repositoryPath) {
    const candidates = findRouterFiles(repositoryPath);
    const parseCache = makeParseCache();
    const routes = [];
    for (const file of candidates) {
        try {
            const fileRoutes = extractRoutesFromFile(file, parseCache);
            routes.push(...fileRoutes);
        }
        catch {
            // Parse error or fs error — skip this file silently, keep going.
        }
    }
    return routes;
}
function parseFile(filePath, cache) {
    const cached = cache.files.get(filePath);
    if (cached)
        return cached;
    try {
        const source = fs.readFileSync(filePath, "utf-8");
        const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, 
        /*setParentNodes*/ true, scriptKindFor(filePath));
        cache.files.set(filePath, sf);
        return sf;
    }
    catch {
        return null;
    }
}
// ── File discovery ────────────────────────────────────────────────────────
function findRouterFiles(repoPath) {
    const matches = [];
    const targets = new Set(ROUTER_FILE_GLOBS.map((g) => g.toLowerCase()));
    const indexExtensions = new Set([".ts", ".tsx", ".js", ".jsx"]);
    function walk(dir, depth) {
        if (depth > MAX_WALK_DEPTH)
            return;
        if (matches.length >= MAX_CANDIDATE_FILES)
            return;
        let entries;
        try {
            entries = fs.readdirSync(dir, { withFileTypes: true });
        }
        catch {
            return;
        }
        for (const entry of entries) {
            if (matches.length >= MAX_CANDIDATE_FILES)
                return;
            const full = path.join(dir, entry.name);
            if (entry.isDirectory()) {
                if (SKIP_DIRS.has(entry.name) || entry.name.startsWith("."))
                    continue;
                walk(full, depth + 1);
            }
            else if (entry.isFile()) {
                const lower = entry.name.toLowerCase();
                if (targets.has(lower)) {
                    matches.push(full);
                    continue;
                }
                // Directus-style modules: `…/modules/<name>/index.{ts,tsx,js,jsx}`
                // hosts a `defineModule({ routes: [...] })` call.
                if (lower.startsWith("index.")) {
                    const ext = path.extname(lower);
                    if (indexExtensions.has(ext) && full.includes(`${path.sep}modules${path.sep}`)) {
                        matches.push(full);
                    }
                }
            }
        }
    }
    walk(repoPath, 0);
    return matches;
}
// ── Per-file extraction ────────────────────────────────────────────────────
function extractRoutesFromFile(filePath, cache) {
    const sf = parseFile(filePath, cache);
    if (!sf)
        return [];
    const imports = collectImports(sf, filePath, cache);
    const localConsts = collectLocalConsts(sf);
    const routes = [];
    visit(sf, (node) => {
        // 1. JSX route elements: <Route path="…" element={<X/>} />
        if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
            const r = jsxRouteFromNode(node, filePath, imports, localConsts, cache);
            if (r)
                routes.push(r);
            return;
        }
        // 2. defineModule({ id, routes: [...] }) — Directus per-module aggregation.
        if (ts.isCallExpression(node)) {
            const moduleRoutes = defineModuleRoutesFromCall(node, filePath, imports, localConsts, cache);
            if (moduleRoutes) {
                routes.push(...moduleRoutes);
            }
        }
    });
    // 3. Vue Router object-literal arrays (top-level, not inside defineModule).
    // Walk separately so we can detect the "routes:" property context and skip
    // arrays of unrelated config objects.
    routes.push(...vueRouterRoutesFromFile(sf, filePath, imports, localConsts, cache));
    return routes;
}
function jsxRouteFromNode(node, filePath, imports, localConsts, cache) {
    const tag = jsxTagOf(node);
    if (!tag || !ROUTE_TAG_NAMES.has(tag))
        return null;
    const attrs = jsxAttributesOf(node);
    const pathAttr = attrs.find((a) => a.name === "path");
    if (!pathAttr)
        return null;
    const pathValue = resolveAttrToString(pathAttr, filePath, imports, localConsts, cache);
    if (pathValue === null)
        return null;
    const componentAttr = attrs.find((a) => a.name === "element" || a.name === "component");
    if (!componentAttr || componentAttr.kind !== "identifier")
        return null;
    return {
        path: pathValue,
        componentName: componentAttr.value,
        componentFile: imports.get(componentAttr.value)?.filePath,
        declaredIn: filePath,
    };
}
function scriptKindFor(filePath) {
    const ext = path.extname(filePath).toLowerCase();
    switch (ext) {
        case ".tsx":
            return ts.ScriptKind.TSX;
        case ".jsx":
            return ts.ScriptKind.JSX;
        case ".ts":
            return ts.ScriptKind.TS;
        case ".js":
            return ts.ScriptKind.JS;
        default:
            return ts.ScriptKind.TSX;
    }
}
function visit(node, fn) {
    fn(node);
    node.forEachChild((child) => visit(child, fn));
}
// ── JSX inspection ─────────────────────────────────────────────────────────
function jsxTagOf(node) {
    const tag = node.tagName;
    if (ts.isIdentifier(tag))
        return tag.text;
    // Member expression (e.g. <Foo.Bar/>) — not relevant for routes.
    return null;
}
function jsxAttributesOf(node) {
    const out = [];
    for (const attr of node.attributes.properties) {
        if (!ts.isJsxAttribute(attr))
            continue;
        const name = attr.name.getText();
        const init = attr.initializer;
        if (!init) {
            out.push({ name, kind: "other", value: "" });
            continue;
        }
        if (ts.isStringLiteral(init)) {
            out.push({ name, kind: "string", value: init.text });
            continue;
        }
        if (ts.isJsxExpression(init) && init.expression) {
            out.push(parseExpressionAttr(name, init.expression));
            continue;
        }
        out.push({ name, kind: "other", value: "" });
    }
    return out;
}
function parseExpressionAttr(name, expr) {
    if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
        return { name, kind: "string", value: expr.text };
    }
    if (ts.isJsxSelfClosingElement(expr) || ts.isJsxOpeningElement(expr)) {
        const tag = jsxTagOf(expr);
        if (tag)
            return { name, kind: "identifier", value: tag };
    }
    if (ts.isJsxElement(expr)) {
        const tag = jsxTagOf(expr.openingElement);
        if (tag)
            return { name, kind: "identifier", value: tag };
    }
    if (ts.isIdentifier(expr)) {
        return { name, kind: "identifier", value: expr.text };
    }
    // Template literals, binary expressions, etc. — defer to the resolver.
    return { name, kind: "expression", value: "", expression: expr };
}
/**
 * Map local name → imported binding info. Used to:
 *   - Resolve component identifiers to file paths (existing slice 3a use)
 *   - Resolve path identifiers across file boundaries (slice 3b new use)
 *
 * Handles:
 *   import Cart from './pages/Cart'                  // default, relative
 *   import { APPLICATIONS_URL } from './constants'   // named, relative
 *   import { X as Y } from './foo'                   // aliased named
 *   import { WORKSPACE_URL } from "constants/routes" // baseUrl-resolved
 *
 * Bare imports (`react-router-dom`) are skipped — those aren't local files.
 * `paths` aliases (e.g. `@/*`, `ee/*`) aren't currently honored; the only
 * consequence in target repos is missing some constants in deep-aliased
 * subtrees, which downgrades to "couldn't resolve, skip the route" — safe.
 */
function collectImports(sf, filePath, cache) {
    const result = new Map();
    const fileDir = path.dirname(filePath);
    const baseUrl = findTsconfigBaseUrl(fileDir, cache);
    for (const stmt of sf.statements) {
        if (!ts.isImportDeclaration(stmt))
            continue;
        const moduleSpec = stmt.moduleSpecifier;
        if (!ts.isStringLiteral(moduleSpec))
            continue;
        const spec = moduleSpec.text;
        let resolved;
        if (spec.startsWith("/")) {
            // Absolute import specs would let a hostile repo point the analyzer
            // at arbitrary host paths during route extraction (e.g. /etc/passwd).
            // Real codebases don't use literal absolute imports — they use
            // relative paths or tsconfig path aliases.
            continue;
        }
        if (spec.startsWith(".")) {
            resolved = resolveImportPath(fileDir, spec);
        }
        else if (baseUrl) {
            // Try baseUrl resolution for non-relative imports.
            resolved = resolveImportPath(baseUrl, spec);
        }
        if (!resolved)
            continue;
        const clause = stmt.importClause;
        if (!clause)
            continue;
        if (clause.name) {
            result.set(clause.name.text, { filePath: resolved, exportedName: null });
        }
        if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
            for (const elt of clause.namedBindings.elements) {
                // `import { X as Y }`: local name is `Y`, exported name is `X`.
                // `import { X }`: local + exported are both `X`.
                const exportedName = elt.propertyName?.text ?? elt.name.text;
                result.set(elt.name.text, {
                    filePath: resolved,
                    exportedName,
                });
            }
        }
    }
    return result;
}
/**
 * Map local const names → their initializer expression. Used to resolve
 * `const X = "/cart"; <Route path={X} />` within a single file, and to
 * supply the source for cross-file constant lookups.
 */
function collectLocalConsts(sf) {
    const result = new Map();
    for (const stmt of sf.statements) {
        const decls = getVariableDeclarationList(stmt);
        if (!decls)
            continue;
        for (const d of decls.declarations) {
            if (!ts.isIdentifier(d.name))
                continue;
            if (!d.initializer)
                continue;
            result.set(d.name.text, d.initializer);
        }
    }
    return result;
}
/**
 * Variable declarations can appear at top level (`const X = ...`) or
 * re-exported (`export const X = ...`). Both look the same to us.
 */
function getVariableDeclarationList(stmt) {
    if (ts.isVariableStatement(stmt))
        return stmt.declarationList;
    return null;
}
/**
 * Resolve a parsed JSX/object-literal attribute to a concrete string value.
 *
 * Returns:
 *   - The string content for string-literal attrs.
 *   - The resolved string for identifier attrs (via local consts or imports).
 *   - The composed string for template literals whose parts all resolve.
 *   - `null` when resolution fails — caller should skip the route.
 */
function resolveAttrToString(attr, filePath, imports, localConsts, cache) {
    if (attr.kind === "string")
        return attr.value;
    if (attr.kind === "identifier") {
        return resolveIdentifierToString(attr.value, filePath, imports, localConsts, cache, 0);
    }
    if (attr.kind === "expression" && attr.expression) {
        return resolveExpressionToString(attr.expression, filePath, imports, localConsts, cache, 0);
    }
    return null;
}
/**
 * Resolve an identifier to its concrete string value. Looks first in the
 * current file's local consts; falls back to imports + cross-file lookup.
 */
function resolveIdentifierToString(name, filePath, imports, localConsts, cache, depth) {
    if (depth > MAX_RESOLVE_DEPTH)
        return null;
    const local = localConsts.get(name);
    if (local) {
        return resolveExpressionToString(local, filePath, imports, localConsts, cache, depth + 1);
    }
    const imp = imports.get(name);
    if (imp) {
        // exportedName === null marks a default import: `import Foo from './foo'`.
        // The resolver looks up "default" specifically, so it can match either
        // `export default <expr>` or `export { X as default }`.
        return resolveExportedSymbolToString(imp.filePath, imp.exportedName ?? "default", cache, depth + 1);
    }
    return null;
}
/**
 * Resolve an arbitrary expression to a string. Handles the forms commonly
 * seen in route declarations: literals, identifiers, template literals
 * composed of resolvable parts, simple binary `+` concatenation.
 */
function resolveExpressionToString(expr, filePath, imports, localConsts, cache, depth) {
    if (depth > MAX_RESOLVE_DEPTH)
        return null;
    if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
        return expr.text;
    }
    if (ts.isIdentifier(expr)) {
        return resolveIdentifierToString(expr.text, filePath, imports, localConsts, cache, depth);
    }
    if (ts.isTemplateExpression(expr)) {
        let out = expr.head.text;
        for (const span of expr.templateSpans) {
            const partVal = resolveExpressionToString(span.expression, filePath, imports, localConsts, cache, depth + 1);
            if (partVal === null)
                return null;
            out += partVal + span.literal.text;
        }
        return out;
    }
    if (ts.isBinaryExpression(expr) &&
        expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
        const left = resolveExpressionToString(expr.left, filePath, imports, localConsts, cache, depth + 1);
        if (left === null)
            return null;
        const right = resolveExpressionToString(expr.right, filePath, imports, localConsts, cache, depth + 1);
        if (right === null)
            return null;
        return left + right;
    }
    return null;
}
/**
 * Open a target file from cache, find the named export, resolve it.
 * Used when the calling file imports a constant: `import { X } from './foo'`.
 */
function resolveExportedSymbolToString(filePath, exportedName, cache, depth) {
    if (depth > MAX_RESOLVE_DEPTH)
        return null;
    const sf = parseFile(filePath, cache);
    if (!sf)
        return null;
    const imports = collectImports(sf, filePath, cache);
    const localConsts = collectLocalConsts(sf);
    const fileDir = path.dirname(filePath);
    const baseUrl = findTsconfigBaseUrl(fileDir, cache);
    // 1. Look for `export const X = …` directly in this file.
    for (const stmt of sf.statements) {
        if (!ts.isVariableStatement(stmt))
            continue;
        const isExported = !!stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
        if (!isExported)
            continue;
        for (const d of stmt.declarationList.declarations) {
            if (!ts.isIdentifier(d.name))
                continue;
            if (d.name.text !== exportedName)
                continue;
            if (!d.initializer)
                return null;
            return resolveExpressionToString(d.initializer, filePath, imports, localConsts, cache, depth + 1);
        }
    }
    // 1b. Handle `export default <expr>` and `export { X as default }` when
    //     the caller is resolving a default import.
    if (exportedName === "default") {
        for (const stmt of sf.statements) {
            if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
                // `export default <expr>` — direct default export.
                return resolveExpressionToString(stmt.expression, filePath, imports, localConsts, cache, depth + 1);
            }
            if (ts.isExportDeclaration(stmt) && stmt.exportClause &&
                ts.isNamedExports(stmt.exportClause) && !stmt.moduleSpecifier) {
                // `export { X as default }` — local re-export aliased as default.
                for (const elt of stmt.exportClause.elements) {
                    if (elt.name.text !== "default")
                        continue;
                    const sourceName = elt.propertyName?.text ?? elt.name.text;
                    const local = localConsts.get(sourceName);
                    if (local) {
                        return resolveExpressionToString(local, filePath, imports, localConsts, cache, depth + 1);
                    }
                    // Could be re-exporting an imported binding: import X; export { X as default }.
                    const imp = imports.get(sourceName);
                    if (imp) {
                        return resolveExportedSymbolToString(imp.filePath, imp.exportedName ?? "default", cache, depth + 1);
                    }
                }
            }
        }
    }
    // 2. Follow `export { X } from './foo'` and `export * from './foo'` re-exports.
    for (const stmt of sf.statements) {
        if (!ts.isExportDeclaration(stmt))
            continue;
        if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier))
            continue;
        const targetSpec = stmt.moduleSpecifier.text;
        let targetFile;
        if (targetSpec.startsWith("/")) {
            // Absolute paths rejected for the same reason as collectImports.
            continue;
        }
        if (targetSpec.startsWith(".")) {
            targetFile = resolveImportPath(fileDir, targetSpec);
        }
        else if (baseUrl) {
            targetFile = resolveImportPath(baseUrl, targetSpec);
        }
        if (!targetFile)
            continue;
        if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
            // `export { X as Y } from './foo'` — walk through with the original name.
            for (const elt of stmt.exportClause.elements) {
                if (elt.name.text !== exportedName)
                    continue;
                const sourceName = elt.propertyName?.text ?? elt.name.text;
                const v = resolveExportedSymbolToString(targetFile, sourceName, cache, depth + 1);
                if (v !== null)
                    return v;
            }
        }
        else if (!stmt.exportClause) {
            // `export * from './foo'` — recurse with the original name.
            const v = resolveExportedSymbolToString(targetFile, exportedName, cache, depth + 1);
            if (v !== null)
                return v;
        }
    }
    return null;
}
const IMPORT_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js", ".vue"];
function resolveImportPath(fromDir, spec) {
    const base = path.resolve(fromDir, spec);
    // 1. Exact file (with the extension already in spec).
    if (fileExistsSync(base))
        return base;
    // 2. Try each known extension.
    for (const ext of IMPORT_EXTENSIONS) {
        const candidate = base + ext;
        if (fileExistsSync(candidate))
            return candidate;
    }
    // 3. Treat as directory with index.{tsx,jsx,ts,js,vue}.
    for (const ext of IMPORT_EXTENSIONS) {
        const candidate = path.join(base, "index" + ext);
        if (fileExistsSync(candidate))
            return candidate;
    }
    return undefined;
}
function fileExistsSync(p) {
    try {
        const stat = fs.statSync(p);
        return stat.isFile();
    }
    catch {
        return false;
    }
}
/**
 * Walk parent directories until a `tsconfig.json` is found, then resolve its
 * `baseUrl` (following one level of `extends` for the common case where
 * tsconfig.json extends a tsconfig.path.json sibling). Returns null when no
 * tsconfig is found or no `baseUrl` is configured.
 *
 * Memoized in the parse cache: cache hits AND misses, both keyed by the
 * directory we started searching from. Prevents re-stat'ing the same parent
 * chain on every import in a deeply nested tree.
 */
function findTsconfigBaseUrl(fromDir, cache) {
    const cached = cache.tsconfigBaseUrl.get(fromDir);
    if (cached !== undefined)
        return cached;
    let cur = fromDir;
    for (let i = 0; i < 12; i++) {
        const tsconfigPath = path.join(cur, "tsconfig.json");
        if (fileExistsSync(tsconfigPath)) {
            const baseUrl = readTsconfigBaseUrl(tsconfigPath);
            cache.tsconfigBaseUrl.set(fromDir, baseUrl);
            return baseUrl;
        }
        const parent = path.dirname(cur);
        if (parent === cur)
            break;
        cur = parent;
    }
    cache.tsconfigBaseUrl.set(fromDir, null);
    return null;
}
function readTsconfigBaseUrl(tsconfigPath) {
    // Cap recursion through `extends` to avoid pathological loops.
    return readTsconfigBaseUrlInner(tsconfigPath, 0);
}
function readTsconfigBaseUrlInner(tsconfigPath, depth) {
    if (depth > 4)
        return null;
    let parsed;
    try {
        const raw = fs.readFileSync(tsconfigPath, "utf-8");
        // tsconfig allows comments; do a lenient parse.
        parsed = parseJsonWithComments(raw);
    }
    catch {
        return null;
    }
    const baseUrl = parsed?.compilerOptions?.baseUrl;
    if (typeof baseUrl === "string") {
        return path.resolve(path.dirname(tsconfigPath), baseUrl);
    }
    const extendsPath = parsed?.extends;
    if (typeof extendsPath === "string") {
        const tsconfigDir = path.dirname(tsconfigPath);
        let extendedPath;
        if (extendsPath.startsWith(".") || extendsPath.startsWith("/")) {
            extendedPath = path.resolve(tsconfigDir, extendsPath);
        }
        else {
            // Bare-extend (e.g. "@tsconfig/node20/tsconfig.json") — skip.
            return null;
        }
        if (!extendedPath.endsWith(".json"))
            extendedPath += ".json";
        return readTsconfigBaseUrlInner(extendedPath, depth + 1);
    }
    return null;
}
/**
 * Strip line/block comments from JSON before parsing. tsconfig.json files
 * commonly contain comments; `JSON.parse` would reject them.
 */
function parseJsonWithComments(raw) {
    const stripped = raw
        .replace(/\/\*[\s\S]*?\*\//g, "")
        .replace(/^\s*\/\/.*$/gm, "");
    return JSON.parse(stripped);
}
// ── Vue Router object literals ─────────────────────────────────────────────
/**
 * Walk the file looking for object literals with both `path` and `component`
 * properties — the Vue Router (and React Router data-API) shape:
 *
 *   { path: '/login', component: Login }
 *
 * Skips object literals nested inside `defineModule({ routes: [...] })` calls
 * — those are handled by `defineModuleRoutesFromCall` with the module-id
 * prefix applied. Without this skip we'd emit the same routes twice with
 * different paths.
 */
function vueRouterRoutesFromFile(sf, filePath, imports, localConsts, cache) {
    const out = [];
    visit(sf, (node) => {
        if (!ts.isObjectLiteralExpression(node))
            return;
        if (isInsideDefineModule(node))
            return;
        const r = routeFromObjectLiteral(node, "", filePath, imports, localConsts, cache);
        if (r)
            out.push(r);
    });
    return out;
}
function isInsideDefineModule(node) {
    let cur = node.parent;
    while (cur) {
        if (ts.isCallExpression(cur)) {
            const expr = cur.expression;
            if (ts.isIdentifier(expr) && expr.text === "defineModule")
                return true;
        }
        cur = cur.parent;
    }
    return false;
}
/**
 * Build a SourceRoute from an object literal that has the route shape
 * (`path` + `component` properties). Returns null when the shape doesn't
 * match or path/component can't be resolved.
 *
 * `pathPrefix` is the module-id prefix from defineModule aggregation —
 * empty string for top-level routes, "/notifications" for module routes.
 */
function routeFromObjectLiteral(obj, pathPrefix, filePath, imports, localConsts, cache) {
    let pathExpr;
    let componentName;
    for (const prop of obj.properties) {
        if (!ts.isPropertyAssignment(prop))
            continue;
        if (!ts.isIdentifier(prop.name))
            continue;
        const propName = prop.name.text;
        if (propName === "path") {
            pathExpr = prop.initializer;
        }
        else if (propName === "component") {
            const init = prop.initializer;
            if (ts.isIdentifier(init))
                componentName = init.text;
            // Lazy imports `() => import('./pages/X')` are common but require their
            // own resolution path; skip for now.
        }
    }
    if (!pathExpr || !componentName)
        return null;
    const pathValue = resolveExpressionToString(pathExpr, filePath, imports, localConsts, cache, 0);
    if (pathValue === null)
        return null;
    const composed = composeRoutePath(pathPrefix, pathValue);
    return {
        path: composed,
        componentName,
        componentFile: imports.get(componentName)?.filePath,
        declaredIn: filePath,
    };
}
function composeRoutePath(prefix, child) {
    // Empty child path inside a module → prefix is the URL.
    if (child === "")
        return prefix || "/";
    // Child already absolute → ignore the prefix (matches Vue Router semantics).
    if (child.startsWith("/"))
        return child;
    // Compose: ensure exactly one slash between prefix and child.
    const normalizedPrefix = prefix.replace(/\/$/, "");
    const normalizedChild = child.replace(/^\//, "");
    return normalizedPrefix + "/" + normalizedChild;
}
// ── defineModule aggregation (Directus) ────────────────────────────────────
/**
 * Detect `defineModule({ id: 'foo', routes: [...] })` calls and yield routes
 * from the nested array, prefixed with `/<id>`.
 *
 * This handles the Directus extension shape:
 *
 *   defineModule({
 *     id: 'notifications',
 *     routes: [{ path: '', component: NotificationsCollection }],
 *   })
 *
 * → emits one SourceRoute with path `/notifications`.
 */
function defineModuleRoutesFromCall(call, filePath, imports, localConsts, cache) {
    const callee = call.expression;
    if (!ts.isIdentifier(callee) || callee.text !== "defineModule")
        return null;
    const arg = call.arguments[0];
    if (!arg || !ts.isObjectLiteralExpression(arg))
        return null;
    let moduleId;
    let routesArray;
    for (const prop of arg.properties) {
        if (!ts.isPropertyAssignment(prop))
            continue;
        if (!ts.isIdentifier(prop.name))
            continue;
        const propName = prop.name.text;
        if (propName === "id") {
            const idVal = resolveExpressionToString(prop.initializer, filePath, imports, localConsts, cache, 0);
            if (idVal !== null)
                moduleId = idVal;
        }
        else if (propName === "routes" && ts.isArrayLiteralExpression(prop.initializer)) {
            routesArray = prop.initializer;
        }
    }
    if (!moduleId || !routesArray)
        return null;
    const prefix = "/" + moduleId.replace(/^\/+/, "");
    const out = [];
    for (const elt of routesArray.elements) {
        if (!ts.isObjectLiteralExpression(elt))
            continue;
        const r = routeFromObjectLiteral(elt, prefix, filePath, imports, localConsts, cache);
        if (r)
            out.push(r);
    }
    return out;
}
