diff --git a/packages/docs/src/pages/api/FunstackStatic.mdx b/packages/docs/src/pages/api/FunstackStatic.mdx index 8c5aeac..87f6194 100644 --- a/packages/docs/src/pages/api/FunstackStatic.mdx +++ b/packages/docs/src/pages/api/FunstackStatic.mdx @@ -72,6 +72,7 @@ export default defineConfig({ fsRoutes: { dir: "./src/pages", root: "./src/root.tsx", + adapter: "@funstack/static/fs-routes/next-adapter", }, }), react(), @@ -203,16 +204,16 @@ funstackStatic({ fsRoutes: { dir: "./src/pages", root: "./src/root.tsx", - adapter: "./src/my-adapter.ts", // optional + adapter: "@funstack/static/fs-routes/next-adapter", }, }); ``` -`FsRoutesConfig` fields: +`FsRoutesConfig` fields (all required — you must be explicit about each): -- **`dir`** (optional, default `"./src/pages"`) — directory scanned for route files, relative to the Vite root. +- **`dir`** (required) — directory scanned for route files, relative to the Vite root. Commonly `"./src/pages"`. - **`root`** (required) — path to the root (HTML shell) component module. -- **`adapter`** (optional) — path to a module that `export default`s an `FsRoutesAdapter`. Defaults to the built-in Next.js-like adapter (`nextRoutes()` from `@funstack/static/fs-routes`). +- **`adapter`** (required) — a module that `export default`s an `FsRoutesAdapter`, given as a bare module specifier or a path relative to the Vite root. Use the bundled `@funstack/static/fs-routes/next-adapter` for the built-in Next.js-like convention, or point it at your own module for a custom one. Enable [`ssr`](#ssr-optional) alongside `fsRoutes` — it is required for the dev server to render pages and recommended in general. diff --git a/packages/docs/src/pages/learn/FileSystemRouting.mdx b/packages/docs/src/pages/learn/FileSystemRouting.mdx index 4736151..eed6df6 100644 --- a/packages/docs/src/pages/learn/FileSystemRouting.mdx +++ b/packages/docs/src/pages/learn/FileSystemRouting.mdx @@ -31,6 +31,7 @@ export default defineConfig({ fsRoutes: { dir: "./src/pages", root: "./src/root.tsx", + adapter: "@funstack/static/fs-routes/next-adapter", }, }), react(), @@ -38,9 +39,11 @@ export default defineConfig({ }); ``` -- `dir` — the directory scanned for route files (default `./src/pages`). +All `fsRoutes` fields are required, so each choice is explicit in your config: + +- `dir` — the directory scanned for route files (commonly `./src/pages`). - `root` — the HTML shell component (`…{children}`). -- `adapter` — optional path to a custom adapter module (defaults to the built-in Next.js-like adapter). +- `adapter` — a module that `export default`s the adapter defining the directory / file-name convention. The bundled `@funstack/static/fs-routes/next-adapter` provides the built-in Next.js-like convention with default options; see [Custom Conventions](#custom-conventions-adapters) to configure it or write your own. `fsRoutes` is mutually exclusive with the `root` + `app` (single-entry) and `entries` (multiple entries) modes. @@ -108,7 +111,7 @@ A dynamic route without `generateStaticParams` is **not** pre-rendered (a warnin ## Custom Conventions (Adapters) -The convention is defined by an **adapter** implementing `FsRoutesAdapter`. Point `adapter` at a module that `export default`s an adapter to use a different convention: +The convention is defined by an **adapter** implementing `FsRoutesAdapter`. The built-in `@funstack/static/fs-routes/next-adapter` is the Next.js-like adapter with default options; to use a different convention, point `adapter` at your own module that `export default`s an adapter: ```typescript // vite.config.ts diff --git a/packages/example-fs-routing/vite.config.ts b/packages/example-fs-routing/vite.config.ts index 998bd97..fba687e 100644 --- a/packages/example-fs-routing/vite.config.ts +++ b/packages/example-fs-routing/vite.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ fsRoutes: { dir: "./src/pages", root: "./src/root.tsx", + adapter: "@funstack/static/fs-routes/next-adapter", }, }), react(), diff --git a/packages/static/e2e/fixture-fs-routing/vite.config.ts b/packages/static/e2e/fixture-fs-routing/vite.config.ts index 9c027db..400fef3 100644 --- a/packages/static/e2e/fixture-fs-routing/vite.config.ts +++ b/packages/static/e2e/fixture-fs-routing/vite.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ fsRoutes: { dir: "./src/pages", root: "./src/root.tsx", + adapter: "@funstack/static/fs-routes/next-adapter", }, }), react(), diff --git a/packages/static/package.json b/packages/static/package.json index 374f323..5da0f52 100644 --- a/packages/static/package.json +++ b/packages/static/package.json @@ -35,6 +35,10 @@ "types": "./dist/fs-routes/index.d.mts", "import": "./dist/fs-routes/index.mjs" }, + "./fs-routes/next-adapter": { + "types": "./dist/fs-routes/next-adapter.d.mts", + "import": "./dist/fs-routes/next-adapter.mjs" + }, "./entries/*": "./dist/entries/*.mjs" }, "imports": { diff --git a/packages/static/src/fs-routes/next-adapter.ts b/packages/static/src/fs-routes/next-adapter.ts new file mode 100644 index 0000000..ae58a7f --- /dev/null +++ b/packages/static/src/fs-routes/next-adapter.ts @@ -0,0 +1,31 @@ +/** + * Convenience adapter module for the built-in Next.js-like file-system routing + * convention, configured with default options. + * + * Point the `fsRoutes.adapter` plugin option at this module to use it without + * writing your own adapter file: + * + * ```ts + * funstackStatic({ + * fsRoutes: { + * dir: "./src/pages", + * root: "./src/root.tsx", + * adapter: "@funstack/static/fs-routes/next-adapter", + * }, + * }); + * ``` + * + * To customize the convention, call `nextRoutes(options)` from + * `@funstack/static/fs-routes` in your own adapter module instead. + * + * @experimental File-system routing is experimental and not yet subject to + * semantic versioning. + * + * @packageDocumentation + */ +import { nextRoutes } from "./nextAdapter"; +import type { FsRoutesAdapter } from "./types"; + +const adapter: FsRoutesAdapter = nextRoutes(); + +export default adapter; diff --git a/packages/static/src/plugin/index.ts b/packages/static/src/plugin/index.ts index 9e3fc7d..3e9bc55 100644 --- a/packages/static/src/plugin/index.ts +++ b/packages/static/src/plugin/index.ts @@ -96,9 +96,9 @@ export interface FsRoutesConfig { /** * Directory containing route files, relative to the Vite root. * - * @default "./src/pages" + * Commonly `"./src/pages"`. */ - dir?: string; + dir: string; /** * Path to the root (HTML shell) component module. * The file should `export default` a React component that renders the whole @@ -106,13 +106,16 @@ export interface FsRoutesConfig { */ root: string; /** - * Path to a module that `export default`s an `FsRoutesAdapter`, which defines - * the directory / file-name convention. + * Module that `export default`s an `FsRoutesAdapter`, which defines the + * directory / file-name convention. Either a bare module specifier (a package + * import) or a path to a local module, relative to the Vite root. * - * Defaults to the built-in Next.js-like adapter (`nextRoutes()` from - * `@funstack/static/fs-routes`). + * To use the built-in Next.js-like convention with default options, point + * this at the bundled module `@funstack/static/fs-routes/next-adapter`. + * For custom options, `export default nextRoutes(options)` (from + * `@funstack/static/fs-routes`) in your own module and point this at it. */ - adapter?: string; + adapter: string; } interface FsRoutesOptions { @@ -131,6 +134,15 @@ interface FsRoutesOptions { export type FunstackStaticOptions = FunstackStaticBaseOptions & (SingleEntryOptions | MultipleEntriesOptions | FsRoutesOptions); +/** + * Whether `id` is a bare module specifier (a package import) rather than a + * relative or absolute file path. Bare specifiers are passed through to the + * generated import as-is; paths are resolved against the Vite root. + */ +function isBareSpecifier(id: string): boolean { + return !id.startsWith(".") && !path.isAbsolute(id); +} + export default function funstackStatic( options: FunstackStaticOptions, ): (Plugin | Plugin[])[] { @@ -159,7 +171,7 @@ export default function funstackStatic( let resolvedBuildEntry: string | undefined; // Resolved configuration for file-system routing (fsRoutes mode). let resolvedFsRoutes: - | { root: string; adapter: string | undefined; globBase: string } + | { root: string; adapter: string; globBase: string } | undefined; // Determine which entry mode the user selected. @@ -198,20 +210,24 @@ export default function funstackStatic( configResolved(config) { if (isFsRoutes) { const fsRoutes = options.fsRoutes; - const dir = fsRoutes.dir ?? "./src/pages"; const resolvedRoot = normalizePath( path.resolve(config.root, fsRoutes.root), ); - const resolvedDir = normalizePath(path.resolve(config.root, dir)); + const resolvedDir = normalizePath( + path.resolve(config.root, fsRoutes.dir), + ); // Glob patterns in generated virtual modules must be root-relative // (a virtual module has no real path to resolve "./" against). const relativeDir = normalizePath( path.relative(config.root, resolvedDir), ); const globBase = `/${relativeDir.replace(/^\.?\/?/, "").replace(/\/$/, "")}`; - const adapter = fsRoutes.adapter - ? normalizePath(path.resolve(config.root, fsRoutes.adapter)) - : undefined; + // The adapter may be a bare module specifier (e.g. the built-in + // `@funstack/static/fs-routes/next-adapter`) or a path to a local module. + // Resolve only the latter against the Vite root. + const adapter = isBareSpecifier(fsRoutes.adapter) + ? fsRoutes.adapter + : normalizePath(path.resolve(config.root, fsRoutes.adapter)); resolvedFsRoutes = { root: resolvedRoot, adapter, globBase }; } else if (isMultiEntry) { resolvedEntriesModule = normalizePath( @@ -294,19 +310,15 @@ export default function funstackStatic( const globPattern = `${globBase}/**/*.{tsx,jsx}`; const lines = [ `import Root from "${root}";`, + `import adapter from "${adapter}";`, `import { createFsRoutesEntries } from "@funstack/static/fs-routes";`, - ]; - if (adapter) { - lines.push(`import adapter from "${adapter}";`); - } - lines.push( `const modules = import.meta.glob(${JSON.stringify(globPattern)}, { eager: true });`, `export default createFsRoutesEntries({`, ` modules,`, ` root: Root,`, - ...(adapter ? [` adapter,`] : []), + ` adapter,`, `});`, - ); + ]; return lines.join("\n"); } if (isMultiEntry) { diff --git a/packages/static/tsdown.config.ts b/packages/static/tsdown.config.ts index 7a6a140..6e3b80c 100644 --- a/packages/static/tsdown.config.ts +++ b/packages/static/tsdown.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ "src/entries/*.ts", "src/bin/*.ts", "src/fs-routes/index.ts", + "src/fs-routes/next-adapter.ts", ], // Vite virtual modules & subpath imports external: [/^virtual:/, /^#/],