1+ import { flatRoutes } from "@remix-run/dev/dist/config/flat-routes.js" ;
2+ import type { RouteManifest } from "@remix-run/dev/dist/config/routes.js" ;
13import { matchPath } from "@remix-run/router" ;
24import { existsSync , readdirSync , statSync } from "node:fs" ;
35import { join } from "node:path" ;
@@ -9,7 +11,8 @@ import {
911 resolveDeeplinkPage ,
1012} from "./deeplinkPages" ;
1113
12- const ROUTES_DIR = join ( __dirname , "../routes" ) ;
14+ const APP_DIR = join ( __dirname , ".." ) ;
15+ const ROUTES_DIR = join ( APP_DIR , "routes" ) ;
1316
1417// Flat-route prefix for every page that renders inside an environment. The trailing dot matters:
1518// it excludes the layout route itself (`…env.$envParam`), which has no segment of its own.
@@ -26,6 +29,40 @@ const NOT_DEEPLINK_NAMES = new Set(["_index", "queues_"]);
2629/** Stands in for a param segment, so a deep path under test looks like a real URL. */
2730const PROBE = "probe_01ABC" ;
2831
32+ /** The route module whose filename is what produces the deeplink URL. */
33+ const DEEPLINK_ROUTE_FILE = "routes/[_].$.ts" ;
34+
35+ /**
36+ * The app's routes as Remix itself compiles them, so the URL under test is the one the router will
37+ * really serve rather than one this file asserts into existence. `flatRoutes` is the same function
38+ * the vite plugin calls, and the ignore list mirrors `ignoredRouteFiles` in `vite.config.ts`.
39+ */
40+ const compiledRoutes : RouteManifest = flatRoutes ( APP_DIR , [ "**/.*" ] ) ;
41+
42+ /**
43+ * A route's whole URL, walking up the manifest — a `path` is relative to its parent's, and a
44+ * pathless layout contributes nothing. Top-level routes name `root`, which the manifest omits.
45+ */
46+ function compiledUrl ( id : string ) : string {
47+ // An unknown id would otherwise walk zero routes and quietly read as the site root.
48+ if ( ! compiledRoutes [ id ] ) throw new Error ( `no compiled route with id ${ id } ` ) ;
49+
50+ const segments : string [ ] = [ ] ;
51+ let route = compiledRoutes [ id ] ;
52+ while ( route ) {
53+ if ( route . path ) segments . unshift ( route . path ) ;
54+ route = route . parentId ? compiledRoutes [ route . parentId ] : undefined ;
55+ }
56+ return `/${ segments . join ( "/" ) } ` ;
57+ }
58+
59+ /** What Remix actually mounts `[_].$.ts` at, e.g. `/_`. Derived, never assumed. */
60+ const COMPILED_DEEPLINK_PATH = ( ( ) => {
61+ const entry = Object . values ( compiledRoutes ) . find ( ( route ) => route . file === DEEPLINK_ROUTE_FILE ) ;
62+ if ( ! entry ) throw new Error ( `${ DEEPLINK_ROUTE_FILE } is not in the compiled route manifest` ) ;
63+ return compiledUrl ( entry . id ) . replace ( / \/ \* $ / , "" ) ;
64+ } ) ( ) ;
65+
2966const routeEntries = readdirSync ( ROUTES_DIR ) ;
3067
3168/** A route directory only contributes a route if it actually holds a `route` module. */
@@ -270,6 +307,45 @@ describe("resolveDeeplinkPage", () => {
270307 } ) ;
271308} ) ;
272309
310+ describe ( "the route Remix compiles from the filename" , ( ) => {
311+ // The escape in `[_].$.ts` is the whole reason this route works, and getting it wrong fails
312+ // catastrophically rather than visibly: a flat-route segment starting with `_` is a *pathless
313+ // layout* contributing nothing to the URL, so an unescaped `_.$.ts` would mount this loader at
314+ // `/*` — a splat over the entire site whose loader redirects unconditionally. Nothing else in
315+ // the app uses `[…]` escaping, so there is no precedent to lean on and the compiled manifest is
316+ // the only honest source. Everything here is read out of `flatRoutes`, never asserted into it.
317+
318+ it ( "mounts the deeplink route at /_ and nowhere else" , ( ) => {
319+ expect ( COMPILED_DEEPLINK_PATH ) . toBe ( "/_" ) ;
320+ // The constant the loader strips has to agree with what Remix mounted, so renaming either the
321+ // file or the constant without the other fails here.
322+ expect ( COMPILED_DEEPLINK_PATH ) . toBe ( DEEPLINK_PATH_PREFIX ) ;
323+ } ) ;
324+
325+ it ( "does not mount anything as a site-wide splat" , ( ) => {
326+ // The disaster case, asserted for every route rather than just this one: had the underscore
327+ // been read as pathless, this is the assertion that would have caught it.
328+ const siteWide = Object . values ( compiledRoutes )
329+ . filter ( ( route ) => compiledUrl ( route . id ) === "/*" )
330+ . map ( ( route ) => route . file ) ;
331+
332+ expect ( siteWide ) . toEqual ( [ ] ) ;
333+ } ) ;
334+
335+ it ( "compiled the manifest it is reading" , ( ) => {
336+ // Keeps the two assertions above from passing because `flatRoutes` returned nothing useful.
337+ expect ( Object . keys ( compiledRoutes ) . length ) . toBeGreaterThan ( 400 ) ;
338+ // A sample of ordinary routes, so a manifest full of undefined paths would not read as a pass.
339+ expect ( compiledUrl ( "routes/login.magic" ) ) . toBe ( "/login/magic" ) ;
340+ // `queues_.$queueParam` opts out of the parent layout; the trailing `_` is not a URL character.
341+ expect (
342+ compiledUrl (
343+ "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam"
344+ )
345+ ) . toBe ( "/orgs/:organizationSlug/projects/:projectParam/env/:envParam/queues/:queueParam" ) ;
346+ } ) ;
347+ } ) ;
348+
273349describe ( "deeplinkSuffix" , ( ) => {
274350 it ( "strips the route's own prefix" , ( ) => {
275351 expect ( deeplinkSuffix ( "/_/tasks" ) ) . toBe ( "tasks" ) ;
@@ -290,13 +366,11 @@ describe("deeplinkSuffix", () => {
290366 expect ( deeplinkSuffix ( "/_/tasks/standard/My-Task" ) ) . toBe ( "tasks/standard/My-Task" ) ;
291367 } ) ;
292368
293- it ( "is mounted where the route filename says it is" , ( ) => {
294- // `[_].$` is an escaped literal, not a pathless layout: Remix's `createRoutePath` drops a
295- // segment only when the cooked and the raw spelling both start with `_`, and the raw spelling
296- // is `[_]`. A plain `_.$` would compile to `/*` and swallow the site, so this pins the prefix
297- // the loader strips to the URL the router actually serves.
298- const route = `${ DEEPLINK_PATH_PREFIX } /*` ;
299- expect ( route ) . toBe ( "/_/*" ) ;
369+ it ( "matches the URL the router serves for it" , ( ) => {
370+ // Behaviour of the pattern itself. What the pattern *is* is settled against the compiled
371+ // manifest above — building it from DEEPLINK_PATH_PREFIX alone would only compare the constant
372+ // with itself.
373+ const route = `${ COMPILED_DEEPLINK_PATH } /*` ;
300374 expect ( matchPath ( route , "/_/apikeys" ) ?. params [ "*" ] ) . toBe ( "apikeys" ) ;
301375 expect ( matchPath ( route , "/_/runs/run_123" ) ?. params [ "*" ] ) . toBe ( "runs/run_123" ) ;
302376 // The splat keeps the case it was given, which is why the loader folds only the page name.
0 commit comments