Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/example-fs-routing/package.json
Original file line number Diff line number Diff line change
@@ -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:"
}
}
6 changes: 6 additions & 0 deletions packages/example-fs-routing/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Router } from "@funstack/router";
import { routes } from "./routes";

export default function App({ ssrPath }: { ssrPath: string }) {
return <Router routes={routes} fallback="static" ssr={{ path: ssrPath }} />;
}
29 changes: 29 additions & 0 deletions packages/example-fs-routing/src/entries.tsx
Original file line number Diff line number Diff line change
@@ -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: <App ssrPath={pathname} />,
}));
}
55 changes: 55 additions & 0 deletions packages/example-fs-routing/src/index.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
16 changes: 16 additions & 0 deletions packages/example-fs-routing/src/pages/about.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default function About() {
return (
<div>
<h1>About</h1>
<p>
This example demonstrates file-system routing with{" "}
<a href="https://github.com/uhyo/funstack-static">FUNSTACK Static</a>.
</p>
<p>
Routes are derived from the file structure under <code>src/pages/</code>{" "}
using Vite&apos;s <code>import.meta.glob</code>, which also enables hot
module replacement during development.
</p>
</div>
);
}
15 changes: 15 additions & 0 deletions packages/example-fs-routing/src/pages/blog/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default function Blog() {
return (
<div>
<h1>Blog</h1>
<p>
This page is at <code>pages/blog/index.tsx</code>, which maps to the{" "}
<code>/blog</code> route.
</p>
<p>
Nested directories create nested URL paths. An <code>index.tsx</code>{" "}
file in a directory maps to the directory&apos;s path.
</p>
</div>
);
}
28 changes: 28 additions & 0 deletions packages/example-fs-routing/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export default function Home() {
return (
<div>
<h1>Home</h1>
<p>
Welcome to the file-system routing example! Pages in{" "}
<code>src/pages/</code> are automatically mapped to routes using{" "}
<code>import.meta.glob</code>.
</p>
<h2>How it works</h2>
<ul>
<li>
<code>pages/index.tsx</code> → <code>/</code>
</li>
<li>
<code>pages/about.tsx</code> → <code>/about</code>
</li>
<li>
<code>pages/blog/index.tsx</code> → <code>/blog</code>
</li>
</ul>
<p>
Add a new <code>.tsx</code> file in the <code>pages/</code> directory
and it will be automatically discovered as a new route.
</p>
</div>
);
}
21 changes: 21 additions & 0 deletions packages/example-fs-routing/src/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type React from "react";
import "./index.css";

export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FUNSTACK Static - File-System Routing</title>
</head>
<body>
<nav>
<a href="/">Home</a> | <a href="/about">About</a> |{" "}
<a href="/blog">Blog</a>
</nav>
<main>{children}</main>
</body>
</html>
);
}
24 changes: 24 additions & 0 deletions packages/example-fs-routing/src/routes.tsx
Original file line number Diff line number Diff line change
@@ -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: <Page />,
});
},
);
18 changes: 18 additions & 0 deletions packages/example-fs-routing/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
12 changes: 12 additions & 0 deletions packages/example-fs-routing/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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(),
],
});
31 changes: 31 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.