Skip to content

feat: built-in file-system routing (experimental)#123

Merged
uhyo merged 5 commits into
masterfrom
claude/funstack-fs-routing-nef99a
Jun 26, 2026
Merged

feat: built-in file-system routing (experimental)#123
uhyo merged 5 commits into
masterfrom
claude/funstack-fs-routing-nef99a

Conversation

@uhyo

@uhyo uhyo commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Summary

Adds built-in file-system routing to @funstack/static, so users no longer have to wire up routing in userland (as in the previous example-fs-routing). Pages discovered under a configurable directory are mapped to routes and rendered with FUNSTACK Router, generating one static HTML file per route.

This feature is marked experimental and is not yet covered by semantic versioning.

Usage

// vite.config.ts
funstackStatic({
  fsRoutes: {
    dir: "./src/pages",      // default "./src/pages"
    root: "./src/root.tsx",  // HTML shell
    adapter: "./src/my-adapter.ts", // optional; defaults to nextRoutes()
  },
})

The new fsRoutes mode is mutually exclusive with root+app (single-entry) and entries (multiple entries). The plugin synthesizes the entries module (emitting import.meta.glob in a virtual module that Vite transforms), so the user writes no glob themselves.

Design

  • Router is fixed to FUNSTACK Router@funstack/router is an optional peer dependency, required only when using this feature.
  • Convention is pluggable via an adapter (FsRoutesAdapter) — the directory/file-name convention is decoupled from the framework's tree→routes/entries logic.
  • Built-in Next.js-like adapter (nextRoutes) supporting page/layout files, dynamic [param] and catch-all [...param] segments, and (group) route groups.
  • Dynamic routes are statically generated via a generateStaticParams export (Next.js-like); pages receive resolved params as a prop.
  • The runtime primitive createFsRoutesEntries and nextRoutes are exported from @funstack/static/fs-routes as an escape hatch for fully custom routing.

Known limitation

Because static hosting serves one pre-rendered RSC payload per page, soft client-side navigation between different values of the same dynamic route reflects the params of the initially-loaded page. Loading a dynamic URL directly (or via the SPA fallback) always renders correct params, and static routes + layouts navigate fully on the client. This is documented in the guide.

Testing

  • Unit tests for the adapter, static-path collection, and glob-prefix stripping (51 passed).
  • New e2e fixture (fixture-fs-routing) + build/dev specs wired into Playwright.
  • example-fs-routing converted to the built-in feature.
  • Docs updated: new File-System Routing guide + fsRoutes option in the API reference.
  • build, typecheck, lint, format:check all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_012L8PCU7xhNXfuepS7eKzro


Generated by Claude Code

claude added 4 commits June 26, 2026 13:04
Add a built-in file-system router so users no longer have to wire up
routing in userland. Pages discovered under a configurable directory are
mapped to routes and rendered with FUNSTACK Router, generating one static
HTML file per route.

- New `fsRoutes` plugin option (mutually exclusive with `root`+`app` and
  `entries`), taking `dir`, `root`, and an optional `adapter`.
- Convention is pluggable via an `FsRoutesAdapter` (adapter pattern).
- Built-in Next.js-like adapter (`nextRoutes`) supporting `page`/`layout`
  files, dynamic `[param]` and catch-all `[...param]` segments, and
  `(group)` route groups. Dynamic routes are statically generated via a
  `generateStaticParams` export.
- Public entry `@funstack/static/fs-routes` exporting `nextRoutes` and the
  adapter types; `@funstack/router` is an optional peer dependency.
- Unit tests for the adapter and static-path collection; e2e fixture and
  specs; example-fs-routing converted to the built-in feature; docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012L8PCU7xhNXfuepS7eKzro
Address review feedback:

- Drop the dedicated `fsRoutes` plugin option. File-system routing is now
  set up entirely through the existing `entries` option: the user writes an
  entries module that globs pages with `import.meta.glob` and passes them to
  `createFsRoutesEntries` from `@funstack/static/fs-routes`, along with the
  `root` component and an optional `adapter`. This keeps the plugin surface
  to two modes and lets the glob/Root/adapter be plain values.
- `createFsRoutesEntries` now auto-strips the common glob prefix
  (`modulesToRouteFiles`), so no directory needs to be configured twice.
- Mark file-system routing as experimental (not yet covered by semantic
  versioning) in the public API JSDoc and docs.
- Update the example, e2e fixture, and docs to the entries-based usage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012L8PCU7xhNXfuepS7eKzro
Bring back the dedicated `fsRoutes` plugin option as the user-facing way to
enable file-system routing, so users don't have to write `import.meta.glob`
themselves. The plugin synthesizes the entries module (emitting the glob in a
virtual module that Vite transforms) and delegates to `createFsRoutesEntries`
from `@funstack/static/fs-routes`, which remains exported as the underlying
primitive and escape hatch.

- `fsRoutes: { dir?, root, adapter? }` config; mutually exclusive with
  `root`+`app` and `entries`.
- Generated module relies on the runtime's automatic common-prefix stripping,
  so no separate base needs to be threaded through.
- Marked experimental (not yet covered by semantic versioning).
- Example, e2e fixture, and docs switched back to the `fsRoutes` option.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012L8PCU7xhNXfuepS7eKzro
The dev server (vite dev) could not render file-system routes with the
default `ssr: false`: pages are server components rendered through FUNSTACK
Router (a client component), and @vitejs/plugin-rsc serializes that
server-component route data as eval'd dev-JSX that fails to load in the
browser. Production builds are unaffected (the RSC payload is pre-rendered),
and enabling `ssr: true` renders pages on the server in dev as well.

Set `ssr: true` in the example and e2e fixture, and document that SSR is
required for the dev server (and recommended for SEO / initial load).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012L8PCU7xhNXfuepS7eKzro
@uhyo uhyo merged commit b6dbea5 into master Jun 26, 2026
2 checks passed
@uhyo uhyo deleted the claude/funstack-fs-routing-nef99a branch June 26, 2026 23:25
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.

2 participants