Repository: tatethurston/nextjs-routes
Pull request #92 (2022-11-07 23:16:06)

app directory


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 650724a..9fba1de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,11 @@
 # Changelog
 
-## 0.1.6
+## 0.1.7
 
 - Support [Next 13 app (beta)](https://nextjs.org/docs/advanced-features/custom-app)
 
+## 0.1.6
+
 - Accept path strings for static routes:
 
   ```tsx
diff --git a/src/core.ts b/src/core.ts
index 7aea715..e46ff17 100644
--- a/src/core.ts
+++ b/src/core.ts
@@ -267,6 +267,29 @@ interface NextJSRoutesOptions {
   i18n?: I18NConfig | null;
 }
 
+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 getAppRoutes(files: string[], opts: Opts): string[] {
   return (
     commonProcessing(files, opts)
@@ -277,9 +300,11 @@ export function getAppRoutes(files: string[], opts: Opts): string[] {
         file
           .split(sep)
           // remove named groups
-          .filter((segment) => segment.startsWith("(") && segment.endsWith(")"))
+          .filter(
+            (segment) => !(segment.startsWith("(") && segment.endsWith(")"))
+          )
           // remove page
-          .filter((file) => parse(file).name === "page")
+          .filter((file) => parse(file).name !== "page")
           .join(sep)
       )
   );
@@ -301,29 +326,6 @@ export function getPageRoutes(files: string[], opts: Opts): string[] {
   );
 }
 
-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: "",
diff --git a/src/utils.ts b/src/utils.ts
index e95285c..2d18ed8 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -16,7 +16,6 @@ export function getAppDirectory(cwd = process.cwd()): string | undefined {
     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/src/__snapshots__/core.test.ts.snap b/src/__snapshots__/core.test.ts.snap
index 96b9429..43889be 100644
--- a/src/__snapshots__/core.test.ts.snap
+++ b/src/__snapshots__/core.test.ts.snap
@@ -1,5 +1,148 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`route generation app directory (experimental) 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: "/foo"; query?: Query | undefined }
+    | { pathname: "/bar"; 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 configuration i18n 1`] = `
 [
   [
diff --git a/src/core.test.ts b/src/core.test.ts
index 63a813d..2313d7b 100644
--- a/src/core.test.ts
+++ b/src/core.test.ts
@@ -78,6 +78,17 @@ describe("route generation", () => {
     expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
   });
 
+  it("app directory (experimental)", () => {
+    // getAppRoutes
+    existsSyncMock
+      .mockImplementationOnce(() => false)
+      .mockImplementationOnce(() => false)
+      .mockImplementationOnce(() => true);
+    findFilesMock.mockReturnValueOnce(["app/foo/page.tsx", "app/bar/page.ts"]);
+    writeNextjsRoutes({});
+    expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
+  });
+
   describe("configuration", () => {
     describe("pageExtensions", () => {
       it("default", () => {