Skip to content

Commit 3de75c1

Browse files
committed
Assert the deeplink route's path from Remix's own route manifest
The previous test built the route pattern out of DEEPLINK_PATH_PREFIX and then asserted that pattern equalled the same constant, so it could not fail if Remix compiled `[_].$.ts` to something else. That is the one thing worth guarding here: an unescaped `_` reads as a pathless layout, which would mount this loader at `/*` — a splat over the whole site whose loader redirects unconditionally. Run the real routes directory through the same `flatRoutes` the vite plugin uses, derive the mounted path from the manifest entry for the route file, and check the constant against that. Also assert no route anywhere compiles to a bare site-wide splat, which is the assertion that catches the failure independently of this route. Verified by mutation: unescaping the filename fails the suite, and a decoy pathless splat route fails the splat assertion.
1 parent f93a892 commit 3de75c1

1 file changed

Lines changed: 82 additions & 8 deletions

File tree

apps/webapp/app/utils/deeplinkPages.test.ts

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { flatRoutes } from "@remix-run/dev/dist/config/flat-routes.js";
2+
import type { RouteManifest } from "@remix-run/dev/dist/config/routes.js";
13
import { matchPath } from "@remix-run/router";
24
import { existsSync, readdirSync, statSync } from "node:fs";
35
import { 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. */
2730
const 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+
2966
const 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+
273349
describe("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

Comments
 (0)