feat(router): add typed file routes with codegen and Standard Schema validation with JSON-LD support#2125
Conversation
✅ Deploy Preview for analog-blog ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
I dug into the hot-added route issue and confirmed the original failure mode is addressed in commit d4d014a ( What was happening in the first scenario:
Why
|
|
2f336b2 to
96b75e9
Compare
|
@coderabbitai resume |
✅ Actions performedReviews resumed. |
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (1)
packages/router/src/lib/route-path.ts (1)
192-201:⚠️ Potential issue | 🟠 MajorReject empty arrays for required catch-all params.
{ slug: [] }currently joins to'', and the later slash cleanup turns/docs/[...slug]into/docsinstead of surfacing missing input. Required catch-all segments should fail fast here too.🧭 Suggested fix
url = url.replace(/\[\.\.\.([^\]]+)\]/g, (_, name) => { const value = params[name]; - if (value == null) { + if (value == null || (Array.isArray(value) && value.length === 0)) { throw new Error( `Missing required catch-all param "${name}" for path "${path}"`, ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/router/src/lib/route-path.ts` around lines 192 - 201, In the url.replace handler for catch-all params (the function using params[name] in route-path.ts), add a check to reject empty arrays: if value is an Array and value.length === 0, throw the same Missing required catch-all param error (referencing name and path) instead of joining to an empty string; keep the existing Array.isArray branch to encode/join non-empty arrays. This ensures required catch-all segments like `[...slug]` fail fast when params[name] is [].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/router/src/lib/inject-navigate.ts`:
- Around line 28-35: The returned navigate function's spread signature allows a
NavigationBehaviorOptions object to be structurally accepted as route options
and dropped; fix this by adding explicit function overloads for the returned
function (one overload for calling with only RoutePathArgs<P> and another
overload that requires a separate NavigationBehaviorOptions as the final
argument) so callers must pass extras as a distinct parameter; update the
implementation of the returned function (the closure returned in
inject-navigate.ts) to read extras only from the explicit extras overload
position (e.g., treat the last argument as extras only when the overload that
includes NavigationBehaviorOptions is selected) and add a regression test
asserting that navigate('/about', { replaceUrl: true }) does not lose extras
(and that navigate('/about', undefined, { replaceUrl: true }) still works).
In `@packages/router/src/lib/inject-typed-params.ts`:
- Around line 17-48: Replace the local parser and strict-check with the shared
route parser so optional catch-all params aren't treated as required: import and
call the shared extractRouteParams(...) instead of extractParamNames(...) inside
assertRouteMatch (keep the function name assertRouteMatch and the same
signature), use the returned param descriptors to derive expected params and
only warn when a param is required/missing (skip optional or optional-catch-all
descriptors like [[...slug]]), and remove or deprecate extractParamNames so the
runtime assertion matches the manifest-generated route model.
In `@packages/router/src/lib/route-path.ts`:
- Around line 75-80: RoutePathOptions currently hardcodes query as
Record<string,string|string[]|undefined>, losing route-specific narrowing;
change both occurrences of the query property inside RoutePathOptions to use the
route-specific type by replacing the hardcoded shape with RouteQueryOutput<P>
(keeping the optional flag), analogous to how params uses RouteParamsOutput<P>,
so the two branches become query?: RouteQueryOutput<P> and ensure the generic P
is used in both branches where params already uses RouteParamsOutput<P>.
- Around line 72-82: The type check treating "no params" uses Params extends
Record<string, never> which misses cases where all params are optional (e.g., {
category?: string[] }); update the conditional in RoutePathOptions and
RoutePathArgs to detect "all-optional" params by checking keyof Required<Params>
extends never (i.e., replace the Params extends Record<string, never> branch
with a check like keyof Required<Params> extends never) so routes whose params
are present but all optional are treated as no-params and the options/args
become optional accordingly; apply this change to the conditional branches that
currently reference Params (types RoutePathOptions and RoutePathArgs /
AnalogRouteTable usage).
In `@packages/vite-plugin-routes/src/lib/route-file-discovery.ts`:
- Around line 39-46: joinDir currently prepends workspaceRoot for inputs
starting with '/' or resolving relative paths but incorrectly double-prepends
when entries in additionalPagesDirs/additionalContentDirs are already full
normalized paths; update joinDir to follow the three-way dispatch used by
normalizeWatchedDir: if the incoming dir already starts with workspaceRoot
return normalizePath(dir) directly; else if dir startsWith('/') return
normalizePath(`${workspaceRoot}${dir}`); otherwise return
normalizePath(resolve(workspaceRoot, dir)); apply this logic to where
additionalPagesRoots/additionalContentRoots are computed so already-normalized
`${workspaceRoot}/...` entries are not modified.
In `@packages/vite-plugin-routes/src/lib/typed-routes-plugin.ts`:
- Around line 177-200: The JSON-LD entries are built from raw
routeFiles/contentFiles which can conflict with the collision resolution in
generateRouteManifest; update generate() so collision precedence from manifest
is applied before calling buildJsonLdEntries (e.g., compute the set of canonical
file paths from manifest.routes and pass only those canonical files or pass
manifest into buildJsonLdEntries) so jsonLdEntries honors the same winner
selection as manifest.routes; adjust the call that currently uses
(resolveDiscoveredFile, routeFiles, contentFiles) to use the manifest-filtered
file list or manifest-aware logic (affecting generate(), buildJsonLdEntries, and
any use of jsonLdEntries.map(entry => entry.routePath)).
- Around line 121-175: ensureEntryImport currently mutates src/main.ts or
src/main.server.ts even during read-only verification runs; modify
ensureEntryImport (or call sites like buildStart which invokes generate() then
ensureEntryImport) to skip any file writes when resolvedOptions.verify or
resolvedOptions.verifyOnBuild (or a similar "verify" flag in resolvedOptions) is
true — i.e., compute specifier and detect existing import as now, but before
writeFileSync/console.log/warn return early if in verify mode; apply the same
guard to the other mutation site around lines 269-271 so verification/freshness
checks remain read-only.
---
Duplicate comments:
In `@packages/router/src/lib/route-path.ts`:
- Around line 192-201: In the url.replace handler for catch-all params (the
function using params[name] in route-path.ts), add a check to reject empty
arrays: if value is an Array and value.length === 0, throw the same Missing
required catch-all param error (referencing name and path) instead of joining to
an empty string; keep the existing Array.isArray branch to encode/join non-empty
arrays. This ensures required catch-all segments like `[...slug]` fail fast when
params[name] is [].
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 37a03574-48df-4953-8c87-bac824b50b1e
⛔ Files ignored due to path filters (2)
.dagger/src/index.tsis excluded by none and included by none.nxignoreis excluded by none and included by none
📒 Files selected for processing (44)
.github/pr-scope-map.json.github/workflows/ci.ymlapps/analog-app-e2e/playwright.config.tsapps/analog-app/project.jsonapps/analog-app/src/main.tsapps/docs-app/docs/features/data-fetching/validation.mdapps/docs-app/docs/features/routing/typed-routes.mdapps/tanstack-query-app-e2e/playwright.config.tsapps/tanstack-query-app/project.jsonpackages/content-plugin/project.jsonpackages/content-plugin/tsdown.config.tspackages/content/project.jsonpackages/create-analog/project.jsonpackages/platform/src/lib/options.tspackages/router/manifest/src/index.tspackages/router/project.jsonpackages/router/src/index.tspackages/router/src/lib/experimental.tspackages/router/src/lib/inject-navigate.spec.tspackages/router/src/lib/inject-navigate.tspackages/router/src/lib/inject-typed-params.spec.tspackages/router/src/lib/inject-typed-params.tspackages/router/src/lib/route-generation.integration.spec.tspackages/router/src/lib/route-manifest.spec.tspackages/router/src/lib/route-path.spec.tspackages/router/src/lib/route-path.tspackages/storybook-angular/src/lib/preset.spec.tspackages/vite-plugin-routes/README.mdpackages/vite-plugin-routes/src/index.tspackages/vite-plugin-routes/src/lib/json-ld-manifest-plugin.spec.tspackages/vite-plugin-routes/src/lib/json-ld-manifest-plugin.tspackages/vite-plugin-routes/src/lib/route-file-discovery.spec.tspackages/vite-plugin-routes/src/lib/route-file-discovery.tspackages/vite-plugin-routes/src/lib/typed-routes-plugin.spec.tspackages/vite-plugin-routes/src/lib/typed-routes-plugin.tspackages/vite-plugin-routes/tsconfig.lib.jsonpackages/vite-plugin-routes/vite.config.lib.tspackages/vitest-angular-tools/collection.jsonpackages/vitest-angular-tools/project.jsonpackages/vitest-angular-tools/vite.config.lib.tspackages/vitest-angular/project.jsontools/scripts/build-lib.mtstools/scripts/verify-package-artifacts.mtstools/scripts/verify-route-freshness.mts
✅ Files skipped from review due to trivial changes (10)
- packages/router/project.json
- packages/vite-plugin-routes/tsconfig.lib.json
- packages/vite-plugin-routes/src/lib/route-file-discovery.spec.ts
- packages/vite-plugin-routes/README.md
- packages/vite-plugin-routes/src/index.ts
- apps/docs-app/docs/features/routing/typed-routes.md
- packages/router/src/lib/route-path.spec.ts
- apps/docs-app/docs/features/data-fetching/validation.md
- packages/router/src/lib/route-generation.integration.spec.ts
- packages/router/src/lib/experimental.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- apps/tanstack-query-app-e2e/playwright.config.ts
- .github/pr-scope-map.json
- packages/content/project.json
- packages/create-analog/project.json
- packages/vite-plugin-routes/src/lib/json-ld-manifest-plugin.ts
- packages/vite-plugin-routes/src/lib/json-ld-manifest-plugin.spec.ts
- packages/router/manifest/src/index.ts
Pitfall: Unnecessary package separation adds complexityThe dependency chain for typed route codegen is:
Both could live in
The current separation creates: an extra published package ( |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
apps/blog-app/src/routeTree.gen.ts (1)
325-340:⚠️ Potential issue | 🟠 MajorRegenerate this route tree; the lib content path is still malformed.
Lines 326-328 still collapse
"/libs/shared/feature/src/content/test.md"into"/libs/shared/featuretest", and Line 417 repeats that bad token in the lookup map. That will surface the wrong full path in typed navigation/runtime lookups. Please rerun codegen; if it reproduces, thesrc/contentnormalization is still dropping the separator and needs to emit.../feature/testinstead.Also applies to: 417-417
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blog-app/src/routeTree.gen.ts` around lines 325 - 340, The route tree generation collapsed the path segment separator so the route id and fullPath show "/libs/shared/featuretest" instead of the correct "/libs/shared/feature/test" (see the generated entry that currently has id "/libs/shared/featuretest" and sourceFile "/libs/shared/feature/src/content/test.md" and the lookup map entry for the same token). Regenerate the route tree codegen and fix the path normalization logic that builds route ids/fullPath from sourceFile (the normalization that converts "src/content/test.md" into the route segment), ensuring it preserves the separator so it emits ".../feature/test" (and update any code that fills AnalogFileRoutesById keys/lookups to use the corrected token).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@apps/blog-app/src/routeTree.gen.ts`:
- Around line 325-340: The route tree generation collapsed the path segment
separator so the route id and fullPath show "/libs/shared/featuretest" instead
of the correct "/libs/shared/feature/test" (see the generated entry that
currently has id "/libs/shared/featuretest" and sourceFile
"/libs/shared/feature/src/content/test.md" and the lookup map entry for the same
token). Regenerate the route tree codegen and fix the path normalization logic
that builds route ids/fullPath from sourceFile (the normalization that converts
"src/content/test.md" into the route segment), ensuring it preserves the
separator so it emits ".../feature/test" (and update any code that fills
AnalogFileRoutesById keys/lookups to use the corrected token).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6dd7cd78-d3b7-40f1-802d-6518f2b5a497
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamland included by none
📒 Files selected for processing (4)
apps/analog-app/src/routeTree.gen.tsapps/blog-app/src/main.tsapps/blog-app/src/routeTree.gen.tspackage.json
✅ Files skipped from review due to trivial changes (2)
- apps/blog-app/src/main.ts
- apps/analog-app/src/routeTree.gen.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- package.json
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/vite-plugin-nitro/src/lib/build-sitemap.ts (1)
196-200: Verify:new URL(host)throws on invalid host strings.
normalizeSitemapHostusesnew URL(host)which throwsTypeErrorfor invalid URLs. This provides early validation but means consumers with invalidhostvalues will see an uncaught error.This is likely intentional (fail-fast), but worth noting that the error won't be user-friendly. Consider whether a more descriptive error message would help.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/vite-plugin-nitro/src/lib/build-sitemap.ts` around lines 196 - 200, normalizeSitemapHost currently calls new URL(host) which will throw a raw TypeError on invalid input; wrap the URL construction in a try/catch inside normalizeSitemapHost (or pre-validate the string) and rethrow a clearer, user-friendly error that includes the problematic host string and guidance (e.g., "Invalid sitemap host: '<host>' — expected a fully qualified URL including scheme"), or return a controlled fallback; reference normalizeSitemapHost and the URL construction to locate where to add the try/catch and the improved error message.packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts (1)
16-21: Minor: Reset order inafterEachmay not work as intended.
vi.restoreAllMocks()restores mocks to their original implementation, which would undo themockReturnValue(true)set on line 18. The subsequentmockReset()calls then clear call history but leave the mock in its restored state.If the intent is to have
existsSyncMockreturntrueby default for subsequent tests, consider reordering or usingbeforeEachfor the default setup.That said, since each test explicitly sets
existsSyncMock.mockReturnValue(...)when needed, this is functionally correct — just slightly confusing.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts` around lines 16 - 21, The afterEach resets/ restores mocks in the wrong order causing existsSyncMock.mockReturnValue(true) to be undone by vi.restoreAllMocks(); fix by either moving the default setup into a beforeEach (set existsSyncMock.mockReturnValue(true) there) or by reordering afterEach so vi.restoreAllMocks() runs first and then call existsSyncMock.mockReturnValue(true), keeping mkdirSyncMock.mockReset() and writeFileSyncMock.mockReset() as needed; refer to the afterEach block and the mocks existsSyncMock, mkdirSyncMock, writeFileSyncMock and the call vi.restoreAllMocks().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts`:
- Around line 16-21: The afterEach resets/ restores mocks in the wrong order
causing existsSyncMock.mockReturnValue(true) to be undone by
vi.restoreAllMocks(); fix by either moving the default setup into a beforeEach
(set existsSyncMock.mockReturnValue(true) there) or by reordering afterEach so
vi.restoreAllMocks() runs first and then call
existsSyncMock.mockReturnValue(true), keeping mkdirSyncMock.mockReset() and
writeFileSyncMock.mockReset() as needed; refer to the afterEach block and the
mocks existsSyncMock, mkdirSyncMock, writeFileSyncMock and the call
vi.restoreAllMocks().
In `@packages/vite-plugin-nitro/src/lib/build-sitemap.ts`:
- Around line 196-200: normalizeSitemapHost currently calls new URL(host) which
will throw a raw TypeError on invalid input; wrap the URL construction in a
try/catch inside normalizeSitemapHost (or pre-validate the string) and rethrow a
clearer, user-friendly error that includes the problematic host string and
guidance (e.g., "Invalid sitemap host: '<host>' — expected a fully qualified URL
including scheme"), or return a controlled fallback; reference
normalizeSitemapHost and the URL construction to locate where to add the
try/catch and the improved error message.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9d5aedc4-62a3-462b-9531-400d4bdc954c
📒 Files selected for processing (10)
apps/docs-app/docs/features/routing/typed-routes.mdapps/docs-app/docs/features/server/static-site-generation.mdpackages/platform/src/index.tspackages/platform/src/lib/options.tspackages/vite-plugin-nitro/src/index.tspackages/vite-plugin-nitro/src/lib/build-sitemap.spec.tspackages/vite-plugin-nitro/src/lib/build-sitemap.tspackages/vite-plugin-nitro/src/lib/options.tspackages/vite-plugin-nitro/src/lib/vite-plugin-nitro.spec.tspackages/vite-plugin-nitro/src/lib/vite-plugin-nitro.ts
✅ Files skipped from review due to trivial changes (1)
- apps/docs-app/docs/features/routing/typed-routes.md
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/platform/src/index.ts
- packages/platform/src/lib/options.ts
|
@coderabbitai quick update on the latest push:
The newest commits on the branch are |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 282 🏁 Script executed: Length of output: 62524
Inject navigate, typing, and runtime guards
Typed routes docs and sitemap docs
Nitro: dynamic client outDir and sitemap pipeline
closeBundle extraction
Typed-routes plugin polish
Light suggestions (non-blocking)
Everything else looks solid. Nice work tightening the contracts, guarding verify/freshness paths against unintended mutations, and expanding doc clarity exactly where devs will look. 🧠 Learnings used |
…validation with JSON-LD support
b163d05 to
f15da7d
Compare
|
@brandonroberts ready to merge ... finally. |


PR Checklist
Closes #2044
Affected scope
Recommended merge strategy for maintainer [optional]
Commit preservation note [optional]
Crosses multiple scopes because typed file routes require coordinated changes across codegen, platform opt-in, router runtime, and content metadata. Squash merge is still preferred.
What is the new behavior?
Typed file-route codegen —
@analogjs/vite-plugin-routesscans.page.tsand content files, generatessrc/routeTree.gen.tswith typed route table, route tree metadata, and JSON-LD manifest.Standard Schema validation —
defineAction()anddefineApiRoute()validate params, query, and body using any Standard Schema library (Valibot, Zod, ArkType). Content frontmatter validation viacontentFileResource({ schema }).Router runtime —
routePath()type-safe URL builder,buildUrl()with error on missing required params,injectTypedParams()/injectTypedQuery()signal accessors,routeLinkpipe.Platform integration —
experimental.typedRouter: trueinanalog()config enables codegen via@analogjs/vite-plugin-routes. JSON-LD manifest combined intorouteTree.gen.ts.Vite 6–8 compat — build configs use
rollupOptions(cross-version) with commentedrolldownOptionsfor future Vite 8-only migration.Test plan
nx format:checkpnpm buildpnpm testDoes this PR introduce a breaking change?
Other information
Scoped to typed file routes, route contracts, and the supporting platform/runtime/docs work needed to make the feature production-ready.