Skip to content

Entrypoint rules for JS/TS-only frameworks: Remix, Astro, Next.js src/ layout, and four resolver gaps #206

Description

@rahlk

Problem

The entrypoint pass (#72, #159, #161, #167) ships rules for nestjs, angular, nextjs, sveltekit,
electron, commander and worker_threads. Several JS/TS-only frameworks with no Java or Python
counterpart are missing, and measuring against a fixture turned up three resolver gaps that block
them independently of any rule row:

  • Remix / React Router v7 — app/routes/** modules exporting loader / action / default.
    These land in the tree as exported, module-level, correctly-named callables, so the existing
    file tier (entrypointsFromFiles, src/entrypoints/matching.ts:335) resolves them with no code
    change. Nothing detects them today.
  • Astro API routes — src/pages/**/*.{ts,js} exporting GET/POST/…, same shape.
  • Next.js src/ layout — nextjs.app-route and nextjs.pages-api anchor their globs at the
    repo root (pages/api/**), so the officially-supported src/pages/api/** and src/app/**/route.ts
    layouts match nothing. Asserted as current behaviour at test/entrypoints-files.test.ts:26.
  • Named function expression handlers — app.get("/x", function named(req, res) {}) is counted
    in entrypoint_report.unresolved instead of resolving. resolveHandler
    (src/entrypoints/matching.ts:192-197) accepts an inline callable only when its name is
    "(anonymous)"; a named function expression passes the INLINE regex at :132 and then matches
    nothing. Measured: of four Express handler shapes, the identifier, arrow and async-arrow forms
    resolve and this one does not.
  • Nitro / Nuxt — export default defineEventHandler((event) => {}). The wrapped arrow is
    (anonymous) with is_exported: false, and resolveDefaultExport (matching.ts:309-324)
    requires the anonymous callable's span to begin exactly after the export default token, which
    a wrapper call displaces.

Scope boundary

Rules for the frameworks whose shape the current machinery can already resolve, plus the four
narrow resolver fixes that unblock them. Specifically NOT in this issue:

  • No promotion of Express / Fastify / Koa / Hapi / Hono to declared frameworks: rules.
    Framework-tier calls: rules match the import-table-resolved callee and bail on an unresolvable
    one (matching.ts:172). Their receiver is a factory result held in a local (const app = express()), which resolveWritten (src/syntactic_analysis/importResolver.ts:29-35) cannot
    name — it resolves only spellings whose head is an imported binding, which is why
    electron.app-on works and app.get cannot. These frameworks keep working via the heuristic
    tier and keep reporting framework: "heuristic". Promoting them needs a local-to-factory-origin
    link; separate issue.
  • No Cloudflare Workers export default { async fetch() {} }. Object-literal methods yield
    zero module-level callables today, so there is nothing for any rule to target. Symbol-table
    scope; separate issue. Bun.serve({ fetch }) misses for the same reason.
  • No .astro / .vue / .svelte parsing — only the .ts/.js files beside them.
  • No change to TSEntrypoint, the Neo4j schema, or SCHEMA_VERSION.

Goals

Rules (src/entrypoints/rules.yml):

  • remix block — detect: [@remix-run/node, @remix-run/react, @remix-run/server-runtime, @remix-run/cloudflare, react-router]; three file rules over app/routes/**/*.{ts,tsx,js,jsx}: remix.loader (exports: [loader]), remix.action (exports: [action]), remix.route-component (exports: [default])
  • astro block — detect: [astro]; astro.api-route over src/pages/**/*.{ts,js}, exports: [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, ALL], methods: {from: export_name}
  • nextjs — add nextjs.app-route-src (src/app/**/route.{ts,tsx,js,mjs}) and nextjs.pages-api-src (src/pages/api/**/*.{ts,tsx,js,mjs})

Resolver fixes:

  • resolveHandler (matching.ts:192-197) — in the INLINE branch, select the earliest callable whose span lies inside the call site regardless of its name, so a named function expression resolves
  • resolveDefaultExport (matching.ts:309-324) — accept export default <wrapper>(<handler>), bounding the gap between the export default token and the callable to wrapper-call text only (/^(?:[A-Za-z_$][\w$.]*\s*\(\s*)+$/) so an unrelated later callable cannot be claimed
  • routeFromFileKey (matching.ts:291) — strip a leading src/ from the glob's literal prefix, so src/app/users/route.ts yields /users and src/pages/api/hello.ts yields /api/hello
  • methodsOf export_name branch (matching.ts:101) — filter through HTTP_VERBS as the match_suffix branch already does at :92, so Astro's ALL yields [] instead of the non-method ["ALL"]

Caveats and known risks

  • framework values shift for existing projects. A Remix or Astro project whose handlers the
    heuristic tier previously claimed will now report framework: "remix" / "astro", because the
    file tier runs before the heuristic tiers (src/entrypoints/pipeline.ts:69-92) and the heuristic
    calls tier skips framework-claimed nodes (pipeline.ts:113). This is an output-content change,
    not a schema change. Mitigation: pinned by a test asserting the exact new framework/rule
    values, and called out here.
  • remix detects on react-router because v7 absorbed Remix. A React Router SPA that happens to
    have app/routes/ and an export named loader will be flagged. The alternative — omitting it —
    misses every React Router v7 app. Accepted at confidence: certain, not declared.
  • Astro's ALL export produces http_methods: []. Deliberate: ALL is not an HTTP method, and
    is_entrypoint plus route still carry the finding.
  • Remix loader / action carry http_methods: []. Mapping loader to GET and action to the
    mutating verbs needs a per-export method source the rule grammar does not have; the rule id
    (remix.loader vs remix.action) carries the distinction instead. Not attempted here.
  • resolveDefaultExport's wrapper shape still misses a handler that is not the first callable in
    the wrapper call — defineEventHandler({ onRequest: fn }) resolves nothing. Counted in
    unresolved rather than silently dropped.
  • The src/ strip in routeFromFileKey applies to the glob's literal prefix only, so the existing
    routeFromFileKey("src/routes/x/+server.ts", "**/+server.{ts,js}") → /src/routes/x assertion
    (test/entrypoints-files.test.ts:35) is unaffected.

Definition of done

  • One fixture carrying every shape named above; the entrypoint record set equals a
    hand-written expected set — exact framework, rule, confidence, route and
    http_methods per target, not a non-empty check
  • entrypoint_report.frameworks_detected equals exactly ["astro", "nextjs", "remix"] on that
    fixture
  • A test per resolver fix that fails on main and passes after: named function expression
    handler resolves and no longer appears in unresolved; export default defineEventHandler(...) resolves; src/app/** and src/pages/api/** produce /users and
    /api/hello; Astro ALL yields http_methods: []
  • The four globToRegExp / routeFromFileKey assertions at
    test/entrypoints-files.test.ts:24-36 pass unchanged
  • bun test green, bun run typecheck clean
  • bun run gen:schema leaves schema.neo4j.json byte-identical, demonstrating no contract move

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions