Repository: tatethurston/nextjs-routes
Pull request #90 (2022-10-28 23:20:50)

app directory


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7fc19a..650724a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,9 @@
 
 ## 0.1.6
 
-- `nextjs-routes` now accepts path strings for static routes:
+- Support [Next 13 app (beta)](https://nextjs.org/docs/advanced-features/custom-app)
+
+- Accept path strings for static routes:
 
   ```tsx
   <Link href="/foo">Foo</Link>
@@ -10,7 +12,7 @@
 
   Thanks [@MariaSolOs](https://github.com/MariaSolOs) for the contribution!
 
-- `nextjs-routes` now uses function overloads for `Link` and `router.push` and `router.replace`. This yields better hints for typos in pathnames:
+- Use function overloads for `Link` and `router.push` and `router.replace`. This yields better hints for typos in pathnames:
 
   ```tsx
   <Link href={{ pathname: "/foosy/[foo]" }}>Foo</Link>
diff --git a/public.package.json b/public.package.json
index cc908fb..148c623 100644
--- a/public.package.json
+++ b/public.package.json
@@ -1,6 +1,6 @@
 {
   "name": "nextjs-routes",
-  "version": "0.1.5",
+  "version": "0.1.6",
   "description": "Type safe routing for Next.js",
   "license": "MIT",
   "author": "Tate <tatethurston@gmail.com>",
diff --git a/src/config.ts b/src/config.ts
index 8a7a12c..852b86b 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -6,7 +6,7 @@
 import type { NextConfig } from "next";
 import { join } from "path";
 import type { Configuration, WebpackPluginInstance } from "webpack";
-import { getPagesDirectory } from "./utils.js";
+import { getAppDirectory, getPagesDirectory } from "./utils.js";
 import { watch } from "chokidar";
 import { logger, writeNextjsRoutes } from "./core.js";
 
@@ -32,16 +32,17 @@ class NextJSRoutesPlugin implements WebpackPluginInstance {
   ) {}
 
   apply() {
-    const pagesDirectory = getPagesDirectory();
-    if (pagesDirectory) {
+    const watchDirs = [getPagesDirectory(), getAppDirectory()]
+      .filter((x) => x != undefined)
+      .map((dir) => join(process.cwd(), dir as string));
+
+    if (watchDirs.length > 0) {
       const options = {
         ...this.config,
         ...this.options,
-        pagesDirectory,
       };
       if (this.options.watch) {
-        const dir = join(process.cwd(), pagesDirectory);
-        const watcher = watch(dir, {
+        const watcher = watch(watchDirs, {
           persistent: true,
         });
         // batch changes
@@ -51,10 +52,11 @@ class NextJSRoutesPlugin implements WebpackPluginInstance {
         writeNextjsRoutes(options);
       }
     } else {
-      logger.error(`Could not find a Next.js pages directory. Expected to find either pages(1) or src/pages(2).
+      logger.error(`Could not find a Next.js pages directory. Expected to find either 'pages' (1), 'src/pages' (2), or 'app' (3) in your project root.
 
   1. https://nextjs.org/docs/basic-features/pages
   2. https://nextjs.org/docs/advanced-features/src-directory
+  3. https://nextjs.org/blog/next-13#app-directory-beta
   `);
     }
   }
diff --git a/src/core.ts b/src/core.ts
index c36e212..7aea715 100644
--- a/src/core.ts
+++ b/src/core.ts
@@ -1,10 +1,7 @@
 import { existsSync, mkdirSync, writeFileSync } from "fs";
 import { I18NConfig } from "next/dist/server/config-shared.js";
-import { join } from "path";
-import { findFiles, getPagesDirectory } from "./utils.js";
-
-const NEXTJS_NON_ROUTABLE = ["/_app", "/_document", "/_error", "/middleware"];
-const DYNAMIC_SEGMENT_RE = /\[(.*?)\]/g;
+import { join, parse, sep } from "path";
+import { findFiles, getAppDirectory, getPagesDirectory } from "./utils.js";
 
 type QueryType = "dynamic" | "catch-all" | "optional-catch-all";
 
@@ -17,24 +14,8 @@ function convertWindowsPathToUnix(file: string): string {
   return file.replace(/\\/g, "/");
 }
 
-export function nextRoutes(files: string[], pagesDirectory: string): Route[] {
-  const pathnames = files
-    // remove page directory path
-    .map((file) => file.replace(pagesDirectory, ""))
-    // remove file extensions (.tsx, .test.tsx)
-    .map((file) => file.replace(/(\.\w+)+$/, ""))
-    // remove duplicates from file extension removal (eg foo.ts and foo.test.ts)
-    .filter((file, idx, array) => array.indexOf(file) === idx)
-    // normalize paths from windows users
-    .map(convertWindowsPathToUnix)
-    // remove index if present (/foos/index.ts is the same as /foos.ts)
-    .map((file) => file.replace(/index$/, ""))
-    // remove trailing slash if present
-    .map((file) =>
-      file.endsWith("/") && file.length > 2 ? file.slice(0, -1) : file
-    )
-    // exclude nextjs special routes
-    .filter((file) => !NEXTJS_NON_ROUTABLE.includes(file));
+export function nextRoutes(pathnames: string[]): Route[] {
+  const DYNAMIC_SEGMENT_RE = /\[(.*?)\]/g;
 
   return pathnames.map((pathname) => {
     const segments = pathname.match(DYNAMIC_SEGMENT_RE) ?? [];
@@ -268,10 +249,6 @@ export const logger: Pick<Console, "error"> = {
 };
 
 interface NextJSRoutesOptions {
-  /**
-   * The directory where pages are located;
-   */
-  pagesDirectory: string;
   /**
    * The file path indicating the output directory where the generated route types
    * should be written to (e.g.: "types").
@@ -290,6 +267,63 @@ interface NextJSRoutesOptions {
   i18n?: I18NConfig | null;
 }
 
+export function getAppRoutes(files: string[], opts: Opts): string[] {
+  return (
+    commonProcessing(files, opts)
+      // app pages must be named 'page'
+      .filter((file) => parse(file).name === "page")
+      .map((file) =>
+        // transform filepath to url path
+        file
+          .split(sep)
+          // remove named groups
+          .filter((segment) => segment.startsWith("(") && segment.endsWith(")"))
+          // remove page
+          .filter((file) => parse(file).name === "page")
+          .join(sep)
+      )
+  );
+}
+
+export function getPageRoutes(files: string[], opts: Opts): string[] {
+  const NEXTJS_NON_ROUTABLE = ["/_app", "/_document", "/_error", "/middleware"];
+
+  return (
+    commonProcessing(files, opts)
+      // remove index if present (/foos/index.ts is the same as /foos.ts)
+      .map((file) => file.replace(/index$/, ""))
+      // remove trailing slash if present
+      .map((file) =>
+        file.endsWith("/") && file.length > 2 ? file.slice(0, -1) : file
+      )
+      // exclude nextjs special routes
+      .filter((file) => !NEXTJS_NON_ROUTABLE.includes(file))
+  );
+}
+
+interface Opts {
+  pageExtensions: string[];
+  directory: string;
+}
+
+function commonProcessing(paths: string[], opts: Opts): string[] {
+  return (
+    paths
+      // filter page extensions
+      .filter((file) => {
+        return opts.pageExtensions.some((ext) => file.endsWith(ext));
+      })
+      // remove file extensions (.tsx, .test.tsx)
+      .map((file) => file.replace(/(\.\w+)+$/, ""))
+      // remove duplicates from file extension removal (eg foo.ts and foo.test.ts)
+      .filter((file, idx, array) => array.indexOf(file) === idx)
+      // remove page directory path
+      .map((file) => file.replace(opts.directory, ""))
+      // normalize paths from windows users
+      .map(convertWindowsPathToUnix)
+  );
+}
+
 export function writeNextjsRoutes(options: NextJSRoutesOptions): void {
   const defaultOptions = {
     outDir: "",
@@ -299,14 +333,28 @@ export function writeNextjsRoutes(options: NextJSRoutesOptions): void {
     ...defaultOptions,
     ...options,
   };
+  const files = [];
+  const pagesDirectory = getPagesDirectory();
+  if (pagesDirectory) {
+    const routes = getPageRoutes(findFiles(join(".", pagesDirectory)), {
+      pageExtensions: opts.pageExtensions,
+      directory: pagesDirectory,
+    });
+    files.push(...routes);
+  }
+  const appDirectory = getAppDirectory();
+  if (appDirectory) {
+    const routes = getAppRoutes(findFiles(join(".", appDirectory)), {
+      pageExtensions: opts.pageExtensions,
+      directory: appDirectory,
+    });
+    files.push(...routes);
+  }
+  const outputFilepath = join(opts.outDir, "nextjs-routes.d.ts");
   if (opts.outDir && !existsSync(opts.outDir)) {
     mkdirSync(opts.outDir, { recursive: true });
   }
-  const outputFilepath = join(opts.outDir, "nextjs-routes.d.ts");
-  const files = findFiles(join(".", opts.pagesDirectory)).filter((file) => {
-    return opts.pageExtensions.some((ext) => file.endsWith(ext));
-  });
-  const routes = nextRoutes(files, opts.pagesDirectory);
+  const routes = nextRoutes(files);
   const generated = generate(routes, opts);
   writeFileSync(outputFilepath, generated);
 }
@@ -315,17 +363,18 @@ export function cli(): void {
   console.warn(
     `[nextjs-routes]: Direct invocation of nextjs-routes has been deprecated in favor of automatic regeneration via 'withRoutes': https://github.com/tatethurston/nextjs-routes#installation--usage-. See https://github.com/tatethurston/nextjs-routes/issues/63 for the motivation behind this change or to voice any concerns.`
   );
-  const pagesDirectory = getPagesDirectory();
-  if (!pagesDirectory) {
-    logger.error(`Could not find a Next.js pages directory. Expected to find either pages(1) or src/pages(2).
+  const dirs = [getPagesDirectory(), getAppDirectory()].filter(
+    (x) => x != undefined
+  );
+  if (dirs.length === 0) {
+    logger.error(`Could not find a Next.js pages directory. Expected to find either 'pages' (1), 'src/pages' (2), or 'app' (3) in your project root.
 
   1. https://nextjs.org/docs/basic-features/pages
   2. https://nextjs.org/docs/advanced-features/src-directory
+  3. https://nextjs.org/blog/next-13#app-directory-beta
   `);
     process.exit(1);
   } else {
-    writeNextjsRoutes({
-      pagesDirectory,
-    });
+    writeNextjsRoutes({});
   }
 }
diff --git a/src/utils.ts b/src/utils.ts
index 8171717..976431d 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -11,6 +11,12 @@ export function getPagesDirectory(): string | undefined {
   }
 }
 
+export function getAppDirectory(): string | undefined {
+  if (existsSync("app")) {
+    return "app";
+  }
+}
+
 // istanbul ignore next: io mocking not worthwhile
 export function findFiles(entry: string): string[] {
   return readdirSync(entry).flatMap((file) => {


Test patch:
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
index 9041ebf..269437e 100644
--- a/e2e/package-lock.json
+++ b/e2e/package-lock.json
@@ -20,7 +20,7 @@
     },
     "../dist": {
       "name": "nextjs-routes",
-      "version": "0.1.5",
+      "version": "0.1.6",
       "dev": true,
       "license": "MIT",
       "dependencies": {
diff --git a/src/__snapshots__/core.test.ts.snap b/src/__snapshots__/core.test.ts.snap
index dfa4c09..96b9429 100644
--- a/src/__snapshots__/core.test.ts.snap
+++ b/src/__snapshots__/core.test.ts.snap
@@ -590,6 +590,148 @@ declare module "next/router" {
 ]
 `;
 
+exports[`route generation dedupes 1`] = `
+[
+  [
+    "nextjs-routes.d.ts",
+    "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+// This file will be automatically regenerated when your Next.js server is running.
+/* eslint-disable */
+
+// prettier-ignore
+declare module "nextjs-routes" {
+  export type Route =
+    | { pathname: "/"; query?: Query | undefined };
+
+  type Query<Params = {}> = Params & {
+    [key: string]: string | string[] | undefined;
+  };
+
+  type QueryForPathname = {
+    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
+  };
+
+  export type RoutedQuery<P extends Route["pathname"]> = QueryForPathname[P];
+
+  export type Locale = undefined;
+
+  /**
+   * A typesafe utility function for generating paths in your application.
+   *
+   * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar".
+   */
+  export declare function route(r: Route): string;
+}
+
+// prettier-ignore
+declare module "next/link" {
+  import type { Route } from "nextjs-routes";
+  import type { LinkProps as NextLinkProps } from "next/dist/client/link";
+  import type { PropsWithChildren, MouseEventHandler } from "react";
+  export * from "next/dist/client/link";
+
+  type Query = { query?: { [key: string]: string | string[] | undefined } };
+  type StaticRoute = Exclude<Route, { query: any }>["pathname"];
+
+  export interface LinkProps<Href extends Route | Query = Route | Query>
+    extends Omit<NextLinkProps, "href" | "locale"> {
+    href: Href;
+    locale?: false;
+  }
+
+  type LinkReactElement = DetailedReactHTMLElement<
+    {
+      onMouseEnter?: MouseEventHandler<Element> | undefined;
+      onClick: MouseEventHandler;
+      href?: string | undefined;
+      ref?: any;
+    },
+    HTMLElement
+  >;
+
+  declare function Link(
+    props: PropsWithChildren<LinkProps<Route>>
+  ): LinkReactElement;
+  declare function Link(
+    props: PropsWithChildren<LinkProps<StaticRoute>>
+  ): LinkReactElement;
+  declare function Link(
+    props: PropsWithChildren<LinkProps<Query>>
+  ): LinkReactElement;
+
+  export default Link;
+}
+
+// prettier-ignore
+declare module "next/router" {
+  import type { Locale, Route, RoutedQuery } from "nextjs-routes";
+  import type { NextRouter as Router } from "next/dist/client/router";
+  export * from "next/dist/client/router";
+  export { default } from "next/dist/client/router";
+
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
+  type StaticRoute = Exclude<Route, { query: any }>["pathname"];
+
+  interface TransitionOptions extends Omit<NextTransitionOptions, "locale"> {
+    locale?: false;
+  };
+
+  export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
+    extends Omit<
+      Router,
+      | "push"
+      | "replace"
+      | "locale"
+      | "locales"
+      | "defaultLocale"
+      | "domainLocales"
+    > {
+    defaultLocale?: undefined;
+    domainLocales?: undefined;
+    locale?: Locale;
+    locales?: undefined;
+    pathname: P;
+    push(
+      url: Route,
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    push(
+      url: StaticRoute,
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    push(
+      url: { query: { [key: string]: string | string[] | undefined } },
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    query: RoutedQuery<P>;
+    replace(
+      url: Route,
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    replace(
+      url: StaticRoute,
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    replace(
+      url: { query: { [key: string]: string | string[] | undefined } },
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    route: P;
+  }
+
+  export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
+}
+",
+  ],
+]
+`;
+
 exports[`route generation no routes 1`] = `
 [
   [
@@ -732,6 +874,148 @@ declare module "next/router" {
 ]
 `;
 
+exports[`route generation transforms windows paths 1`] = `
+[
+  [
+    "nextjs-routes.d.ts",
+    "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+// This file will be automatically regenerated when your Next.js server is running.
+/* eslint-disable */
+
+// prettier-ignore
+declare module "nextjs-routes" {
+  export type Route =
+    | { pathname: "src/pages/[foo]/bar"; query: Query<{ "foo": string }> };
+
+  type Query<Params = {}> = Params & {
+    [key: string]: string | string[] | undefined;
+  };
+
+  type QueryForPathname = {
+    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
+  };
+
+  export type RoutedQuery<P extends Route["pathname"]> = QueryForPathname[P];
+
+  export type Locale = undefined;
+
+  /**
+   * A typesafe utility function for generating paths in your application.
+   *
+   * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar".
+   */
+  export declare function route(r: Route): string;
+}
+
+// prettier-ignore
+declare module "next/link" {
+  import type { Route } from "nextjs-routes";
+  import type { LinkProps as NextLinkProps } from "next/dist/client/link";
+  import type { PropsWithChildren, MouseEventHandler } from "react";
+  export * from "next/dist/client/link";
+
+  type Query = { query?: { [key: string]: string | string[] | undefined } };
+  type StaticRoute = Exclude<Route, { query: any }>["pathname"];
+
+  export interface LinkProps<Href extends Route | Query = Route | Query>
+    extends Omit<NextLinkProps, "href" | "locale"> {
+    href: Href;
+    locale?: false;
+  }
+
+  type LinkReactElement = DetailedReactHTMLElement<
+    {
+      onMouseEnter?: MouseEventHandler<Element> | undefined;
+      onClick: MouseEventHandler;
+      href?: string | undefined;
+      ref?: any;
+    },
+    HTMLElement
+  >;
+
+  declare function Link(
+    props: PropsWithChildren<LinkProps<Route>>
+  ): LinkReactElement;
+  declare function Link(
+    props: PropsWithChildren<LinkProps<StaticRoute>>
+  ): LinkReactElement;
+  declare function Link(
+    props: PropsWithChildren<LinkProps<Query>>
+  ): LinkReactElement;
+
+  export default Link;
+}
+
+// prettier-ignore
+declare module "next/router" {
+  import type { Locale, Route, RoutedQuery } from "nextjs-routes";
+  import type { NextRouter as Router } from "next/dist/client/router";
+  export * from "next/dist/client/router";
+  export { default } from "next/dist/client/router";
+
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
+  type StaticRoute = Exclude<Route, { query: any }>["pathname"];
+
+  interface TransitionOptions extends Omit<NextTransitionOptions, "locale"> {
+    locale?: false;
+  };
+
+  export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
+    extends Omit<
+      Router,
+      | "push"
+      | "replace"
+      | "locale"
+      | "locales"
+      | "defaultLocale"
+      | "domainLocales"
+    > {
+    defaultLocale?: undefined;
+    domainLocales?: undefined;
+    locale?: Locale;
+    locales?: undefined;
+    pathname: P;
+    push(
+      url: Route,
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    push(
+      url: StaticRoute,
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    push(
+      url: { query: { [key: string]: string | string[] | undefined } },
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    query: RoutedQuery<P>;
+    replace(
+      url: Route,
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    replace(
+      url: StaticRoute,
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    replace(
+      url: { query: { [key: string]: string | string[] | undefined } },
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    route: P;
+  }
+
+  export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
+}
+",
+  ],
+]
+`;
+
 exports[`route generation typescript 1`] = `
 [
   [
diff --git a/src/core.test.ts b/src/core.test.ts
index 558dc86..63a813d 100644
--- a/src/core.test.ts
+++ b/src/core.test.ts
@@ -1,15 +1,16 @@
-import { writeFileSync } from "fs";
-import { nextRoutes, writeNextjsRoutes } from "./core.js";
+import { existsSync, writeFileSync } from "fs";
+import { writeNextjsRoutes } from "./core.js";
 import { findFiles } from "./utils.js";
 
 // eslint-disable-next-line @typescript-eslint/no-unsafe-return
 jest.mock("fs", () => ({
   ...jest.requireActual("fs"),
   writeFileSync: jest.fn(),
-  existsSync: jest.fn(() => true),
+  existsSync: jest.fn(() => false),
   mkdirSync: jest.fn(),
 }));
 const writeFileSyncMock = writeFileSync as jest.Mock;
+const existsSyncMock = existsSync as jest.Mock;
 
 // eslint-disable-next-line @typescript-eslint/no-unsafe-return
 jest.mock("./utils.js", () => ({
@@ -18,34 +19,36 @@ jest.mock("./utils.js", () => ({
 }));
 const findFilesMock = findFiles as jest.Mock;
 
-describe("nextRoutes", () => {
-  it("transforms windows paths", () => {
-    const pages = ["src\\pages\\[foo]\\bar\\index.ts"];
-    const { pathname } = nextRoutes(pages, "src\\pages")[0];
-    expect(pathname).toEqual("/[foo]/bar");
+describe("route generation", () => {
+  it("no routes", () => {
+    // getPageRoutes
+    existsSyncMock.mockImplementationOnce(() => true);
+    findFilesMock.mockReturnValueOnce([]);
+    writeNextjsRoutes({});
+    expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
   });
 
-  it("colocated test files", () => {
-    const pages = ["pages/index.tsx", "pages/index.test.tsx"];
-    expect(nextRoutes(pages, "pages")).toMatchInlineSnapshot(`
-      [
-        {
-          "pathname": "/",
-          "query": {},
-        },
-      ]
-    `);
+  it("transforms windows paths", () => {
+    // getPageRoutes src/pages
+    existsSyncMock
+      .mockImplementationOnce(() => false)
+      .mockImplementationOnce(() => true);
+    findFilesMock.mockReturnValueOnce(["src\\pages\\[foo]\\bar\\index.ts"]);
+    writeNextjsRoutes({});
+    expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
   });
-});
 
-describe("route generation", () => {
-  it("no routes", () => {
-    findFilesMock.mockReturnValueOnce([]);
-    writeNextjsRoutes({ pagesDirectory: "pages" });
+  it("dedupes", () => {
+    // getPageRoutes
+    existsSyncMock.mockImplementationOnce(() => true);
+    findFilesMock.mockReturnValueOnce(["pages/index.tsx", "pages/index.ts"]);
+    writeNextjsRoutes({});
     expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
   });
 
   it("typescript", () => {
+    // getPageRoutes
+    existsSyncMock.mockImplementationOnce(() => true);
     findFilesMock.mockReturnValueOnce([
       "pages/404.ts",
       "pages/[foo].ts",
@@ -71,19 +74,23 @@ describe("route generation", () => {
       "pages/settings/foo.ts",
       "pages/settings/index.ts",
     ]);
-    writeNextjsRoutes({ pagesDirectory: "pages" });
+    writeNextjsRoutes({});
     expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
   });
 
   describe("configuration", () => {
     describe("pageExtensions", () => {
       it("default", () => {
+        // getPageRoutes
+        existsSyncMock.mockImplementationOnce(() => true);
         findFilesMock.mockReturnValueOnce(["pages/404.ts", "pages/404.md"]);
-        writeNextjsRoutes({ pagesDirectory: "pages" });
+        writeNextjsRoutes({});
         expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
       });
 
       it("configured", () => {
+        // getPageRoutes
+        existsSyncMock.mockImplementationOnce(() => true);
         findFilesMock.mockReturnValueOnce([
           "pages/404.ts",
           "pages/index.md",
@@ -91,7 +98,6 @@ describe("route generation", () => {
           "pages/foo/index.test.tsx",
         ]);
         writeNextjsRoutes({
-          pagesDirectory: "pages",
           pageExtensions: ["ts", "md", "page.tsx"],
         });
         expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
@@ -99,15 +105,18 @@ describe("route generation", () => {
     });
 
     it("outDir", () => {
+      // getPageRoutes
+      existsSyncMock.mockImplementationOnce(() => true);
       findFilesMock.mockReturnValueOnce(["pages/404.ts"]);
-      writeNextjsRoutes({ pagesDirectory: "pages", outDir: "src" });
+      writeNextjsRoutes({ outDir: "src" });
       expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
     });
 
     it("i18n", () => {
+      // getPageRoutes
+      existsSyncMock.mockImplementationOnce(() => true);
       findFilesMock.mockReturnValueOnce(["pages/index.ts"]);
       writeNextjsRoutes({
-        pagesDirectory: "pages",
         i18n: {
           locales: ["en-US", "fr", "nl-NL"],
           defaultLocale: "en-US",