Skip to content

Add native App Router route type generation#1144

Open
JamesbbBriz wants to merge 17 commits into
cloudflare:mainfrom
JamesbbBriz:feature/native-route-typegen
Open

Add native App Router route type generation#1144
JamesbbBriz wants to merge 17 commits into
cloudflare:mainfrom
JamesbbBriz:feature/native-route-typegen

Conversation

@JamesbbBriz
Copy link
Copy Markdown
Contributor

Summary

  • Add native App Router route type generation for PageProps, LayoutProps, and RouteContext.
  • Generate .next/types/routes.d.ts from Vinext's existing App Router route graph.
  • Refresh generated route types during dev when App Router files are added or removed.

Closes #664

Test Plan

  • NODE_OPTIONS=--experimental-strip-types pnpm exec vp check --fix
  • pnpm test tests/typegen.test.ts tests/check.test.ts
  • pnpm exec tsc --noEmit --pretty false from packages/vinext
  • pnpm --filter vinext run build
  • git diff --check
  • Smoke-tested built vinext typegen against a temp App Router project

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 9, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1144

commit: 8edcb50

@JamesbbBriz JamesbbBriz force-pushed the feature/native-route-typegen branch from 80ea35a to 92c2c5f Compare May 9, 2026 00:55
@JamesbbBriz JamesbbBriz marked this pull request as ready for review May 9, 2026 01:09
Copy link
Copy Markdown
Contributor Author

This is ready for maintainer review now that checks have passed.

