class

Analyzer::Javascript::Remix

Inherits Analyzer::Javascript::JavascriptEngine < Analyzer < FileHelper < Reference < Object

Remix v2 is filesystem-routed under app/routes/. The route URL is derived from the filename via dot-flat naming and a handful of special prefixes:

app/routes/_index.tsx → GET / app/routes/about.tsx → GET /about app/routes/users._index.tsx → GET /users app/routes/users.$id.tsx → GET /users/{id} app/routes/_auth.login.tsx → GET /login (_auth is a pathless layout — no URL effect) app/routes/api.users.ts → exports drive verbs app/routes/$.tsx → catch-all

Verb detection:

  • .tsx / .jsx files render a page — emit GET.
  • Files exporting loader add GET (or keep it).
  • Files exporting action add POST / PUT / PATCH / DELETE (Remix dispatches every non-GET to action at runtime; we surface the full set so downstream tooling can fan out).
  • Resource-only .ts / .js files without loader or action are skipped — they're not handler routes.

Out of scope for this first cut:

  • Per-handler request-helper scanning. Remix loaders / actions receive { request, params, context } — accurate read tracking needs cross-call value flow. Path placeholders still surface via the optimizer.
  • Optional segments ($lang) (translated to a literal lang today; Remix folds optionality into a wildcard match that's hard to represent without per-route alternatives).
  • The legacy v1 nested-folder convention — v2 flat is what the toolchain has been on since Remix 1.15 / Remix 2. (v2 folder routes — routes/users.$id/route.tsx — are read.)

Constants

CONFIG_BASENAMES = ["remix.config.js", "remix.config.ts", "remix.config.mjs", "remix.config.cjs"]
EXTENSIONS = PAGE_EXTENSIONS + RESOURCE_EXTENSIONS
FLAT_SEGMENT_RULES = Noir::TopLevelSplit::Rules.new(nest: Noir::TopLevelSplit::Nest::Bracket, quotes: "", escape: Noir::TopLevelSplit::Escape::None, strip: false, empties: Noir::TopLevelSplit::Empties::Keep, per_kind: false, clamp: true)

[...] is the only thing that nests in a Remix flat-route filename, and a filename has no string literals, so quote handling is off — a route named it's.tsx would otherwise open a quoted run and never split. Empties are kept and parts are not stripped because normalize_segment and the caller decide what an empty segment means; a filename cannot contain leading or trailing whitespace worth trimming anyway.

FOLDER_ROUTE_MODULES = ["route", "index"]

A folder route's module: routes/users.$id/route.tsx (or index.tsx).

NON_GET_VERBS = ["POST", "PUT", "PATCH", "DELETE"]
PACKAGE_MARKERS = (["dev", "react", "node", "serve", "server-runtime", "cloudflare", "cloudflare-pages", "cloudflare-workers", "deno", "express", "architect"] of ::String) .map do |name| "\"@remix-run/#{name}\"" end

A Remix project is anchored by remix.config.* or a package.json pulling in a Remix v2 package. Scoping to it keeps Remix off a sibling React Router app's app/routes/, which uses the same file convention. Not the bare @remix-run/ scope: React Router apps depend on utilities published under it (@remix-run/node-fetch-server).

PAGE_EXTENSIONS = [".tsx", ".jsx"]
RESOURCE_EXTENSIONS = [".ts", ".js", ".mjs"]
ROUTE_CONFIG_BASENAMES = ["routes.ts", "routes.js", "routes.mts", "routes.mjs"]

React Router v7's route config. Remix has none, so an app directory holding one belongs to React Router even inside a Remix project root (a monorepo whose root package.json hoists @remix-run/dev).

ROUTE_CONFIG_MARKER = "@react-router/"

Class methods

tech_name
Source

Instance methods

analyze
Source
tech

Instance-side view of the same declaration. The per-file rescues live on this base class, which has no way to name the analyzer that is running inside them, so a skipped file could not be attributed to a tech. Deriving it from analyzer_for keeps the name written exactly once.

Source