feat(entrypoints): Remix, Astro and Next.js src/ layout rules (#206) - #207
Merged
Merged
Conversation
Four framework rows the shipped ruleset was missing, plus the resolver gaps
they exposed. No schema change: no new field, node label, edge type or
property — only new values in `framework`/`rule`/`route`/`http_methods`,
the same class of change a user's own `--entrypoint-rules` file makes.
Rules (`src/entrypoints/rules.yml`):
- `remix` — detected on any `@remix-run/*` or on `react-router` alone, since
v7 absorbed Remix. Three file rules over `app/routes/**`, one per export
(`loader`, `action`, `default`), so the `rule` id tells a consumer whether
it is looking at a data loader, a mutation or the rendered component.
`http_methods` stays empty: mapping `loader` to GET needs a per-export
method source the rule grammar does not have.
- `astro` — `astro.api-route` over `src/pages/**/*.{ts,js}`, verb exports plus
`ALL`. `.astro` files are not parsed, so this covers the endpoints only.
- `nextjs` — `nextjs.app-route-src` and `nextjs.pages-api-src` for the `src/`
layout Next.js supports officially. Separate rows rather than a loosened
glob: a convention path must match exactly, not anywhere in the tree.
Resolver fixes (`src/entrypoints/matching.ts`):
- `methodsOf` filters `export_name` through `HTTP_VERBS`, as `match_suffix`
already did. Astro's `ALL` is a real handler but not an HTTP method, and
`http_methods` is a field consumers filter on.
- `routeFromFileKey` strips the glob's leading LAYOUT directories — `app/`,
`pages/`, Remix's `app/routes/`, and an optional `src/` in front of any of
them. Whatever literal prefix survives is a route prefix, so `pages/api/`
still serves at `/api`. A glob with no literal prefix is untouched.
- `resolveHandler`'s INLINE branch no longer gates on `name ===
"(anonymous)"`: `app.get("/x", function named(req, res) {})` is a named
function expression, and gating on the name resolved nothing while counting
the site unresolved. Position identifies it; the outermost callable in the
span wins, so a callable nested inside the handler is never picked.
- `resolveDefaultExport` handles `export default <wrapper>(<handler>)` —
Nitro/Nuxt's `defineEventHandler(h)` and every `withSentry(h)` shape. The
gap between token and callable must be nothing but call openings, which is
what keeps `defineHandler({onRequest: fn})` counted unresolved instead of
claiming an unrelated later callable.
Tests: `test/entrypoints-jsts-frameworks.test.ts` (11 tests) covers each rule
id, the three-framework project where astro and nextjs globs overlap but
their `exports:` lists disagree, both resolver gaps, and the guard that a
wrapper whose handler is not a direct argument stays unresolved.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #206.
Four framework rows the shipped ruleset was missing, and the two resolver gaps writing the tests for them exposed.
Rules —
src/entrypoints/rules.ymlremix@remix-run/*, orreact-routeralone (v7 absorbed Remix)remix.loader,remix.action,remix.route-componentoverapp/routes/**/*.{ts,tsx,js,jsx}astroastroastro.api-routeoversrc/pages/**/*.{ts,js}, verb exports plusALLnextjsnextnextjs.app-route-src,nextjs.pages-api-srcfor thesrc/layoutOne Remix rule per export rather than one rule listing all three: the
ruleid is then what tells a consumer whether it is looking at a data loader, a mutation, or the rendered component.http_methodsstays empty for Remix — mappingloaderto GET andactionto the mutating verbs needs a per-export method source the rule grammar does not have, and inventing one here would coin a spelling no sibling analyzer has agreed to.The two
nextjsrows are separate rules, not a loosened glob.globToRegExpanchors a pattern containing/at the repo root by design, and a convention path has to match exactly rather than anywhere in the tree —test/entrypoints-files.test.tslocks that in (globToRegExp("app/**/route.{ts,js}").test("src/app/route.ts") === false) and this PR does not touch it.Resolver fixes —
src/entrypoints/matching.tsmethodsOf,export_namebranch now filters throughHTTP_VERBS, exactly asmatch_suffixalready did. Astro'sALLis a real handler but not an HTTP method, andhttp_methodsis a field consumers filter on.routeFromFileKeystrips the glob's leading layout directories —app/,pages/, Remix'sapp/routes/, and an optionalsrc/in front of any of them. Whatever literal prefix survives is a genuine route prefix, sopages/api/still serves at/api. A glob with no literal prefix is untouched, so**/+server.{ts,js}still yields the whole key.resolveHandler's INLINE branch no longer gates onname === "(anonymous)".app.get("/x", function named(req, res) {})is a named function expression: it passed the position test, failed the name test, resolved nothing, and the site was counted unresolved. Position alone identifies it, and the sort takes the outermost callable in the span, so a callable nested inside the handler is never picked instead.resolveDefaultExporthandlesexport default <wrapper>(<handler>)— Nitro/Nuxt'sdefineEventHandler(h), and everywithSentry(h)-shaped wrapper, which displace the callable past theexport defaulttoken so the exact-offset match misses it. The gap between token and callable must be nothing but call openings, which is what keepsexport default defineHandler({onRequest: fn})counted inunresolvedinstead of claiming an unrelated later callable in the file.No contract move
No new field, node label, edge type or property.
TSEntrypointand the Neo4j schema are untouched; the change adds values to existing strings, the same class of change a user's own--entrypoint-rulesfile makes.bun run gen:schemaleavesschema.neo4j.jsonbyte-identical.Tests
test/entrypoints-jsts-frameworks.test.ts, 11 tests: each rule id with itsroute/http_methods, Remix detected onreact-routeralone,ALLcarrying no method, thesrc/route-prefix unit cases (including the unchanged**/+serverone), both resolver gaps, the nested-callable guard, and a three-framework project where the astro and nextjs globs overlap on one file but theirexports:lists disagree — so the verb export is astro's and the default export is nextjs'.Full suite: 369 pass, 8 skip, 0 fail.
bun run typecheckclean.test/entrypoints-rules.test.ts's framework inventory assertion grew by the two new names.Propagation
Propagation verdict: codeanalyzer-python (same
http_methodsjunk-value bug class on its ownmatch_suffixpath —_methods_ofreturns[verb.upper()]unfiltered while_HTTP_VERBSsits in the same module, so the shippedheuristic.http-verbrule emitsWEBSOCKET; filed as codellm-devkit/codeanalyzer-python#213), python-sdk (pin bump needed —pyproject.tomlhard-pinscodeanalyzer-typescript==1.5.3, so these rules do not reach an SDK user until this repo releases and that pin moves).Checked and negative: no other sibling analyzer implements the file-convention or
export_nametier at all (it is TS/JS-only, no python analog — python's entrypoint pass has nofiles:rules), so therouteFromFileKeyandresolveDefaultExportfixes have no counterpart to port. Docs:docs/design/specs/entrypoint-detection.mdalready lists Remix under "file convention", so the spec is satisfied rather than made stale; the consumer skill's vocabulary reference names the fields, not the frameworks; no README or docs-repo page enumerates rule ids. No fixture in any sibling repo encodes the old behaviour — the only fixtures asserting these paths are this repo's, and both affected files are updated here.