Repository: vercel/next.js
Pull request: feat: make typed routes output deterministic

### What?

This PR makes changes to the next plugin for typed routes. The issue here is that the output of the plugin, specifically the part where static and dynamic routes are defined changes constantly. This PR makes the output of the plugin deterministic.

### Why?

We work in a monorepo with multiple Next.js projects. We encountered multiple issues with git and git hooks. If you had a stale `.next` output the routes type would also be stale and caused issues on commit when you want to sync your work with an upbranch causing either to rebuild the app you are not developing or completly removing the `.next` folder. As a solution we choose to commit `.next/types/link.d.ts` (the output file of the mentioned next.js plugin) and that solved the issue.

Now we have a smaller issue regarding the un-deterministic file output which I want to solve with this MR.

### How?

Before writing the routes in the file, the routes are sorted using native `sort` method on the array instance. The actual sort order is not relevant here, the fact that the output is in the same order everytime is the key.


Changed files:
filename: packages/next/src/build/webpack/plugins/next-types-plugin/index.ts
patch: @@ -240,18 +240,21 @@ const pluginState = getProxiedPluginState({
   collectedRootParams: {} as Record<string, string[]>,
   routeTypes: {
     edge: {
-      static: '',
-      dynamic: '',
+      static: [],
+      dynamic: [],
     },
     node: {
-      static: '',
-      dynamic: '',
+      static: [],
+      dynamic: [],
     },
     extra: {
-      static: '',
-      dynamic: '',
+      static: [],
+      dynamic: [],
     },
-  } as Record<'edge' | 'node' | 'extra', Record<'static' | 'dynamic', string>>,
+  } as Record<
+    'edge' | 'node' | 'extra',
+    Record<'static' | 'dynamic', string[]>
+  >,
 })
 
 function formatRouteToRouteType(route: string) {
@@ -278,7 +281,7 @@ function formatRouteToRouteType(route: string) {
 
   return {
     isDynamic,
-    routeType: `\n    | \`${route}\``,
+    routeType: route,
   }
 }
 
@@ -356,8 +359,9 @@ function addRedirectsRewritesRouteTypes(
 
       for (const normalizedRoute of possibleNormalizedRoutes) {
         const { isDynamic, routeType } = formatRouteToRouteType(normalizedRoute)
-        pluginState.routeTypes.extra[isDynamic ? 'dynamic' : 'static'] +=
+        pluginState.routeTypes.extra[isDynamic ? 'dynamic' : 'static'].push(
           routeType
+        )
       }
     }
   }
@@ -385,18 +389,31 @@ function addRedirectsRewritesRouteTypes(
   }
 }
 
+function serializeRouteTypes(routeTypes: string[]) {
+  // route collection is not deterministic, this makes the output of the file deterministic
+  return routeTypes
+    .sort()
+    .map((route) => `\n    | \`${route}\``)
+    .join('')
+}
+
 function createRouteDefinitions() {
-  let staticRouteTypes = ''
-  let dynamicRouteTypes = ''
+  let staticRouteTypes = []
+  let dynamicRouteTypes = []
 
   for (const type of ['edge', 'node', 'extra'] as const) {
-    staticRouteTypes += pluginState.routeTypes[type].static
-    dynamicRouteTypes += pluginState.routeTypes[type].dynamic
+    staticRouteTypes.push(...pluginState.routeTypes[type].static)
+    dynamicRouteTypes.push(...pluginState.routeTypes[type].dynamic)
   }
 
+  const serializedStaticRouteTypes = serializeRouteTypes(staticRouteTypes)
+  const serializedDynamicRouteTypes = serializeRouteTypes(dynamicRouteTypes)
+
   // If both StaticRoutes and DynamicRoutes are empty, fallback to type 'string & {}'.
   const routeTypesFallback =
-    !staticRouteTypes && !dynamicRouteTypes ? 'string & {}' : ''
+    !serializedStaticRouteTypes && !serializedDynamicRouteTypes
+      ? 'string & {}'
+      : ''
 
   return `// Type definitions for Next.js routes
 
@@ -428,9 +445,9 @@ declare namespace __next_route_internal_types__ {
   type OptionalCatchAllSlug<S extends string> =
     S extends \`\${string}\${SearchOrHash}\` ? never : S
 
-  type StaticRoutes = ${staticRouteTypes || 'never'}
+  type StaticRoutes = ${serializedStaticRouteTypes || 'never'}
   type DynamicRoutes<T extends string = string> = ${
-    dynamicRouteTypes || 'never'
+    serializedDynamicRouteTypes || 'never'
   }
 
   type RouteImpl<T> = ${
@@ -887,7 +904,7 @@ export class NextTypesPlugin {
 
     pluginState.routeTypes[this.isEdgeServer ? 'edge' : 'node'][
       isDynamic ? 'dynamic' : 'static'
-    ] += routeType
+    ].push(routeType)
   }
 
   apply(compiler: webpack.Compiler) {
@@ -1014,11 +1031,11 @@ export class NextTypesPlugin {
 
           // Clear routes
           if (this.isEdgeServer) {
-            pluginState.routeTypes.edge.dynamic = ''
-            pluginState.routeTypes.edge.static = ''
+            pluginState.routeTypes.edge.dynamic = []
+            pluginState.routeTypes.edge.static = []
           } else {
-            pluginState.routeTypes.node.dynamic = ''
-            pluginState.routeTypes.node.static = ''
+            pluginState.routeTypes.node.dynamic = []
+            pluginState.routeTypes.node.static = []
           }
 
           compilation.chunkGroups.forEach((chunkGroup) => {