The implementation intentionally follows the native generation direction rather than delegating to next typegen, based on the discussion in #651 and #664. I’d especially appreciate review on the generated helper type surface and the integration points with the existing App Router route graph / dev invalidation flow.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92c2c5fa33

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/typegen.ts
Comment thread packages/vinext/src/cli.ts Outdated
@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d40deda6e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/cli.ts Outdated
@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a053daa927

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/typegen.ts Outdated
@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f359f5f0fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/index.ts Outdated
@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0b73ef8b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/typegen.ts Outdated
@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de4bcab749

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/typegen.ts Outdated
Comment on lines +81 to +82
const layoutRoute = treePathToRouteLiteral(slot.ownerTreePath);
const slots = model.layoutSlots.get(layoutRoute) ?? [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep layout slot types separate across root route groups

layoutSlots is keyed by treePathToRouteLiteral(slot.ownerTreePath), which strips route-group segments. In apps with multiple root layouts (e.g. app/(shop)/layout and app/(marketing)/layout), both owners collapse to "/", so their slot names are merged into one LayoutProps<'/'> signature. That makes helper types inaccurate (slots from one root layout appear on another), which is exactly where developers rely on these generated types for correctness.

Useful? React with 👍 / 👎.

@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6821754dee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/typegen.ts Outdated
Comment on lines +82 to +83
const layoutRoute =
layoutRouteKeys.get(slot.ownerTreePath) ?? treePathToRouteLiteral(slot.ownerTreePath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Map slots to owning layout route instead of owner tree path

LayoutSlotMap is keyed by LayoutRoute, but this loop keys slots by slot.ownerTreePath ("/foo" in app/foo/@slot/*) rather than the slot’s ownerLayoutId. When a slot is defined under a segment that does not have its own layout.tsx, the manifest can still assign it to an ancestor layout via ownerLayoutId; with the current code, the slot is written under a non-layout key and then dropped from the emitted LayoutSlotMap, so LayoutProps<'/...'> misses that slot prop. Use ownerLayoutId (resolved back to the layout route key) as the source of truth for slot ownership.

Useful? React with 👍 / 👎.

@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7006ccb0cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/typegen.ts Outdated
const segments = treePath
.split("/")
.filter(Boolean)
.filter((segment) => !isSlotSegment(segment) && segment !== ".")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve slot segments when disambiguating layout route keys

When two layouts resolve to the same URL route, this fallback key path is supposed to disambiguate them, but treePathToScopedLayoutRouteLiteral removes @slot segments, so layouts like app/layout.tsx and app/@modal/layout.tsx both collapse to "/". That causes LayoutRoute de-duplication and merges slot ownership in LayoutSlotMap, producing incorrect LayoutProps typings for slot-local layouts (missing or extra slot props).

Useful? React with 👍 / 👎.

@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bcc3a36b17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +24 to +25
await fs.mkdir(path.dirname(outPath), { recursive: true });
await fs.writeFile(outPath, content, "utf-8");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Generate next-env.d.ts alongside route types

generateRouteTypes only writes .next/types/routes.d.ts, but vinext typegen is intended to be a standalone pre-typecheck step and many Next-style TS configs include next-env.d.ts (usually gitignored). In a fresh checkout where that file is absent, running vinext typegen && tsc --noEmit still fails because the env declaration file is never regenerated, so the command does not fully satisfy the expected typegen workflow.

Useful? React with 👍 / 👎.

@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

Hi @james-elicx, this is ready for human review when you have a moment.

  • All CI green; prior Codex P1/P2 rounds addressed (slot ownership via ownerLayoutId, slot-segment preservation in disambiguation, layout params derived from manifest, explicit LayoutProps generic, serialized dev regeneration, decoded layout literals).
  • Latest commit 5900749 adds Next-compatible next-env.d.ts stub generation so the standalone vinext typegen && tsc --noEmit workflow works on a fresh checkout (existing files are preserved; covered by two new tests in tests/typegen.test.ts).
  • The latest Codex P2 about treePathToRouteLiteral() decoding appears to be a false positive — decodeRouteSegment is already applied at packages/vinext/src/typegen.ts and the app/%5Fsites/* case is exercised by the first generateRouteTypes test.

Happy to split this into smaller PRs (e.g. CLI command vs dev-mode integration vs layout slot wiring) if that would make review easier.

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Add native App Router route type generation

Overall this is solid work — the type model design is clean, the layout disambiguation via createLayoutRouteKeyMap is well thought out, the serialized dev regeneration handles rapid watcher bursts correctly, and the test coverage is thorough (slot ownership, route groups, escaped segments, dev-mode invalidation). The Codex review rounds addressed all the major issues I would have flagged.

A few things I'd like to see addressed before merge:

Issues

  1. addRoute uses Array.includes for dedup — O(n) per insertion makes overall route collection O(n²). For typical apps this is fine, but it's a latent scaling issue. A Set alongside the array would be cleaner.

  2. Duplicate isInvisibleSegmenttypegen.ts re-implements isInvisibleSegment identically to the one in app-route-graph.ts. This is a maintenance risk — if the canonical definition changes (e.g. intercepting route segments become invisible), the typegen copy drifts silently. Consider exporting it from the routing module.

  3. next-env.d.ts references next types — The generated stub includes /// <reference types="next" /> and /// <reference types="next/image-types/global" />. For vinext projects that don't have next installed as a dependency, this will cause TS errors. Should this reference vinext's own type declarations instead, or should the stub be conditional on whether next is actually installed?

  4. Race in drainAppRouteTypeGeneration finally block — The finally callback sets appRouteTypeGeneration = null then checks appRouteTypeGenerationPending and re-invokes regenerateAppRouteTypes(). But between null assignment and the if check, there's no actual race (single-threaded), so this is fine. However, the pattern is a bit subtle — a brief comment explaining why the finally re-check is needed would help future readers.

See inline comments for specifics.

if (!routes.includes(route)) {
routes.push(route);
routes.sort(compareStrings);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O(n) includes on every insertion makes this O(n²) overall. For typical apps with <100 routes this doesn't matter, but a Set alongside the sorted array would be cleaner and avoid the latent scaling issue:

Suggested change
}
function addRoute(
routes: string[],
params: Map<string, ParamShape>,
route: string,
paramShape: ParamShape,
): void {
if (!params.has(route)) {
routes.push(route);
routes.sort(compareStrings);
}
params.set(route, paramShape);
}

Since params is already keyed by route, you can use it as the dedup check and avoid the linear scan entirely. (The sort-on-every-push is also O(n² log n) total — could do a single sort at the end of collectRouteTypeModel — but that's a much smaller concern.)

Comment thread packages/vinext/src/typegen.ts Outdated
return (
segment === "." || (segment.startsWith("(") && segment.endsWith(")")) || segment.startsWith("@")
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an exact copy of the isInvisibleSegment in app-route-graph.ts:1860. Duplicating this definition is a maintenance risk — if the canonical definition in the routing module changes (e.g., intercepting route prefixes become invisible), this copy won't be updated.

Consider exporting isInvisibleSegment from app-route-graph.ts (or a shared routing util) and importing it here.


const NEXT_ENV_FILE_CONTENT = `/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import "./.next/types/routes.d.ts" line uses a relative import from the project root, which means next-env.d.ts must live at the project root for this to resolve. That's the standard location, so it works, but it's worth a comment explaining the coupling.

More importantly: /// <reference types="next" /> will fail in vinext projects that don't have next installed. Is the intent that next is always a peer dep? If not, this stub should either reference vinext's own type declarations or be conditional on whether next is resolvable.

This is a nit for now since migrated projects will have next installed, but it'll bite fresh vinext-only projects.

appRouteTypeGeneration = drainAppRouteTypeGeneration().finally(() => {
appRouteTypeGeneration = null;
if (appRouteTypeGenerationPending) regenerateAppRouteTypes();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The finally block sets appRouteTypeGeneration = null then re-checks appRouteTypeGenerationPending. Since JS is single-threaded, there's no actual race here, but the pattern is subtle. A brief comment explaining why the re-check is needed would help:

Suggested change
});
appRouteTypeGeneration = drainAppRouteTypeGeneration().finally(() => {
appRouteTypeGeneration = null;
// An event may have arrived after drain finished but before we cleared
// the in-flight promise. Re-check and restart if so.
if (appRouteTypeGenerationPending) regenerateAppRouteTypes();
});

params: Promise<VinextRouteTypes.ParamMap[Route]>;
children: React.ReactNode;
} & {
[K in VinextRouteTypes.LayoutSlotMap[Route]]: React.ReactNode;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The { mapped type brace is on the same line as the & { intersection. This is correct but dense. More importantly, when LayoutSlotMap[Route] resolves to never (no slots), the intersection & { [K in never]: ReactNode } produces & {} which is fine — just verifying this is intentional and tested (it is, via the "/@modal": never assertion).


for (const slot of route.parallelSlots) {
const ownerLayoutId = findSlotOwnerLayoutId(route, slot);
if (slot.layoutPath) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice — materializing slot-local layouts in the segment graph is the right call. This means app/@modal/layout.tsx gets its own layout entry with a treePath of /@modal, which keeps it distinct from the root layout in typegen's LayoutRoute union.

One question: if a slot has a layoutPath but its ownerLayoutId points to an ancestor, the slot-local layout gets registered here but the slot itself (line 465) still references the ancestor via ownerLayoutId. This is correct for typegen (slot props go to the ancestor), but could it confuse other consumers of the segment graph that iterate layouts and expect every layout to have at least one slot? Worth a brief comment documenting that slot-local layouts may have zero slots in the manifest.

root,
mode: "production",
});
const resolvedNextConfig = await resolveNextConfig(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: resolvedNextConfig loads both loadNextConfig and resolveNextConfig, which reads the config file and resolves paths. If the project has no next.config.{js,ts,mjs}, does loadNextConfig throw or return defaults? If it throws, the error message won't be very helpful for a typegen command. Consider catching and providing a friendlier message like "No next.config found; using defaults".

Comment thread tests/typegen.test.ts

let server: ViteDevServer | null = null;
try {
server = await createServer({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dev-mode test passes { appDir: root } to vinext(), which sets appDir to the project root rather than root + '/app'. This works because the plugin detects the app/ subdirectory, but it's a bit confusing — at first glance it looks like the app dir is the root. Consider passing path.join(root, 'app') for clarity, or add a comment explaining why root works here.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 11, 2026

Review posted. Here's a summary of what I found:

The PR is well-structured — it adds a clean typegen.ts module that generates PageProps, LayoutProps, and RouteContext types from vinext's existing App Router route graph, with a CLI command, dev-mode auto-regeneration, and thorough tests. The 10 commits show good iterative improvement (Codex rounds addressed slot ownership, layout param derivation, serialized regeneration, and scoped route groups).

Key feedback items:

  1. Duplicate isInvisibleSegment — The copy in typegen.ts should import from the routing module instead of re-implementing it, to avoid silent drift.

  2. addRoute O(n²) dedup — Uses Array.includes per insertion; can use the already-present params Map as the dedup set instead.

  3. next-env.d.ts references next types — Will fail for vinext-only projects that don't have next installed. Needs a decision on whether this should reference vinext types or be conditional.

  4. Serialization comment — The finally re-check pattern in drainAppRouteTypeGeneration is correct but subtle; suggested adding a clarifying comment.

  5. Minor nits on test clarity (appDir: root vs appDir: root + '/app') and loadNextConfig error handling in the typegen CLI command.

github run

@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @james-elicx / @ask-bonk. Addressed in 2ce91c8:

  1. addRoute dedup — switched to per-list Set lookups. Side-finding while doing this: the params Map can't be used as the dedup set because it's shared across page/layout/handler routes; using it would silently drop layouts whose route already exists as a page (e.g. /). Caught by the existing slot-local layout test once I tried that path.
  2. Duplicate isInvisibleSegment — exported from routing/app-route-graph.ts and imported in typegen.ts. Single source of truth now.
  3. Drain finally re-check — added explanatory comment at packages/vinext/src/index.ts:2085.
  4. Slot-local layouts may have zero slots — added a documenting comment at packages/vinext/src/routing/app-route-graph.ts:444 explaining the ownerLayoutId redirection.
  5. appDir: root in dev test — added a clarifying comment about the plugin option naming the project root and auto-detecting app/.

Deferred (happy to follow up if you'd prefer them in this PR):

  • next-env.d.ts next type references — kept as-is to match the existing convention in examples/app-router-playground/next-env.d.ts and examples/realworld-api-rest, both of which use the same <reference types="next" /> lines and rely on skipLibCheck: true. Switching to vinext-specific references here would diverge from those examples. Worth a follow-up that updates the examples in lockstep.
  • Friendlier loadNextConfig error message in vinext typegen — Next-style projects without next.config are rare; the current path returns defaults rather than throwing, so the error message Codex was worried about doesn't actually surface in practice. Happy to add a try/catch if you'd still like it.

The LayoutSlotMap[Route] = never intersection is intentional and exercised by the '/@modal': never assertion as noted.

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Add native App Router route type generation

This is well-designed work. The type model is clean, the layout disambiguation via createLayoutRouteKeyMap handles the tricky route-group collision cases correctly, the serialized drain loop for dev regeneration is sound, and the test coverage is thorough across slot ownership, escaped segments, route groups, and dev-mode invalidation. The iterative Codex rounds addressed all the major structural issues (slot ownership, layout params from manifest, scoped route groups, serialized writes).

I have a few remaining items — two that should be fixed before merge, and several minor observations.

Should fix

  1. addRoute sorts on every insertionroutes.sort(compareStrings) runs after every push, making the total cost O(n² log n) across all routes. Since the sort only matters for the final output, deferring it to after collection completes (or using insertion sort for O(n²) but better constants) would be cleaner. Not a correctness issue, but it's unnecessary work and the pattern is easy to misread as intentional. See inline comment.

  2. Slot name dedup uses Array.includes — Same pattern at line 116. Slot lists per layout are small so this is even less of a practical concern, but the inconsistency with the route dedup (which uses Set) stands out. Consider using a Set here too for consistency.

Minor / nits

  1. treePathToScopedLayoutRouteLiteral only filters . — The disambiguation function keeps @slot and (group) segments, which is correct for distinguishing app/layout.tsx from app/@modal/layout.tsx. But it also keeps route-group segments like (marketing), which means two layouts under different groups at the same path disambiguate correctly too. Just want to confirm this is the intentional design — the "/(marketing)" test assertion suggests it is.

  2. writeRouteTypes during configResolved — This runs await writeRouteTypes() at plugin config time (line 875), which means the initial typegen runs synchronously in the config resolution path. For large apps this could slow down Vite startup noticeably. Not blocking, but worth noting — if it becomes a problem, it could be deferred to configureServer with the first write happening asynchronously.

  3. next-env.d.ts references next types — Acknowledged in the PR thread as a known gap for vinext-only projects. The deferral reasoning (matching existing examples that use skipLibCheck: true) is reasonable. Tracking this as a follow-up is fine.

Comment thread packages/vinext/src/typegen.ts Outdated
if (!seen.has(route)) {
seen.add(route);
routes.push(route);
routes.sort(compareStrings);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorting after every push makes addRoute O(n log n) per call and O(n² log n) total across all routes. Since the sorted order only matters for the rendered output, consider deferring the sort to after all routes are collected:

Suggested change
routes.sort(compareStrings);
if (!seen.has(route)) {
seen.add(route);
routes.push(route);
}

Then add a single routes.sort(compareStrings) call at the end of collectRouteTypeModel for each route list (or sort in renderRouteTypes). The current approach is correct but does unnecessary work.

Comment thread packages/vinext/src/typegen.ts Outdated
if (!layoutRoute) continue;

const slots = model.layoutSlots.get(layoutRoute) ?? [];
if (!slots.includes(slot.name)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this uses Array.includes for slot name dedup while route dedup (line 308) uses a Set. For consistency, consider using a Set<string> here too, even though slot lists per layout are small enough that it doesn't matter for performance:

Suggested change
if (!slots.includes(slot.name)) {
const slots = model.layoutSlots.get(layoutRoute) ?? [];
if (!slots.includes(slot.name)) {
slots.push(slot.name);
}

Or better yet, track a Set alongside the array (same pattern as pageRouteSet/layoutRouteSet) and defer the sort to after the loop.

return segments.length === 0 ? "/" : `/${segments.join("/")}`;
}

function treePathToScopedLayoutRouteLiteral(treePath: string): string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a subtle but important distinction from treePathToRouteLiteral — it keeps @slot and (group) segments for disambiguation. The asymmetry between the two functions (one filters isInvisibleSegment, the other only filters .) is correct but easy to misread.

Consider a doc-comment explaining when this variant is used and why it preserves route-group/slot segments:

Suggested change
function treePathToScopedLayoutRouteLiteral(treePath: string): string {
/**
* Convert a layout tree path to a scoped route literal that preserves
* route-group and @slot segments. Used only for disambiguation when
* multiple layouts collapse to the same URL route literal.
*/
function treePathToScopedLayoutRouteLiteral(treePath: string): string {

return layoutRouteKeys.get(layout.treePath) ?? treePathToRouteLiteral(layout.treePath);
}

function treePathToRouteLiteral(treePath: string): string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestion — a brief doc-comment here would help readers understand the contract:

Suggested change
function treePathToRouteLiteral(treePath: string): string {
/** Convert a layout tree path to its URL route literal, stripping invisible segments. */
function treePathToRouteLiteral(treePath: string): string {

return outPath;
}

async function ensureNextEnvFile(root: string): Promise<void> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ensureNextEnvFile check uses fs.access to test existence and silently catches to create. This is correct, but there's a TOCTOU window — if two concurrent generateRouteTypes calls race (e.g., during the dev drain loop), both could see the file as absent and write. In practice the content is idempotent so this is harmless, but fs.writeFile with { flag: 'wx' } (exclusive create) would be more precise:

Suggested change
async function ensureNextEnvFile(root: string): Promise<void> {
async function ensureNextEnvFile(root: string): Promise<void> {
const envPath = path.join(root, "next-env.d.ts");
try {
await fs.writeFile(envPath, NEXT_ENV_FILE_CONTENT, { flag: "wx" });
} catch (error: unknown) {
// File already exists — leave it untouched.
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
}
}

This is a nit — the current approach works fine.

instrumentationPath = findInstrumentationFile(root, fileMatcher);
instrumentationClientPath = findInstrumentationClientFile(root, fileMatcher);
middlewarePath = findMiddlewareFile(root, fileMatcher);
await writeRouteTypes();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This await writeRouteTypes() runs in the configResolved hook, which means it blocks Vite startup. For most apps this is fast, but for large route trees the FS scan + write could add noticeable latency to vite dev startup.

Not blocking — just flagging that if startup perf becomes a concern, this could be moved to fire-and-forget in configureServer (since the types aren't needed for Vite to start serving).

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 12, 2026

Review posted successfully. Here's a summary of what I found:

Overall assessment: This is solid, well-iterated work. The type model design is clean, layout disambiguation via createLayoutRouteKeyMap correctly handles route-group collisions, the serialized drain loop for dev regeneration is sound, and test coverage is thorough.

Key feedback:

  1. addRoute sorts on every insertion (should fix) — routes.sort() after every push makes collection O(n² log n). Deferring the sort to after all routes are collected would be cleaner.

  2. Slot name dedup inconsistency (should fix) — Uses Array.includes while route dedup uses Set. Should use the same pattern for consistency.

  3. Missing doc-comments on disambiguation functions (nit) — treePathToRouteLiteral vs treePathToScopedLayoutRouteLiteral have subtle but important semantic differences that deserve doc-comments.

  4. ensureNextEnvFile TOCTOU (nit) — fs.access + fs.writeFile has a race window; fs.writeFile with { flag: 'wx' } would be more precise.

  5. writeRouteTypes blocks Vite startup (observation) — Runs in configResolved, which could add latency for large route trees. Flagged as a potential future optimization.

  6. next-env.d.ts references next types — Acknowledged deferral is reasonable given existing examples use skipLibCheck: true.

github run

@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

Thanks @james-elicx / @ask-bonk. Addressed in e06d6d7:

  1. addRoute per-insertion sort — dropped the in-loop routes.sort(). collectRouteTypeModel now does a single sort pass over pageRoutes, layoutRoutes, routeHandlerRoutes, and each layoutSlots array after collection completes. Comment in place explaining why.
  2. Slot name dedup uses Set — slot collection now uses a parallel Map<string, Set<string>> for O(1) dedup, same pattern as the per-list route Sets. The deferred sort above covers slot ordering too.
  3. Doc comments on treePathToRouteLiteral / treePathToScopedLayoutRouteLiteral — added; calls out the asymmetric segment filter and that the scoped variant only fires as a disambiguation fallback.
  4. ensureNextEnvFile TOCTOU nit — switched to fs.writeFile(..., { flag: "wx" }) with an EEXIST swallow. Same behavior, no access/writeFile race window.

Skipped per your earlier framing:

  • writeRouteTypes in configResolved — agree this could become a Vite startup concern for very large route trees, but moving it to configureServer (fire-and-forget) changes the timing contract a bit (first tsc --noEmit after vite dev start could race the initial write). Happy to do it in a follow-up once we have a real signal; for now keeping the deterministic ordering.
  • next-env.d.ts next references — left as-is per the prior thread.

@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

Hey @james-elicx, quick ping from my side. The last review blocker is fixed now and CI is green on dcc0c0f.

I can’t trigger /bigbonk review from here, but I think this is ready for your final look whenever you get a minute.

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Add native App Router route type generation

This is well-designed, well-iterated work. The type model is clean, the layout disambiguation via createLayoutRouteKeyMap handles route-group collisions correctly, the serialized drain loop for dev regeneration is sound, and the test coverage is thorough (slot ownership, escaped segments, route groups, dev-mode invalidation, next-env.d.ts preservation). The paramShapesEqual assertion on duplicate routes is a nice defensive measure.

The prior review rounds have addressed all the major structural issues. I have a few remaining items — one that I think should be fixed, and several observations.

Should fix

  1. addRoute still does redundant params.set on known-equal shapes — After the paramShapesEqual check confirms equality, the function unconditionally calls params.set(route, paramShape) even though the existing shape is identical. This is a minor clarity issue — the early return when seen.has(route) already skips the push, but the params.set still runs. Skipping it when the shape already exists would make the intent clearer and avoid an unnecessary Map write on every duplicate.

Observations / nits

  1. next-env.d.ts import vs /// <reference path> — The current implementation matches Next.js canary (import for ESLint compat) and the existing vinext examples. Good.

  2. export {} in generated output — Redundant given the import type * as React already makes the file a module, but reasonable as defensive codegen practice. Fine to keep.

  3. await writeRouteTypes() in configResolved — Blocks Vite startup. For typical apps this is fast, but worth noting as a potential optimization target for very large route trees. Not blocking.

  4. Empty ParamMap fallback — When there are no routes, renderParamMap emits [route: string]: {} which is an open index signature. Only matters in the degenerate no-routes case, so not blocking, but worth noting.

  5. CI reliability improvements — The Node HTTP probe with explicit 127.0.0.1, --max-time-equivalent timeout, and per-attempt logging is a solid improvement over the prior curl-based approach. The trap for cleanup is good practice.

  6. Link navigation test fix — Sequentializing the import calls to avoid a mock-order race is the right fix. The prior Promise.all could allow React to load before the mock was applied.

No blocking issues. This is ready to merge.

if (existingParamShape && !paramShapesEqual(existingParamShape, paramShape)) {
throw new Error(`[vinext] Conflicting route param shapes generated for ${route}`);
}
params.set(route, paramShape);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: after the paramShapesEqual check confirms the shapes match, this still unconditionally overwrites the map entry with an identical value. Skipping the write when the key already exists would make the contract clearer:

Suggested change
params.set(route, paramShape);
if (!existingParamShape) {
params.set(route, paramShape);
}

Not blocking — the current code is correct.

]);

return `// This file is generated by vinext. Do not edit.
import type * as React from "react";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor nit: since React is only used for React.ReactNode in the generated output, you could use a named import type and reference ReactNode directly:

Suggested change
import type * as React from "react";
import type { ReactNode } from "react";

Then React.ReactNodeReactNode in the template strings below. Slightly more precise for a declaration file. Not blocking.

routes: readonly string[],
params: ReadonlyMap<string, ParamShape>,
): string {
if (routes.length === 0) return " [route: string]: {};\n";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When routes is empty, this emits an open index signature [route: string]: {} that accepts any string key. The route unions above correctly resolve to never, so PageProps etc. are already unusable (good). But a consumer could write VinextRouteTypes.ParamMap["anything"] and get {} without a type error.

Consider emitting an empty interface body or a // no routes comment instead, so ParamMap stays closed in the no-routes case. Very low priority — only affects the degenerate empty project scenario.

Comment on lines +128 to +135
// Sort all collected route lists once after collection. addRoute() and the
// slot loop above intentionally skip per-insertion sorts to keep collection
// O(n) — the rendered output relies on stable sorted order, so the single
// pass here is enough.
model.pageRoutes.sort(compareStrings);
model.layoutRoutes.sort(compareStrings);
model.routeHandlerRoutes.sort(compareStrings);
for (const slotNames of model.layoutSlots.values()) slotNames.sort(compareStrings);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice — the single deferred sort pass is the right call. Clean comment explaining the design decision.

Comment on lines +255 to +274
function createLayoutRouteKeyMap(layouts: Iterable<{ treePath: string }>): Map<string, string> {
const treePathsByRoute = new Map<string, string[]>();
for (const { treePath } of layouts) {
const route = treePathToRouteLiteral(treePath);
const treePaths = treePathsByRoute.get(route) ?? [];
treePaths.push(treePath);
treePathsByRoute.set(route, treePaths);
}

const keys = new Map<string, string>();
for (const [route, treePaths] of treePathsByRoute) {
for (const treePath of treePaths) {
keys.set(
treePath,
treePaths.length === 1 ? route : treePathToScopedLayoutRouteLiteral(treePath),
);
}
}
return keys;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good design. The two-pass approach (group by URL route literal, then disambiguate collisions) cleanly separates the common case from the edge case. The treePaths.length === 1 guard ensures disambiguation only fires when necessary.

Comment on lines +462 to +467
if (slot.layoutPath) {
// Materialize the slot-local layout as its own entry so consumers
// (e.g. typegen) can distinguish it from the owning layout. Note
// that this layout may have zero entries in `slots`: the slot
// itself is registered below against `ownerLayoutId`, which points
// to the ancestor layout that owns the slot prop.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good documentation of a non-obvious invariant. This comment will prevent future readers from incorrectly assuming every layout has at least one slot entry.

Comment on lines +2095 to +2101
appRouteTypeGeneration = drainAppRouteTypeGeneration().finally(() => {
appRouteTypeGeneration = null;
// A watcher event may have arrived after the drain loop's final
// check but before this finally runs; restart the loop if so.
if (appRouteTypeGenerationPending) regenerateAppRouteTypes();
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drain + finally re-check design is clean and correct. The comment explaining the edge case is helpful. Verified: if writeRouteTypes() throws persistently, the drain loop exits after one iteration and no infinite retry occurs — new watcher events are needed to restart. This is the right behavior.

* hasRemainingVisibleSegments — keep this the single source of truth.
*/
function isInvisibleSegment(segment: string): boolean {
export function isInvisibleSegment(segment: string): boolean {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good — exporting isInvisibleSegment here and importing it in typegen.ts eliminates the prior duplication. Single source of truth.

Comment thread tests/typegen.test.ts
Comment on lines +164 to +212
it("updates generated route helper types when App Router files are added in dev", async () => {
await withTempProject(async (root) => {
await writeProjectFile(root, "app/layout.tsx", EMPTY_LAYOUT);
await writeProjectFile(root, "app/page.tsx", EMPTY_PAGE);

let server: ViteDevServer | null = null;
try {
// `appDir` in the vinext plugin options names the project root, not
// the App Router directory; the plugin auto-detects `app/` (or
// `src/app/`) under it. Pass the project root explicitly here so
// the dev server uses the same root path for both Vite and vinext.
server = await createServer({
root,
logLevel: "silent",
plugins: [vinext({ appDir: root })],
});

const generatedPath = path.join(root, ".next", "types", "routes.d.ts");
await eventually(async () => {
expect(await readFile(generatedPath, "utf-8")).toContain('type PageRoute = "/";');
});

const aboutPage = path.join(root, "app/about/page.tsx");
await writeProjectFile(root, "app/about/page.tsx", EMPTY_PAGE);
server.watcher.emit("add", aboutPage);

await eventually(async () => {
expect(await readFile(generatedPath, "utf-8")).toContain(
'type PageRoute = "/" | "/about";',
);
});

const blogPage = path.join(root, "app/blog/page.tsx");
const docsPage = path.join(root, "app/docs/page.tsx");
await writeProjectFile(root, "app/blog/page.tsx", EMPTY_PAGE);
await writeProjectFile(root, "app/docs/page.tsx", EMPTY_PAGE);
server.watcher.emit("add", blogPage);
server.watcher.emit("add", docsPage);

await eventually(async () => {
expect(await readFile(generatedPath, "utf-8")).toContain(
'type PageRoute = "/" | "/about" | "/blog" | "/docs";',
);
});
} finally {
await server?.close();
}
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorough dev-mode test — covers single file addition, rapid burst of multiple files, and verifies the serialized drain produces the correct final state. The eventually helper with retry is a clean pattern for async watcher assertions.

Comment thread tests/typegen.test.ts
Comment on lines +150 to +162
it("preserves an existing next-env.d.ts", async () => {
await withTempProject(async (root) => {
await writeProjectFile(root, "app/layout.tsx", EMPTY_LAYOUT);
await writeProjectFile(root, "app/page.tsx", EMPTY_PAGE);
const customContent = '/// <reference types="custom" />\n';
await writeProjectFile(root, "next-env.d.ts", customContent);

await generateRouteTypes({ root });
const preserved = await readFile(path.join(root, "next-env.d.ts"), "utf-8");

expect(preserved).toBe(customContent);
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good test — verifies ensureNextEnvFile with { flag: "wx" } preserves existing content. This is important for projects with customized next-env.d.ts files.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 14, 2026

Review posted. Here's a summary:

Overall assessment: This is solid, well-iterated work that's ready to merge. The type model design is clean, layout disambiguation handles route-group collisions correctly, the serialized drain loop for dev regeneration is sound, and test coverage is thorough across all the important edge cases.

Key feedback:

  1. One minor should-fix: addRoute does a redundant params.set after paramShapesEqual confirms the shapes match — skipping the write when the key already exists would make the contract clearer.

  2. Nits/observations: import type { ReactNode } would be slightly more precise, the empty ParamMap fallback uses an open index signature (only affects no-routes case), and await writeRouteTypes() in configResolved blocks startup (potential future optimization).

  3. Good patterns noted: The paramShapesEqual assertion, single deferred sort pass, createLayoutRouteKeyMap two-pass disambiguation, ensureNextEnvFile with { flag: "wx" }, drain + finally re-check design, and the CI reliability improvements are all well done.

No blocking issues found.

github run

@JamesbbBriz JamesbbBriz force-pushed the feature/native-route-typegen branch from dcc0c0f to ac6a012 Compare May 15, 2026 03:12
@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@james-elicx rebased onto latest main, conflict resolved, and CI is green now. I think ask-bonk’s latest review is non-blocking. Could you take another look when you get a chance?

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Add native App Router route type generation

This is solid, well-structured work that has clearly been improved through multiple review iterations. The type model design is clean, the layout disambiguation logic via createLayoutRouteKeyMap is correct, the serialized drain loop for dev regeneration handles rapid watcher bursts properly, and the test coverage is thorough across all the important edge cases.

I have two minor nits — neither is blocking.

Architecture observations

Good decisions:

  • Reusing isInvisibleSegment from the routing module (single source of truth) instead of the earlier duplicated copy.
  • Adding patternParts/paramNames to RouteManifestLayout so typegen derives layout params from structured manifest data rather than regexing rendered route literals. This makes the %5Bslug%5D (escaped bracket) case correct by construction.
  • The paramShapesEqual assertion on duplicate routes — catches real bugs if page/layout/handler shapes ever diverge.
  • ensureNextEnvFile with { flag: "wx" } — correct and race-free.
  • The deferred single-pass sort after collection — O(n log n) total instead of O(n² log n).
  • Using import in next-env.d.ts matching Next.js canary's ESLint-compat rationale.

Drain loop correctness: Verified. The while loop clears pending before await, so events during the write set pending and trigger another iteration. The finally re-check covers the narrow window between the last loop-condition check and microtask scheduling. If writeRouteTypes() throws persistently, the loop exits after one iteration without infinite retry — new watcher events are needed to restart. All correct.

See inline comments for the two nits.

}
return;
}
params.set(route, paramShape);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: after the paramShapesEqual check confirms the shapes are identical (line 335), this still unconditionally writes the identical value back into the map. Skipping the write when the key already exists would make the intent clearer — if the shapes are proven equal, there's nothing to update:

Suggested change
params.set(route, paramShape);
const existingParamShape = params.get(route);
if (existingParamShape) {
if (!paramShapesEqual(existingParamShape, paramShape)) {
throw new Error(`[vinext] Conflicting route param shapes generated for ${route}`);
}
return;
}
params.set(route, paramShape);

Wait — re-reading the code, this is exactly what's already here. My mistake, the early return on line 338 already skips the set. This is correct as-is. (Leaving the comment for transparency.)

routes: readonly string[],
params: ReadonlyMap<string, ParamShape>,
): string {
if (routes.length === 0) return " [route: string]: {};\n";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: when routes is empty (no App Router routes at all), this emits [route: string]: {}; — an open index signature that lets VinextRouteTypes.ParamMap["anything"] resolve to {} without a type error. The route unions above correctly resolve to never, so the generic constraints on PageProps/LayoutProps/RouteContext already prevent misuse. But if you want the ParamMap interface itself to be closed in the empty case, you could emit an empty body or a // no routes comment instead.

Very low priority — only affects the degenerate empty-project scenario.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 15, 2026

Review posted and approved. Here's a summary:

Approved — this PR is ready to merge. The type model design is clean, the layout disambiguation handles all the tricky edge cases (route-group collisions, slot-local layouts, escaped bracket segments), the serialized drain loop for dev regeneration is correct, and test coverage is thorough.

Key things I verified:

  • isInvisibleSegment is properly imported from the routing module (no duplication)
  • Layout params are derived from structured patternParts (not regexed from rendered literals)
  • paramShapesEqual provides a proper assertion on duplicate routes
  • ensureNextEnvFile uses { flag: "wx" } for atomic create-if-absent
  • Deferred single-pass sort is O(n log n) total
  • next-env.d.ts uses import matching Next.js canary
  • Drain loop handles rapid watcher bursts, persistent errors, and the finally edge case correctly

Two minor nits posted inline (neither blocking): the open ParamMap index signature in the empty-routes case, and a self-correction on the params.set flow which is actually already correct.

github run

@JamesbbBriz JamesbbBriz force-pushed the feature/native-route-typegen branch from f7a061e to 8edcb50 Compare May 15, 2026 11:39
@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@james-elicx rebased onto latest main again, resolved the CLI import conflict, and CI is green. Looks ready to merge when you have a moment.

@JamesbbBriz
Copy link
Copy Markdown
Contributor Author

@james-elicx gentle ping on this one. It has an approving review, all checks are green, and it’s clean with main now. Do you think this is good to merge, or is there anything else you’d like me to adjust?

@james-elicx
Copy link
Copy Markdown
Collaborator

I'll have a look on Monday - currently prioritising bug fixes.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement native Vinext route-aware type generation

2 participants