From 16f693e48adb04ef8a0998607a5083b8777dac20 Mon Sep 17 00:00:00 2001 From: uhyo Date: Sun, 22 Mar 2026 11:05:28 +0900 Subject: [PATCH] feat(example): add file-system routing example Add a new example package demonstrating userland file-system routing with import.meta.glob and @funstack/router. Pages in src/pages/ are automatically discovered and mapped to URL routes. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/example-fs-routing/package.json | 25 +++++++++ packages/example-fs-routing/src/App.tsx | 6 ++ packages/example-fs-routing/src/entries.tsx | 29 ++++++++++ packages/example-fs-routing/src/index.css | 55 +++++++++++++++++++ .../example-fs-routing/src/pages/about.tsx | 16 ++++++ .../src/pages/blog/index.tsx | 15 +++++ .../example-fs-routing/src/pages/index.tsx | 28 ++++++++++ packages/example-fs-routing/src/root.tsx | 21 +++++++ packages/example-fs-routing/src/routes.tsx | 24 ++++++++ packages/example-fs-routing/tsconfig.json | 18 ++++++ packages/example-fs-routing/vite.config.ts | 12 ++++ pnpm-lock.yaml | 31 +++++++++++ 12 files changed, 280 insertions(+) create mode 100644 packages/example-fs-routing/package.json create mode 100644 packages/example-fs-routing/src/App.tsx create mode 100644 packages/example-fs-routing/src/entries.tsx create mode 100644 packages/example-fs-routing/src/index.css create mode 100644 packages/example-fs-routing/src/pages/about.tsx create mode 100644 packages/example-fs-routing/src/pages/blog/index.tsx create mode 100644 packages/example-fs-routing/src/pages/index.tsx create mode 100644 packages/example-fs-routing/src/root.tsx create mode 100644 packages/example-fs-routing/src/routes.tsx create mode 100644 packages/example-fs-routing/tsconfig.json create mode 100644 packages/example-fs-routing/vite.config.ts diff --git a/packages/example-fs-routing/package.json b/packages/example-fs-routing/package.json new file mode 100644 index 0000000..02f26b8 --- /dev/null +++ b/packages/example-fs-routing/package.json @@ -0,0 +1,25 @@ +{ + "name": "funstack-static-example-fs-routing", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@funstack/router": "^1.1.0", + "@funstack/static": "workspace:*", + "@types/node": "catalog:", + "react": "catalog:", + "react-dom": "catalog:" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "catalog:", + "vite": "catalog:" + } +} diff --git a/packages/example-fs-routing/src/App.tsx b/packages/example-fs-routing/src/App.tsx new file mode 100644 index 0000000..e904c34 --- /dev/null +++ b/packages/example-fs-routing/src/App.tsx @@ -0,0 +1,6 @@ +import { Router } from "@funstack/router"; +import { routes } from "./routes"; + +export default function App({ ssrPath }: { ssrPath: string }) { + return ; +} diff --git a/packages/example-fs-routing/src/entries.tsx b/packages/example-fs-routing/src/entries.tsx new file mode 100644 index 0000000..0a54ad3 --- /dev/null +++ b/packages/example-fs-routing/src/entries.tsx @@ -0,0 +1,29 @@ +import type { EntryDefinition } from "@funstack/static/entries"; +import type { RouteDefinition } from "@funstack/router/server"; +import App from "./App"; +import { routes } from "./routes"; + +function collectPaths(routes: RouteDefinition[]): string[] { + const paths: string[] = []; + for (const route of routes) { + if (route.children) { + paths.push(...collectPaths(route.children)); + } else if (route.path !== undefined && route.path !== "*") { + paths.push(route.path); + } + } + return paths; +} + +function pathToEntryPath(path: string): string { + if (path === "/") return "index.html"; + return `${path.slice(1)}.html`; +} + +export default function getEntries(): EntryDefinition[] { + return collectPaths(routes).map((pathname) => ({ + path: pathToEntryPath(pathname), + root: () => import("./root"), + app: , + })); +} diff --git a/packages/example-fs-routing/src/index.css b/packages/example-fs-routing/src/index.css new file mode 100644 index 0000000..c49d528 --- /dev/null +++ b/packages/example-fs-routing/src/index.css @@ -0,0 +1,55 @@ +:root { + font-family: + system-ui, + -apple-system, + sans-serif; + line-height: 1.6; + color: #213547; + background-color: #ffffff; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #ffffffde; + background-color: #242424; + } + + a { + color: #6db3f2; + } +} + +body { + max-width: 720px; + margin: 0 auto; + padding: 2rem; +} + +nav { + padding-bottom: 1rem; + margin-bottom: 2rem; + border-bottom: 1px solid #ddd; +} + +@media (prefers-color-scheme: dark) { + nav { + border-bottom-color: #444; + } +} + +nav a { + margin: 0 0.25rem; +} + +code { + background: #f4f4f4; + padding: 0.15em 0.3em; + border-radius: 3px; + font-size: 0.9em; +} + +@media (prefers-color-scheme: dark) { + code { + background: #333; + } +} diff --git a/packages/example-fs-routing/src/pages/about.tsx b/packages/example-fs-routing/src/pages/about.tsx new file mode 100644 index 0000000..055b02b --- /dev/null +++ b/packages/example-fs-routing/src/pages/about.tsx @@ -0,0 +1,16 @@ +export default function About() { + return ( +
+

About

+

+ This example demonstrates file-system routing with{" "} + FUNSTACK Static. +

+

+ Routes are derived from the file structure under src/pages/{" "} + using Vite's import.meta.glob, which also enables hot + module replacement during development. +

+
+ ); +} diff --git a/packages/example-fs-routing/src/pages/blog/index.tsx b/packages/example-fs-routing/src/pages/blog/index.tsx new file mode 100644 index 0000000..03f048a --- /dev/null +++ b/packages/example-fs-routing/src/pages/blog/index.tsx @@ -0,0 +1,15 @@ +export default function Blog() { + return ( +
+

Blog

+

+ This page is at pages/blog/index.tsx, which maps to the{" "} + /blog route. +

+

+ Nested directories create nested URL paths. An index.tsx{" "} + file in a directory maps to the directory's path. +

+
+ ); +} diff --git a/packages/example-fs-routing/src/pages/index.tsx b/packages/example-fs-routing/src/pages/index.tsx new file mode 100644 index 0000000..3558f66 --- /dev/null +++ b/packages/example-fs-routing/src/pages/index.tsx @@ -0,0 +1,28 @@ +export default function Home() { + return ( +
+

Home

+

+ Welcome to the file-system routing example! Pages in{" "} + src/pages/ are automatically mapped to routes using{" "} + import.meta.glob. +

+

How it works

+
    +
  • + pages/index.tsx/ +
  • +
  • + pages/about.tsx/about +
  • +
  • + pages/blog/index.tsx/blog +
  • +
+

+ Add a new .tsx file in the pages/ directory + and it will be automatically discovered as a new route. +

+
+ ); +} diff --git a/packages/example-fs-routing/src/root.tsx b/packages/example-fs-routing/src/root.tsx new file mode 100644 index 0000000..90a5a40 --- /dev/null +++ b/packages/example-fs-routing/src/root.tsx @@ -0,0 +1,21 @@ +import type React from "react"; +import "./index.css"; + +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + FUNSTACK Static - File-System Routing + + + +
{children}
+ + + ); +} diff --git a/packages/example-fs-routing/src/routes.tsx b/packages/example-fs-routing/src/routes.tsx new file mode 100644 index 0000000..ff25660 --- /dev/null +++ b/packages/example-fs-routing/src/routes.tsx @@ -0,0 +1,24 @@ +import { route, type RouteDefinition } from "@funstack/router/server"; + +const pageModules = import.meta.glob<{ default: React.ComponentType }>( + "./pages/**/*.tsx", + { eager: true }, +); + +function filePathToUrlPath(filePath: string): string { + let urlPath = filePath.replace(/^\.\/pages/, "").replace(/\.tsx$/, ""); + if (urlPath.endsWith("/index")) { + urlPath = urlPath.slice(0, -"/index".length); + } + return urlPath || "/"; +} + +export const routes: RouteDefinition[] = Object.entries(pageModules).map( + ([filePath, module]) => { + const Page = module.default; + return route({ + path: filePathToUrlPath(filePath), + component: , + }); + }, +); diff --git a/packages/example-fs-routing/tsconfig.json b/packages/example-fs-routing/tsconfig.json new file mode 100644 index 0000000..bb5d780 --- /dev/null +++ b/packages/example-fs-routing/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "erasableSyntaxOnly": true, + "allowImportingTsExtensions": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "jsx": "react-jsx" + } +} diff --git a/packages/example-fs-routing/vite.config.ts b/packages/example-fs-routing/vite.config.ts new file mode 100644 index 0000000..e192f96 --- /dev/null +++ b/packages/example-fs-routing/vite.config.ts @@ -0,0 +1,12 @@ +import funstackStatic from "@funstack/static"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + funstackStatic({ + entries: "./src/entries.tsx", + }), + react(), + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1eb9c08..cfbbf30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,37 @@ importers: specifier: 'catalog:' version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(terser@5.46.1)(tsx@4.21.0) + packages/example-fs-routing: + dependencies: + '@funstack/router': + specifier: ^1.1.0 + version: 1.1.0(react@19.2.4) + '@funstack/static': + specifier: workspace:* + version: link:../static + '@types/node': + specifier: 'catalog:' + version: 25.5.0 + react: + specifier: 'catalog:' + version: 19.2.4 + react-dom: + specifier: 'catalog:' + version: 19.2.4(react@19.2.4) + devDependencies: + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: 'catalog:' + version: 6.0.1(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(terser@5.46.1)(tsx@4.21.0)) + vite: + specifier: 'catalog:' + version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(terser@5.46.1)(tsx@4.21.0) + packages/static: dependencies: '@funstack/skill-installer':