Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function About() {
return <div>About</div>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/app/api/users/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function GET() {
return Response.json({ users: [] });
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function BlogPost({ params }: { params: { slug: string } }) {
return <div>Blog: {params.slug}</div>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Settings() {
return <div>Settings</div>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/app/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return <div>Default</div>;
}
4 changes: 4 additions & 0 deletions scripts/fixtures/nextjs-pages/app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"use client";
export default function Error() {
return <div>Error</div>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html><body>{children}</body></html>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Loading() {
return <div>Loading...</div>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function NotFound() {
return <div>Not found</div>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Home() {
return <div>Home</div>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/app/template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Template({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
1 change: 1 addition & 0 deletions scripts/fixtures/nextjs-pages/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
7 changes: 7 additions & 0 deletions scripts/fixtures/nextjs-pages/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "nextjs-pages-fixture",
"dependencies": {
"next": "14.0.0",
"react": "18.0.0"
}
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />;
}
4 changes: 4 additions & 0 deletions scripts/fixtures/nextjs-pages/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (<Html><Head /><body><Main /><NextScript /></body></Html>);
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/pages/about/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function About() {
return <div>About</div>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/pages/api/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function handler(req, res) {
res.status(200).json({ ok: true });
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/pages/blog/[slug].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function BlogPost() {
return <div>Blog Post</div>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Home() {
return <div>Home</div>;
}
3 changes: 3 additions & 0 deletions scripts/fixtures/nextjs-pages/pages/products/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Products() {
return <div>Products</div>;
}
4 changes: 3 additions & 1 deletion src/extractors/fastapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export const fastapi: Extractor = {
const funcParams = m[6]!;
const line = lines.lineAt(m.index);

if (httpMethod === "WEBSOCKET") httpMethod = "WS";
const isWebSocket = httpMethod === "WEBSOCKET";
if (isWebSocket) httpMethod = "WS";

const prefix = routerPrefixes[varName] ?? "";
const fullPath = normalizePath(prefix + routePath);
Expand All @@ -83,6 +84,7 @@ export const fastapi: Extractor = {
endpoints.push(
endpoint({
method: httpMethod,
kind: isWebSocket ? "websocket" : "api",
path: fullPath,
handler: funcName,
file: rel,
Expand Down
201 changes: 201 additions & 0 deletions src/extractors/nextjs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { describe, expect, test } from "bun:test";
import { resolve } from "path";
import { nextjs } from "./nextjs.ts";
import { createScanContext } from "../scan-context.ts";

const FIXTURE = resolve(import.meta.dir, "../../scripts/fixtures/nextjs-pages");

function extract(fixturePath: string = FIXTURE) {
const ctx = createScanContext(fixturePath);
return nextjs.extract(ctx);
}

describe("nextjs page route extraction", () => {
const endpoints = extract();

// ---- App Router pages ----

test("extracts app router root page (/)", () => {
const ep = endpoints.find(
(e) => e.kind === "page" && e.path === "/" && e.file.includes("app/page"),
);
expect(ep).toBeDefined();
expect(ep!.method).toBe("GET");
expect(ep!.handler).toBe("Page");
expect(ep!.framework).toBe("nextjs");
});

test("extracts app router /about page", () => {
const ep = endpoints.find(
(e) =>
e.kind === "page" && e.path === "/about" && e.file.includes("app/"),
);
expect(ep).toBeDefined();
expect(ep!.method).toBe("GET");
});

test("extracts app router dynamic [slug] page", () => {
const ep = endpoints.find(
(e) => e.kind === "page" && e.path === "/blog/[slug]",
);
expect(ep).toBeDefined();
expect(ep!.method).toBe("GET");
expect(ep!.params.length).toBeGreaterThan(0);
expect(ep!.params[0]!.name).toBe("slug");
});

test("extracts app router nested page", () => {
const ep = endpoints.find(
(e) => e.kind === "page" && e.path === "/dashboard/settings",
);
expect(ep).toBeDefined();
expect(ep!.method).toBe("GET");
});

test("excludes layout.tsx from app router pages", () => {
const layouts = endpoints.filter(
(e) => e.kind === "page" && e.file.includes("layout"),
);
expect(layouts).toHaveLength(0);
});

test("excludes loading.tsx from app router pages", () => {
const loading = endpoints.filter(
(e) => e.kind === "page" && e.file.includes("loading"),
);
expect(loading).toHaveLength(0);
});

test("excludes error.tsx from app router pages", () => {
const errors = endpoints.filter(
(e) => e.kind === "page" && e.file.includes("error"),
);
expect(errors).toHaveLength(0);
});

test("excludes not-found.tsx from app router pages", () => {
const notFounds = endpoints.filter(
(e) => e.kind === "page" && e.file.includes("not-found"),
);
expect(notFounds).toHaveLength(0);
});

test("excludes template.tsx from app router pages", () => {
const templates = endpoints.filter(
(e) => e.kind === "page" && e.file.includes("template"),
);
expect(templates).toHaveLength(0);
});

test("excludes default.tsx from app router pages", () => {
const defaults = endpoints.filter(
(e) => e.kind === "page" && e.file.includes("default"),
);
expect(defaults).toHaveLength(0);
});

// ---- App Router API routes still work ----

test("still extracts app router API routes", () => {
const ep = endpoints.find(
(e) => e.kind === "api" && e.path === "/api/users",
);
expect(ep).toBeDefined();
expect(ep!.method).toBe("GET");
expect(ep!.handler).toBe("GET");
});

// ---- Pages Router pages ----

test("extracts pages router root page (pages/index.tsx → /)", () => {
const ep = endpoints.find(
(e) =>
e.kind === "page" && e.path === "/" && e.file.includes("pages/index"),
);
expect(ep).toBeDefined();
expect(ep!.method).toBe("GET");
expect(ep!.handler).toBe("Page");
});

test("extracts pages router /about (pages/about/index.tsx → /about)", () => {
const ep = endpoints.find(
(e) =>
e.kind === "page" &&
e.path === "/about" &&
e.file.includes("pages/about"),
);
expect(ep).toBeDefined();
expect(ep!.method).toBe("GET");
});

test("extracts pages router dynamic route (pages/blog/[slug].tsx → /blog/[slug])", () => {
const ep = endpoints.find(
(e) =>
e.kind === "page" &&
e.path === "/blog/[slug]" &&
e.file.includes("pages/blog"),
);
expect(ep).toBeDefined();
expect(ep!.method).toBe("GET");
expect(ep!.params.length).toBeGreaterThan(0);
expect(ep!.params[0]!.name).toBe("slug");
});

test("extracts pages router /products (pages/products/index.tsx → /products)", () => {
const ep = endpoints.find(
(e) =>
e.kind === "page" &&
e.path === "/products" &&
e.file.includes("pages/products"),
);
expect(ep).toBeDefined();
expect(ep!.method).toBe("GET");
});

test("excludes _app.tsx from pages router pages", () => {
const apps = endpoints.filter(
(e) => e.kind === "page" && e.file.includes("_app"),
);
expect(apps).toHaveLength(0);
});

test("excludes _document.tsx from pages router pages", () => {
const docs = endpoints.filter(
(e) => e.kind === "page" && e.file.includes("_document"),
);
expect(docs).toHaveLength(0);
});

test("does not emit pages/api/ files as pages (handled as API)", () => {
const apiPages = endpoints.filter(
(e) => e.kind === "page" && e.path.startsWith("/api"),
);
expect(apiPages).toHaveLength(0);
});

test("still extracts pages/api/ as API endpoints", () => {
const ep = endpoints.find(
(e) => e.kind === "api" && e.path === "/api/health",
);
expect(ep).toBeDefined();
expect(ep!.method).toBe("ANY");
expect(ep!.handler).toBe("default");
});

// ---- Kind validation ----

test("all page endpoints have kind=page", () => {
const pages = endpoints.filter((e) => e.handler === "Page");
expect(pages.length).toBeGreaterThan(0);
for (const p of pages) {
expect(p.kind).toBe("page");
}
});

test("all page endpoints have method=GET", () => {
const pages = endpoints.filter((e) => e.kind === "page");
for (const p of pages) {
expect(p.method).toBe("GET");
}
});
});
Loading
Loading