"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.nextjsRules = void 0;
/**
 * Next.js Rules — Best practice checks for Next.js App Router projects
 */
const path = __importStar(require("path"));
// ─── Helper ───────────────────────────────────────────────────────
function diag(rule, filePath, message, opts = {}) {
    return {
        ruleId: rule.id,
        filePath,
        line: opts.line,
        severity: rule.severity,
        message,
        help: opts.help || rule.help,
        autoFixable: opts.autoFixable ?? false,
    };
}
// ─── Rules ────────────────────────────────────────────────────────
const missingRootLayout = {
    id: "nextjs/missing-root-layout",
    name: "Root Layout Required",
    category: "nextjs",
    severity: "error",
    description: "Next.js App Router requires a root layout.tsx",
    help: "Create app/layout.tsx with html and body tags wrapping {children}",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const hasLayout = await ctx.fileExists("app/layout.tsx") ||
            await ctx.fileExists("app/layout.jsx") ||
            await ctx.fileExists("src/app/layout.tsx") ||
            await ctx.fileExists("src/app/layout.jsx");
        if (!hasLayout) {
            results.push(diag(this, "app/layout.tsx", "No root layout found — App Router requires app/layout.tsx"));
        }
        return results;
    },
};
const layoutHasHtmlBody = {
    id: "nextjs/layout-html-body",
    name: "Layout Has HTML/Body",
    category: "nextjs",
    severity: "error",
    description: "Root layout must wrap children in <html> and <body> tags",
    help: "Add <html lang='en'><body>{children}</body></html> to root layout",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const layoutPaths = ["app/layout.tsx", "app/layout.jsx", "src/app/layout.tsx", "src/app/layout.jsx"];
        for (const lp of layoutPaths) {
            const content = await ctx.readFile(lp);
            if (!content)
                continue;
            if (!content.includes("<html")) {
                results.push(diag(this, lp, "Root layout missing <html> tag"));
            }
            if (!content.includes("<body")) {
                results.push(diag(this, lp, "Root layout missing <body> tag"));
            }
            break; // only check the first layout found
        }
        return results;
    },
};
const pageDefaultExport = {
    id: "nextjs/page-default-export",
    name: "Page Default Export",
    category: "nextjs",
    severity: "error",
    description: "Page files must have a default export",
    help: "Add 'export default function PageName()' to your page file",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const pages = await ctx.findFiles(/page\.(tsx|jsx|ts|js)$/);
        for (const p of pages) {
            const content = await ctx.readFile(p);
            if (!content)
                continue;
            if (!content.includes("export default")) {
                results.push(diag(this, p, `Page file missing default export`));
            }
        }
        return results;
    },
};
const clientDirective = {
    id: "nextjs/client-directive",
    name: "Client Directive Usage",
    category: "nextjs",
    severity: "warning",
    description: "Files using hooks/browser APIs should have 'use client' directive",
    help: "Add 'use client' at the top of files using useState, useEffect, onClick, etc.",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const clientSignals = [
            /\buseState\b/, /\buseEffect\b/, /\buseRef\b/, /\buseReducer\b/,
            /\buseCallback\b/, /\buseMemo\b/, /\buseContext\b/,
            /\bonClick\b/, /\bonChange\b/, /\bonSubmit\b/,
            /\bwindow\b/, /\bdocument\b/,
        ];
        const files = await ctx.findFiles(/\.(tsx|jsx)$/);
        for (const f of files) {
            // Skip layout/page files that could be server components
            if (/layout\.(tsx|jsx)$/.test(f) && !f.includes("components"))
                continue;
            const content = await ctx.readFile(f);
            if (!content)
                continue;
            if (content.includes('"use client"') || content.includes("'use client'"))
                continue;
            const needsClient = clientSignals.some(sig => sig.test(content));
            if (needsClient) {
                // Find which hook/API triggered it
                const trigger = clientSignals.find(sig => sig.test(content));
                const triggerName = trigger ? trigger.source.replace(/\\b/g, "") : "client API";
                results.push(diag(this, f, `Uses ${triggerName} but missing "use client" directive`, {
                    autoFixable: true,
                }));
            }
        }
        return results;
    },
    async fix(ctx, d) {
        const content = await ctx.readFile(d.filePath);
        if (!content)
            return false;
        const newContent = `"use client";\n\n${content}`;
        const abs = path.join(ctx.root, d.filePath);
        const { writeFile } = await Promise.resolve().then(() => __importStar(require("fs-extra")));
        await writeFile(abs, newContent);
        return true;
    },
};
const serverComponentHooks = {
    id: "nextjs/server-component-hooks",
    name: "No Hooks in Server Components",
    category: "nextjs",
    severity: "error",
    description: "Server components cannot use React hooks",
    help: "Add 'use client' directive or move hooks to a client component",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const hookPattern = /\b(useState|useEffect|useRef|useReducer|useCallback|useMemo|useContext)\s*\(/;
        const serverFiles = await ctx.findFiles(/(page|layout)\.(tsx|jsx)$/);
        for (const f of serverFiles) {
            const content = await ctx.readFile(f);
            if (!content)
                continue;
            if (content.includes('"use client"') || content.includes("'use client'"))
                continue;
            const hookPatternLocal = /\b(useState|useEffect|useRef|useReducer|useCallback|useMemo|useContext)\s*\(/;
            const lines = content.split("\n");
            for (let i = 0; i < lines.length; i++) {
                const line = lines[i];
                if (!line)
                    continue;
                const match = hookPatternLocal.exec(line);
                if (match && match[1]) {
                    results.push(diag(this, f, `Server component uses hook ${match[1]}()`, {
                        line: i + 1,
                    }));
                }
            }
        }
        return results;
    },
};
const metadataExport = {
    id: "nextjs/metadata-export",
    name: "Metadata Export",
    category: "nextjs",
    severity: "warning",
    description: "Public pages/layouts should export metadata or generateMetadata for SEO",
    help: "Add 'export const metadata = { title: ... }' or 'export async function generateMetadata()'",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const pages = await ctx.findFiles(/page\.(tsx|jsx|ts|js)$/);
        // Route groups that are typically auth-gated (no SEO needed)
        const authGatedGroups = new Set([
            "(pms)", "(admin)", "(dashboard)", "(agents)", "(app)",
            "(protected)", "(auth)", "(settings)", "(account)", "(manage)",
        ]);
        // Detect additional auth-gated groups by checking for auth patterns in layouts
        const layouts = await ctx.findFiles(/layout\.(tsx|jsx|ts|js)$/);
        const authPatterns = /\b(redirect|getServerSession|auth|session|middleware|cookies|signIn|requireAuth|useAuth)\b/;
        const detectedAuthGroups = new Set();
        for (const layout of layouts) {
            const content = await ctx.readFile(layout);
            if (!content)
                continue;
            if (authPatterns.test(content)) {
                // Extract route group from path, e.g. "app/(pms)/[slug]/layout.tsx" → "(pms)"
                const groupMatch = layout.match(/\(([^)]+)\)/);
                if (groupMatch) {
                    detectedAuthGroups.add(`(${groupMatch[1]})`);
                }
            }
        }
        for (const p of pages) {
            // Skip pages in auth-gated route groups
            const isAuthGated = [...authGatedGroups, ...detectedAuthGroups].some(group => p.includes(`/${group}/`) || p.startsWith(`${group}/`));
            if (isAuthGated)
                continue;
            // Skip API routes
            if (p.includes("/api/"))
                continue;
            const content = await ctx.readFile(p);
            if (!content)
                continue;
            // Skip client components (they can't export metadata)
            if (content.includes('"use client"') || content.includes("'use client'"))
                continue;
            // Check if there's metadata or generateMetadata
            const hasMetadata = content.includes("export const metadata") ||
                content.includes("export async function generateMetadata") ||
                content.includes("export function generateMetadata");
            if (!hasMetadata) {
                results.push(diag(this, p, "Public page missing metadata export for SEO"));
            }
        }
        return results;
    },
};
const imageComponent = {
    id: "nextjs/image-component",
    name: "Use next/image",
    category: "nextjs",
    severity: "warning",
    description: "Use next/image instead of <img> for optimized images",
    help: "Import Image from 'next/image' and replace <img> tags",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const files = await ctx.findFiles(/\.(tsx|jsx)$/);
        for (const f of files) {
            const content = await ctx.readFile(f);
            if (!content)
                continue;
            const lines = content.split("\n");
            for (let i = 0; i < lines.length; i++) {
                const line = lines[i];
                if (!line)
                    continue;
                if (/<img\s/i.test(line) && !line.includes("eslint-disable")) {
                    results.push(diag(this, f, "Uses <img> instead of next/image", { line: i + 1 }));
                }
            }
        }
        return results;
    },
};
const linkComponent = {
    id: "nextjs/link-component",
    name: "Use next/link",
    category: "nextjs",
    severity: "warning",
    description: "Use next/link for internal navigation instead of <a> tags",
    help: "Import Link from 'next/link' and replace <a href='/...'> with <Link href='/...'>",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const files = await ctx.findFiles(/\.(tsx|jsx)$/);
        for (const f of files) {
            const content = await ctx.readFile(f);
            if (!content)
                continue;
            const lines = content.split("\n");
            for (let i = 0; i < lines.length; i++) {
                const line = lines[i];
                if (!line)
                    continue;
                // Match <a href="/something"> (internal links)
                const match = /<a\s[^>]*href=["'](\/[^"']*)/i.exec(line);
                if (match && !line.includes("eslint-disable")) {
                    results.push(diag(this, f, `Uses <a> for internal link "${match[1] ?? ""}" instead of next/link`, { line: i + 1 }));
                }
            }
        }
        return results;
    },
};
const routeHandlerMethods = {
    id: "nextjs/route-handler-methods",
    name: "Route Handler Exports",
    category: "nextjs",
    severity: "error",
    description: "API route handlers must export named HTTP methods (GET, POST, etc.)",
    help: "Export named functions: export async function GET(request) { ... }",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const routes = await ctx.findFiles(/route\.(ts|js|tsx|jsx)$/);
        for (const r of routes) {
            const content = await ctx.readFile(r);
            if (!content)
                continue;
            const httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
            const hasMethod = httpMethods.some(m => content.includes(`export async function ${m}`) ||
                content.includes(`export function ${m}`) ||
                content.includes(`export const ${m}`));
            if (!hasMethod) {
                results.push(diag(this, r, "Route handler doesn't export any HTTP methods (GET/POST/etc.)"));
            }
        }
        return results;
    },
};
const loadingFiles = {
    id: "nextjs/loading-states",
    name: "Loading States",
    category: "nextjs",
    severity: "warning",
    description: "Route groups with data fetching should have loading.tsx for better UX",
    help: "Create loading.tsx alongside page.tsx for Suspense-based loading states. Run: mycontext tidy --fix",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const pages = await ctx.findFiles(/page\.(tsx|jsx)$/);
        for (const p of pages) {
            const content = await ctx.readFile(p);
            if (!content)
                continue;
            const hasAsyncData = content.includes("async function") && (content.includes("fetch(") || content.includes("await "));
            if (!hasAsyncData)
                continue;
            const dir = path.dirname(p);
            const hasLoading = await ctx.fileExists(path.join(dir, "loading.tsx")) ||
                await ctx.fileExists(path.join(dir, "loading.jsx"));
            if (!hasLoading) {
                results.push(diag(this, p, "Page with async data has no loading.tsx for Suspense", {
                    autoFixable: true,
                }));
            }
        }
        return results;
    },
    async fix(ctx, d) {
        const dir = path.dirname(d.filePath);
        const loadingPath = path.join(dir, "loading.tsx");
        const absPath = path.join(ctx.root, loadingPath);
        try {
            const { SkeletonService } = await Promise.resolve().then(() => __importStar(require("../../services/SkeletonService")));
            const service = new SkeletonService(ctx.root);
            const suggestions = await service.auditSkeletons();
            const loadingSuggestion = suggestions.find(s => s.type === "missing-loading" && s.pagePath === d.filePath);
            if (loadingSuggestion) {
                await service.ensureSkeletonComponent();
                // Also generate the skeleton itself if missing
                const skeletonSuggestion = suggestions.find(s => s.type === "missing-skeleton" && s.pagePath === d.filePath);
                if (skeletonSuggestion) {
                    await service.applySuggestion(skeletonSuggestion);
                }
                return await service.applySuggestion(loadingSuggestion);
            }
        }
        catch {
            // Fall through to generic skeleton
        }
        const skeleton = `export default function Loading() {
  return (
    <div className="flex items-center justify-center min-h-[400px]">
      <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
    </div>
  );
}
`;
        const { writeFile, ensureDir } = await Promise.resolve().then(() => __importStar(require("fs-extra")));
        await ensureDir(path.dirname(absPath));
        await writeFile(absPath, skeleton);
        return true;
    },
};
// ─── Skeleton Pattern Rules ──────────────────────────────────────
const skeletonLoadingPattern = {
    id: "nextjs/skeleton-loading-pattern",
    name: "Skeleton Loading Pattern",
    category: "nextjs",
    severity: "warning",
    description: "Client components with isLoading should return a structural skeleton, not a generic spinner",
    help: "Replace spinner-based isLoading guards with page skeletons. Run: mycontext tidy --fix",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const files = await ctx.findFiles(/\.(tsx|jsx)$/);
        for (const f of files) {
            const content = await ctx.readFile(f);
            if (!content)
                continue;
            if (!content.includes('"use client"') && !content.includes("'use client'"))
                continue;
            const hasIsLoading = /if\s*\(\s*isLoading\s*\)/.test(content);
            if (!hasIsLoading)
                continue;
            // Detect generic spinner patterns in the isLoading return
            const spinnerPatterns = [
                /animate-spin/,
                /spinner/i,
                /loading\.\.\./i,
                /flex.*items-center.*justify-center.*min-h/,
            ];
            const isLoadingBlock = content.match(/if\s*\(\s*isLoading\s*\)\s*(?:return\s*\([\s\S]*?\);|return\s*[\s\S]*?;)/);
            if (!isLoadingBlock?.[0])
                continue;
            const hasSpinner = spinnerPatterns.some(p => p.test(isLoadingBlock[0]));
            const hasSkeleton = /Skeleton|skeleton/i.test(isLoadingBlock[0]);
            if (hasSpinner && !hasSkeleton) {
                results.push(diag(this, f, "isLoading guard uses a generic spinner instead of a structural skeleton", {
                    autoFixable: false,
                    help: "Import a page skeleton from @/components/ui/page-skeletons and return it in the isLoading guard",
                }));
            }
        }
        return results;
    },
};
const missingErrorBoundary = {
    id: "nextjs/missing-error-boundary",
    name: "Error Boundary",
    category: "nextjs",
    severity: "warning",
    description: "Route groups with page.tsx should have error.tsx for graceful error handling",
    help: "Create error.tsx alongside page.tsx. Run: mycontext tidy --fix",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const pages = await ctx.findFiles(/page\.(tsx|jsx)$/);
        for (const p of pages) {
            const dir = path.dirname(p);
            const hasError = await ctx.fileExists(path.join(dir, "error.tsx")) ||
                await ctx.fileExists(path.join(dir, "error.jsx"));
            if (!hasError) {
                results.push(diag(this, p, "Route has no error.tsx for error boundary", {
                    autoFixable: true,
                }));
            }
        }
        return results;
    },
    async fix(ctx, d) {
        const dir = path.dirname(d.filePath);
        const errorPath = path.join(dir, "error.tsx");
        const absPath = path.join(ctx.root, errorPath);
        const errorPage = `"use client";

import { useEffect } from "react";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    console.error(error);
  }, [error]);

  return (
    <div className="flex min-h-[400px] flex-col items-center justify-center gap-4 p-6">
      <h2 className="text-xl font-semibold">Something went wrong</h2>
      {error.message && (
        <p className="text-sm text-muted-foreground">{error.message}</p>
      )}
      <button
        onClick={reset}
        className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90"
      >
        Try again
      </button>
    </div>
  );
}
`;
        const { writeFile, ensureDir } = await Promise.resolve().then(() => __importStar(require("fs-extra")));
        await ensureDir(path.dirname(absPath));
        await writeFile(absPath, errorPage);
        return true;
    },
};
const missingNotFound = {
    id: "nextjs/missing-not-found",
    name: "Not Found Page",
    category: "nextjs",
    severity: "warning",
    description: "Dynamic route segments should have not-found.tsx for missing resources",
    help: "Create not-found.tsx in dynamic route directories ([slug], [id], etc.). Run: mycontext tidy --fix",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        const pages = await ctx.findFiles(/page\.(tsx|jsx)$/);
        for (const p of pages) {
            const dir = path.dirname(p);
            const dirName = path.basename(dir);
            // Only check dynamic route segments
            if (!/^\[.+\]$/.test(dirName))
                continue;
            const hasNotFound = await ctx.fileExists(path.join(dir, "not-found.tsx")) ||
                await ctx.fileExists(path.join(dir, "not-found.jsx"));
            if (!hasNotFound) {
                results.push(diag(this, p, `Dynamic route ${dirName} has no not-found.tsx`, {
                    autoFixable: true,
                }));
            }
        }
        return results;
    },
    async fix(ctx, d) {
        const dir = path.dirname(d.filePath);
        const notFoundPath = path.join(dir, "not-found.tsx");
        const absPath = path.join(ctx.root, notFoundPath);
        const notFoundPage = `import Link from "next/link";

export default function NotFound() {
  return (
    <div className="flex min-h-[400px] flex-col items-center justify-center gap-4 p-6">
      <div className="text-6xl font-bold text-muted-foreground">404</div>
      <h2 className="text-xl font-semibold">Page not found</h2>
      <p className="text-sm text-muted-foreground">
        Sorry, we couldn&apos;t find the page you&apos;re looking for.
      </p>
      <Link
        href="/"
        className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90"
      >
        Go back home
      </Link>
    </div>
  );
}
`;
        const { writeFile, ensureDir } = await Promise.resolve().then(() => __importStar(require("fs-extra")));
        await ensureDir(path.dirname(absPath));
        await writeFile(absPath, notFoundPage);
        return true;
    },
};
const staleSkeletonRule = {
    id: "nextjs/stale-skeleton",
    name: "Skeleton Freshness",
    category: "nextjs",
    severity: "warning",
    description: "Generated skeletons should structurally match their source component",
    help: "Regenerate stale skeletons. Run: mycontext tidy --fix",
    appliesTo: ["nextjs"],
    async check(ctx) {
        const results = [];
        // Find page-skeletons file
        const skeletonPaths = [
            "components/ui/page-skeletons.tsx",
            "src/components/ui/page-skeletons.tsx",
        ];
        let skeletonContent = null;
        let skeletonFile = "";
        for (const sp of skeletonPaths) {
            const content = await ctx.readFile(sp);
            if (content) {
                skeletonContent = content;
                skeletonFile = sp;
                break;
            }
        }
        if (!skeletonContent || !skeletonContent.includes("@generated by mycontext"))
            return results;
        // Extract exported skeleton names
        const exportedSkeletons = [...skeletonContent.matchAll(/export function (\w+Skeleton)/g)]
            .map(m => m[1])
            .filter(Boolean);
        // For each skeleton, check if the source component still exists
        const clientFiles = await ctx.findFiles(/(?:Client|View|Content)\.(tsx|jsx)$/);
        for (const skelName of exportedSkeletons) {
            const baseName = skelName.replace(/Skeleton$/, "");
            const sourceFile = clientFiles.find(f => {
                const fileName = path.basename(f, path.extname(f));
                return fileName.includes(baseName);
            });
            if (!sourceFile) {
                results.push(diag(this, skeletonFile, `Skeleton "${skelName}" has no matching source component — may be stale`, {
                    help: "Remove the stale skeleton or regenerate with: mycontext tidy --fix",
                }));
            }
        }
        return results;
    },
};
exports.nextjsRules = [
    missingRootLayout,
    layoutHasHtmlBody,
    pageDefaultExport,
    clientDirective,
    serverComponentHooks,
    metadataExport,
    imageComponent,
    linkComponent,
    routeHandlerMethods,
    loadingFiles,
    skeletonLoadingPattern,
    missingErrorBoundary,
    missingNotFound,
    staleSkeletonRule,
];
//# sourceMappingURL=nextjs-rules.js.map