Skip to content

Commit 19e6a8e

Browse files
uhyoclaude
andauthored
feat(example): add file-system routing example (#85)
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) <noreply@anthropic.com>
1 parent a395cf5 commit 19e6a8e

12 files changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "funstack-static-example-fs-routing",
3+
"version": "0.0.0",
4+
"private": true,
5+
"license": "MIT",
6+
"type": "module",
7+
"scripts": {
8+
"dev": "vite",
9+
"build": "vite build",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"@funstack/router": "^1.1.0",
14+
"@funstack/static": "workspace:*",
15+
"@types/node": "catalog:",
16+
"react": "catalog:",
17+
"react-dom": "catalog:"
18+
},
19+
"devDependencies": {
20+
"@types/react": "^19.2.14",
21+
"@types/react-dom": "^19.2.3",
22+
"@vitejs/plugin-react": "catalog:",
23+
"vite": "catalog:"
24+
}
25+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Router } from "@funstack/router";
2+
import { routes } from "./routes";
3+
4+
export default function App({ ssrPath }: { ssrPath: string }) {
5+
return <Router routes={routes} fallback="static" ssr={{ path: ssrPath }} />;
6+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { EntryDefinition } from "@funstack/static/entries";
2+
import type { RouteDefinition } from "@funstack/router/server";
3+
import App from "./App";
4+
import { routes } from "./routes";
5+
6+
function collectPaths(routes: RouteDefinition[]): string[] {
7+
const paths: string[] = [];
8+
for (const route of routes) {
9+
if (route.children) {
10+
paths.push(...collectPaths(route.children));
11+
} else if (route.path !== undefined && route.path !== "*") {
12+
paths.push(route.path);
13+
}
14+
}
15+
return paths;
16+
}
17+
18+
function pathToEntryPath(path: string): string {
19+
if (path === "/") return "index.html";
20+
return `${path.slice(1)}.html`;
21+
}
22+
23+
export default function getEntries(): EntryDefinition[] {
24+
return collectPaths(routes).map((pathname) => ({
25+
path: pathToEntryPath(pathname),
26+
root: () => import("./root"),
27+
app: <App ssrPath={pathname} />,
28+
}));
29+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
:root {
2+
font-family:
3+
system-ui,
4+
-apple-system,
5+
sans-serif;
6+
line-height: 1.6;
7+
color: #213547;
8+
background-color: #ffffff;
9+
}
10+
11+
@media (prefers-color-scheme: dark) {
12+
:root {
13+
color: #ffffffde;
14+
background-color: #242424;
15+
}
16+
17+
a {
18+
color: #6db3f2;
19+
}
20+
}
21+
22+
body {
23+
max-width: 720px;
24+
margin: 0 auto;
25+
padding: 2rem;
26+
}
27+
28+
nav {
29+
padding-bottom: 1rem;
30+
margin-bottom: 2rem;
31+
border-bottom: 1px solid #ddd;
32+
}
33+
34+
@media (prefers-color-scheme: dark) {
35+
nav {
36+
border-bottom-color: #444;
37+
}
38+
}
39+
40+
nav a {
41+
margin: 0 0.25rem;
42+
}
43+
44+
code {
45+
background: #f4f4f4;
46+
padding: 0.15em 0.3em;
47+
border-radius: 3px;
48+
font-size: 0.9em;
49+
}
50+
51+
@media (prefers-color-scheme: dark) {
52+
code {
53+
background: #333;
54+
}
55+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export default function About() {
2+
return (
3+
<div>
4+
<h1>About</h1>
5+
<p>
6+
This example demonstrates file-system routing with{" "}
7+
<a href="https://github.com/uhyo/funstack-static">FUNSTACK Static</a>.
8+
</p>
9+
<p>
10+
Routes are derived from the file structure under <code>src/pages/</code>{" "}
11+
using Vite&apos;s <code>import.meta.glob</code>, which also enables hot
12+
module replacement during development.
13+
</p>
14+
</div>
15+
);
16+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default function Blog() {
2+
return (
3+
<div>
4+
<h1>Blog</h1>
5+
<p>
6+
This page is at <code>pages/blog/index.tsx</code>, which maps to the{" "}
7+
<code>/blog</code> route.
8+
</p>
9+
<p>
10+
Nested directories create nested URL paths. An <code>index.tsx</code>{" "}
11+
file in a directory maps to the directory&apos;s path.
12+
</p>
13+
</div>
14+
);
15+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export default function Home() {
2+
return (
3+
<div>
4+
<h1>Home</h1>
5+
<p>
6+
Welcome to the file-system routing example! Pages in{" "}
7+
<code>src/pages/</code> are automatically mapped to routes using{" "}
8+
<code>import.meta.glob</code>.
9+
</p>
10+
<h2>How it works</h2>
11+
<ul>
12+
<li>
13+
<code>pages/index.tsx</code><code>/</code>
14+
</li>
15+
<li>
16+
<code>pages/about.tsx</code><code>/about</code>
17+
</li>
18+
<li>
19+
<code>pages/blog/index.tsx</code><code>/blog</code>
20+
</li>
21+
</ul>
22+
<p>
23+
Add a new <code>.tsx</code> file in the <code>pages/</code> directory
24+
and it will be automatically discovered as a new route.
25+
</p>
26+
</div>
27+
);
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type React from "react";
2+
import "./index.css";
3+
4+
export default function Root({ children }: { children: React.ReactNode }) {
5+
return (
6+
<html lang="en">
7+
<head>
8+
<meta charSet="UTF-8" />
9+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
10+
<title>FUNSTACK Static - File-System Routing</title>
11+
</head>
12+
<body>
13+
<nav>
14+
<a href="/">Home</a> | <a href="/about">About</a> |{" "}
15+
<a href="/blog">Blog</a>
16+
</nav>
17+
<main>{children}</main>
18+
</body>
19+
</html>
20+
);
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { route, type RouteDefinition } from "@funstack/router/server";
2+
3+
const pageModules = import.meta.glob<{ default: React.ComponentType }>(
4+
"./pages/**/*.tsx",
5+
{ eager: true },
6+
);
7+
8+
function filePathToUrlPath(filePath: string): string {
9+
let urlPath = filePath.replace(/^\.\/pages/, "").replace(/\.tsx$/, "");
10+
if (urlPath.endsWith("/index")) {
11+
urlPath = urlPath.slice(0, -"/index".length);
12+
}
13+
return urlPath || "/";
14+
}
15+
16+
export const routes: RouteDefinition[] = Object.entries(pageModules).map(
17+
([filePath, module]) => {
18+
const Page = module.default;
19+
return route({
20+
path: filePathToUrlPath(filePath),
21+
component: <Page />,
22+
});
23+
},
24+
);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"erasableSyntaxOnly": true,
4+
"allowImportingTsExtensions": true,
5+
"strict": true,
6+
"noUnusedLocals": true,
7+
"noUnusedParameters": true,
8+
"skipLibCheck": true,
9+
"verbatimModuleSyntax": true,
10+
"noEmit": true,
11+
"moduleResolution": "Bundler",
12+
"module": "ESNext",
13+
"target": "ESNext",
14+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
15+
"types": ["vite/client"],
16+
"jsx": "react-jsx"
17+
}
18+
}

0 commit comments

Comments
 (0)