Add Tanstack Start to bun init (#24648)

Co-authored-by: Alistair Smith <hi@alistair.sh>
This commit is contained in:
Lydia Hallie
2025-12-01 23:05:47 -06:00
committed by GitHub
parent fe0aba79f4
commit 830fd9b0ae
15 changed files with 758 additions and 11 deletions

View File

@@ -0,0 +1,35 @@
# {[name]s}
A TanStack Start project powered by Bun.
To install dependencies:
```bash
bun install
```
To start a development server:
```bash
bun dev
```
To build for production:
```bash
bun run build
```
## About TanStack Start
[TanStack Start](https://tanstack.com/start/latest) is a full-stack framework powered by TanStack Router for React and Solid that provides:
- File-based routing
- Server-side rendering (SSR)
- Server functions with `createServerFn`
- Built-in data loading with route loaders
- Hot module replacement (HMR)
This project was created using `bun init --react=tanstack` in bun v{[bunVersion]s}. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
For more information, check out Bun's [TanStack Start guide](https://bun.com/guides/ecosystem/tanstack-start).

View File

@@ -228,6 +228,7 @@ pub const InitCommand = struct {
const @"tsconfig.json" = @embedFile("init/tsconfig.default.json");
const @"README.md" = @embedFile("init/README.default.md");
const @"README2.md" = @embedFile("init/README2.default.md");
const @"README-tanstack.md" = @embedFile("init/README-tanstack.default.md");
/// Create a new asset file, overriding anything that already exists. Known
/// assets will have their contents pre-populated; otherwise the file will be empty.
@@ -382,6 +383,10 @@ pub const InitCommand = struct {
template = .react_tailwind_shadcn;
prev_flag_was_react = false;
auto_yes = true;
} else if ((template == .react_blank and prev_flag_was_react and strings.eqlComptime(arg, "tanstack") or strings.eqlComptime(arg, "--react=tanstack")) or strings.eqlComptime(arg, "r=tanstack")) {
template = .react_tanstack;
prev_flag_was_react = false;
auto_yes = true;
} else {
prev_flag_was_react = false;
}
@@ -585,12 +590,14 @@ pub const InitCommand = struct {
default,
tailwind,
shadcn_tailwind,
tanstack,
pub fn fmt(self: @This()) []const u8 {
return switch (self) {
.default => "<blue>Default (blank)<r>",
.tailwind => "<magenta>TailwindCSS<r>",
.shadcn_tailwind => "<green>Shadcn + TailwindCSS<r>",
.tanstack => "<yellow>TanStack Start<r>",
};
}
});
@@ -599,6 +606,7 @@ pub const InitCommand = struct {
.default => .react_blank,
.tailwind => .react_tailwind,
.shadcn_tailwind => .react_tailwind_shadcn,
.tanstack => .react_tanstack,
};
},
.blank => template = .blank,
@@ -613,7 +621,7 @@ pub const InitCommand = struct {
}
switch (template) {
inline .react_blank, .react_tailwind, .react_tailwind_shadcn => |t| {
inline .react_blank, .react_tailwind, .react_tailwind_shadcn, .react_tanstack => |t| {
try t.@"write files and run `bun dev`"(alloc);
return;
},
@@ -791,7 +799,7 @@ pub const InitCommand = struct {
switch (template) {
.blank, .typescript_library => {
Template.createAgentRule();
Template.createAgentRule(template);
if (package_json_file != null and !did_load_package_json) {
Output.prettyln(" + <r><d>package.json<r>", .{});
@@ -910,6 +918,24 @@ const DependencyGroup = struct {
} ++ tailwind.dependencies[0..tailwind.dependencies.len].*,
.devDependencies = &[_]DependencyNeeded{} ++ tailwind.devDependencies[0..tailwind.devDependencies.len].*,
};
pub const tanstack = DependencyGroup{
.dependencies = &[_]DependencyNeeded{
.{ .name = "@tailwindcss/vite", .version = "^4.1.17" },
.{ .name = "@tanstack/react-router", .version = "^1.135.2" },
.{ .name = "@tanstack/react-start", .version = "^1.135.2" },
.{ .name = "react", .version = "^19.2.0" },
.{ .name = "react-dom", .version = "^19.2.0" },
.{ .name = "tailwindcss", .version = "^4.1.17" },
},
.devDependencies = &[_]DependencyNeeded{
.{ .name = "@types/react", .version = "^19.2.3" },
.{ .name = "@types/react-dom", .version = "^19.2.3" },
.{ .name = "@vitejs/plugin-react", .version = "^5.1.0" },
.{ .name = "vite", .version = "^7.2.2" },
.{ .name = "vite-tsconfig-paths", .version = "^5.1.4" },
} ++ blank.devDependencies[0..1].*,
};
};
const Template = enum {
@@ -917,6 +943,7 @@ const Template = enum {
react_blank,
react_tailwind,
react_tailwind_shadcn,
react_tanstack,
typescript_library,
const TemplateFile = struct {
path: [:0]const u8,
@@ -931,7 +958,7 @@ const Template = enum {
}
pub fn isReact(this: Template) bool {
return switch (this) {
.react_blank, .react_tailwind, .react_tailwind_shadcn => true,
.react_blank, .react_tailwind, .react_tailwind_shadcn, .react_tanstack => true,
else => false,
};
}
@@ -959,6 +986,7 @@ const Template = enum {
.react_blank => DependencyGroup.react,
.react_tailwind => DependencyGroup.tailwind,
.react_tailwind_shadcn => DependencyGroup.shadcn,
.react_tanstack => DependencyGroup.tanstack,
.typescript_library => DependencyGroup.blank,
};
}
@@ -969,6 +997,7 @@ const Template = enum {
.react_blank => "bun-react-template",
.react_tailwind => "bun-react-tailwind-template",
.react_tailwind_shadcn => "bun-react-tailwind-shadcn-template",
.react_tanstack => "bun-tanstack-start-template",
};
}
pub fn scripts(this: Template) []const []const u8 {
@@ -986,13 +1015,16 @@ const Template = enum {
"build",
"NODE_ENV=production bun .",
},
.react_tanstack => &.{ "dev", "bun --bun vite dev", "build", "bun --bun vite build", "serve", "bun --bun vite preview" },
};
return s;
}
const agent_rule = @embedFile("../init/rule.md");
const agent_rule_tanstack = @embedFile("../init/rule-tanstack.md");
const cursor_rule = TemplateFile{ .path = ".cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc", .contents = agent_rule };
const cursor_rule_tanstack = TemplateFile{ .path = ".cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc", .contents = agent_rule_tanstack };
const cursor_rule_path_to_claude_md = "../../CLAUDE.md";
fn isClaudeCodeInstalled() bool {
@@ -1012,12 +1044,12 @@ const Template = enum {
return bun.which(pathbuffer, bun.env_var.PATH.get() orelse return false, bun.fs.FileSystem.instance.top_level_dir, "claude") != null;
}
pub fn createAgentRule() void {
pub fn createAgentRule(this: Template) void {
var @"create CLAUDE.md" = Template.isClaudeCodeInstalled() and
// Never overwrite CLAUDE.md
!bun.sys.exists("CLAUDE.md");
if (Template.getCursorRule()) |template_file| {
if (this.getCursorRule()) |template_file| {
var did_create_agent_rule = false;
// If both Cursor & Claude is installed, make the cursor rule a
@@ -1053,9 +1085,13 @@ const Template = enum {
// If cursor is not installed but claude code is installed, then create the CLAUDE.md.
if (@"create CLAUDE.md") {
// In this case, the frontmatter from the cursor rule is not helpful so let's trim it out.
const end_of_frontmatter = if (bun.strings.lastIndexOf(agent_rule, "---\n")) |start| start + "---\n".len else 0;
const rule_to_use = switch (this) {
.react_tanstack => agent_rule_tanstack,
else => agent_rule,
};
const end_of_frontmatter = if (bun.strings.lastIndexOf(rule_to_use, "---\n")) |start| start + "---\n".len else 0;
InitCommand.Assets.createNew("CLAUDE.md", agent_rule[end_of_frontmatter..]) catch {};
InitCommand.Assets.createNew("CLAUDE.md", rule_to_use[end_of_frontmatter..]) catch {};
}
}
@@ -1092,9 +1128,12 @@ const Template = enum {
return false;
}
fn getCursorRule() ?*const TemplateFile {
fn getCursorRule(this: Template) ?*const TemplateFile {
if (isCursorInstalled()) {
return &cursor_rule;
return switch (this) {
.react_tanstack => &cursor_rule_tanstack,
else => &cursor_rule,
};
}
return null;
@@ -1168,17 +1207,36 @@ const Template = enum {
};
};
const ReactTanstack = struct {
const files: []const TemplateFile = &.{
.{ .path = "package.json", .contents = @embedFile("../init/react-tanstack/package.json") },
.{ .path = "tsconfig.json", .contents = @embedFile("../init/react-tanstack/tsconfig.json") },
.{ .path = "vite.config.ts", .contents = @embedFile("../init/react-tanstack/vite.config.ts") },
.{ .path = "styles.css", .contents = @embedFile("../init/react-tanstack/styles.css") },
.{ .path = "README.md", .contents = InitCommand.Assets.@"README-tanstack.md" },
.{ .path = ".gitignore", .contents = InitCommand.Assets.@".gitignore", .can_skip_if_exists = true },
.{ .path = "src/router.tsx", .contents = @embedFile("../init/react-tanstack/src/router.tsx") },
.{ .path = "src/routes/__root.tsx", .contents = @embedFile("../init/react-tanstack/src/routes/__root.tsx") },
.{ .path = "src/routes/index.tsx", .contents = @embedFile("../init/react-tanstack/src/routes/index.tsx") },
.{ .path = "src/routes/stats.tsx", .contents = @embedFile("../init/react-tanstack/src/routes/stats.tsx") },
.{ .path = "src/routeTree.gen.ts", .contents = @embedFile("../init/react-tanstack/src/routeTree.gen.ts") },
.{ .path = "public/header.webp", .contents = @embedFile("../init/react-tanstack/public/header.webp") },
.{ .path = "public/favicon.ico", .contents = @embedFile("../init/react-tanstack/public/favicon.ico") },
};
};
pub fn files(this: Template) []const TemplateFile {
return switch (this) {
.react_blank => ReactBlank.files,
.react_tailwind => ReactTailwind.files,
.react_tailwind_shadcn => ReactShadcn.files,
.react_tanstack => ReactTanstack.files,
else => &.{.{ &.{}, &.{} }},
};
}
pub fn @"write files and run `bun dev`"(comptime this: Template, allocator: std.mem.Allocator) !void {
Template.createAgentRule();
this.createAgentRule();
inline for (comptime this.files()) |file| {
const path = file.path;
@@ -1218,10 +1276,22 @@ const Template = enum {
_ = try install.spawnAndWait();
var cwd_buf: bun.PathBuffer = undefined;
const cwd_path = switch (bun.sys.getcwd(&cwd_buf)) {
.result => |p| p,
.err => |e| {
Output.err(e, "failed to get current working directory", .{});
Global.exit(1);
},
};
const dir_name = std.fs.path.basename(cwd_path);
Output.prettyln(
\\
\\✨ New project configured!
\\
\\<d>cd {s}<r>
\\
\\<b><cyan>Development<r><d> - full-stack dev server with hot reload<r>
\\
\\ <cyan><b>bun dev<r>
@@ -1236,7 +1306,7 @@ const Template = enum {
\\
\\<blue>Happy bunning! 🐇<r>
\\
, .{});
, .{dir_name});
Output.flush();
}

View File

@@ -0,0 +1,28 @@
{
"name": "bun-tanstack-start-template",
"type": "module",
"scripts": {
"dev": "bun --bun vite dev",
"build": "bun --bun vite build",
"start": "bun --bun vite preview"
},
"devDependencies": {
"@types/bun": "^1.3.2",
"@types/react": "^19.2.3",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.0",
"vite": "^7.2.2",
"vite-tsconfig-paths": "^5.1.4"
},
"peerDependencies": {
"typescript": "^5.9.3"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.17",
"@tanstack/react-router": "^1.135.2",
"@tanstack/react-start": "^1.135.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.17"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -0,0 +1,84 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from "./routes/__root";
import { Route as IndexRouteImport } from "./routes/index";
import { Route as StatsRouteImport } from "./routes/stats";
const StatsRoute = StatsRouteImport.update({
id: "/stats",
path: "/stats",
getParentRoute: () => rootRouteImport,
} as any);
const IndexRoute = IndexRouteImport.update({
id: "/",
path: "/",
getParentRoute: () => rootRouteImport,
} as any);
export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
"/stats": typeof StatsRoute;
}
export interface FileRoutesByTo {
"/": typeof IndexRoute;
"/stats": typeof StatsRoute;
}
export interface FileRoutesById {
__root__: typeof rootRouteImport;
"/": typeof IndexRoute;
"/stats": typeof StatsRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: "/" | "/stats";
fileRoutesByTo: FileRoutesByTo;
to: "/" | "/stats";
id: "__root__" | "/" | "/stats";
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
StatsRoute: typeof StatsRoute;
}
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
"/stats": {
id: "/stats";
path: "/stats";
fullPath: "/stats";
preLoaderRoute: typeof StatsRouteImport;
parentRoute: typeof rootRouteImport;
};
"/": {
id: "/";
path: "/";
fullPath: "/";
preLoaderRoute: typeof IndexRouteImport;
parentRoute: typeof rootRouteImport;
};
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
StatsRoute: StatsRoute,
};
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>();
import type { createStart } from "@tanstack/react-start";
import type { getRouter } from "./router.tsx";
declare module "@tanstack/react-start" {
interface Register {
ssr: true;
router: Awaited<ReturnType<typeof getRouter>>;
}
}

View File

@@ -0,0 +1,11 @@
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export function getRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true,
});
return router;
}

View File

@@ -0,0 +1,87 @@
// src/routes/__root.tsx
/// <reference types="vite/client" />
import { createRootRoute, HeadContent, Link, Outlet, Scripts } from "@tanstack/react-router";
import type { ReactNode } from "react";
import appCss from "../../styles.css?url";
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: "utf-8",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "Bun + TanStack Start Starter",
},
],
links: [
{ rel: "stylesheet", href: appCss },
{ rel: "icon", href: "/favicon.ico" },
],
}),
component: RootComponent,
notFoundComponent: NotFoundComponent,
});
function NotFoundComponent() {
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4 antialiased">
<div className="w-full max-w-md">
<div className="relative bg-card/80 backdrop-blur-xl text-card-foreground rounded-2xl border border-border/50 shadow-2xl overflow-hidden h-[400px] max-h-4/5 grid grid-rows-[auto_1fr_auto]">
<div className="px-8 py-6">
<div className="space-y-2 text-center py-2">
<h1 className="text-3xl font-semibold tracking-tight text-foreground">404</h1>
<p className="text-lg text-muted-foreground font-medium -mt-2">Page Not Found</p>
</div>
</div>
<div className="px-8 overflow-y-auto">
<div className="flex flex-col items-center justify-center py-6 min-h-full">
<p className="text-sm text-muted-foreground text-center">
The page you're looking for doesn't exist or has been moved.
</p>
</div>
</div>
<div className="px-8 pb-10">
<div className="pt-6 border-t border-border/30">
<Link
to="/"
className="block w-full px-4 py-2 bg-foreground text-background rounded-lg font-medium hover:opacity-90 transition-opacity text-center text-sm"
>
Back to Home
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
);
}
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
);
}

View File

@@ -0,0 +1,118 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
const getBunInfo = createServerFn({
method: "GET",
}).handler(async () => {
return {
version: Bun.version,
revision: Bun.revision,
};
});
export const Route = createFileRoute("/")({
component: Home,
loader: async () => {
const bunInfo = await getBunInfo();
return { bunInfo };
},
});
function Home() {
const { bunInfo } = Route.useLoaderData();
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4 antialiased">
<div className="w-full max-w-md">
<div className="relative bg-card/80 backdrop-blur-xl text-card-foreground rounded-2xl border border-border/50 shadow-2xl overflow-hidden h-[550px] max-h-5/6 grid grid-rows-[auto_1fr_auto]">
<div className="relative w-full overflow-hidden h-[250px]">
<img src="/header.webp" alt="TanStack Logo" className="w-full h-full object-cover object-center" />
<div className="absolute top-3 right-3 bg-zinc-800/75 text-white text-xs font-medium px-2.5 py-1.5 rounded-md shadow-2xl backdrop-blur-sm">
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 bg-[#39FF14] rounded-full animate-pulse shadow-[0_0_8px_rgba(74,222,128,0.8)]"></div>
<span>Bun {bunInfo.version}</span>
</div>
{bunInfo.revision && (
<a
href={`https://github.com/oven-sh/bun/releases/tag/bun-v${bunInfo.version}`}
target="_blank"
rel="noopener noreferrer"
className="text-[10px] font-mono mt-0.5 opacity-90 pl-[18px] hover:opacity-100 transition-opacity"
>
{bunInfo.revision.slice(0, 8)}
</a>
)}
</div>
</div>
<div className="px-4 overflow-hidden">
<div className="flex flex-col items-center justify-center py-6 min-h-full">
<div className="text-center space-y-3 w-full">
<div>
<h1
className="text-2xl font-bold tracking-tight text-card-foreground leading-tight"
style={{ letterSpacing: "-0.02em" }}
>
Welcome to TanStack Start
</h1>
<p className="text-sm text-muted-foreground font-medium tracking-wide pb-2">
Powered by Bun {"\u2764\uFE0F"}
</p>
</div>
<div className="pt-2 border-t border-border/30">
<p className="text-sm text-muted-foreground/90 font-regular leading-relaxed max-w-sm mx-auto mt-2">
Edit{" "}
<code className="text-[11px] bg-zinc-200 dark:bg-zinc-800 px-1 py-0.5 rounded-xs mx-0.5">
src/routes/index.tsx
</code>{" "}
to see HMR in action.
<br />
Visit{" "}
<Link
to="/stats"
className="text-foreground/80 hover:text-foreground underline underline-offset-2 transition-colors font-medium"
>
/stats
</Link>{" "}
for server-side info, or explore{" "}
<a
href="https://bun.com/docs/runtime/bun-apis"
target="_blank"
rel="noopener noreferrer"
className="text-foreground/80 hover:text-foreground underline underline-offset-2 transition-colors font-medium"
>
Bun's APIs
</a>
.<br />
<br />
Ready to deploy? Check out the{" "}
<a
href="https://bun.com/guides/ecosystem/tanstack-start"
target="_blank"
rel="noopener noreferrer"
className="text-foreground/80 hover:text-foreground underline underline-offset-2 transition-colors font-medium"
>
TanStack guide
</a>
.
</p>
</div>
</div>
</div>
</div>
<div className="px-8 pb-6">
<div className="pt-6">
<Link
to="/stats"
className="block w-full px-4 py-2 bg-foreground text-background rounded-lg font-medium hover:opacity-90 transition-opacity text-center text-sm"
>
View Server Stats
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,157 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { cpus, totalmem } from "os";
const getServerStats = createServerFn({
method: "GET",
}).handler(async () => {
const bunVersion = Bun.version;
const bunRevision = Bun.revision;
const cpuUsage = process.cpuUsage();
const processUptime = process.uptime();
// Calculate CPU usage percentage to avoid showing falsy cumulative values
// CPU percentage = (total CPU time / (uptime * number of cores)) * 100
const numCores = cpus().length;
const totalCpuTime = (cpuUsage.user + cpuUsage.system) / 1000000; // Convert microseconds to seconds
const cpuPercentage = processUptime > 0 ? Math.min(100, (totalCpuTime / (processUptime * numCores)) * 100) : 0;
const cpuInfo = cpus()[0];
return {
bunVersion,
bunRevision,
platform: process.platform,
arch: process.arch,
pid: process.pid,
uptime: Math.floor(processUptime),
cpu: {
percentage: Math.round(cpuPercentage * 100) / 100,
cores: numCores,
},
environment: {
cpuModel: cpuInfo?.model || "Unknown",
totalMemory: totalmem(),
},
};
});
export const Route = createFileRoute("/stats")({
component: Stats,
loader: async () => {
const stats = await getServerStats();
return { stats };
},
});
function Stats() {
const { stats } = Route.useLoaderData();
const formatUptime = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
}
if (minutes > 0) {
return `${minutes}m ${secs}s`;
}
return `${secs}s`;
};
const formatBytes = (bytes: number) => {
const formatter = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1,
minimumFractionDigits: 0,
});
const gb = bytes / (1024 * 1024 * 1024);
if (gb >= 1) {
return `${formatter.format(gb)} GB`;
}
const mb = bytes / (1024 * 1024);
if (mb >= 1) {
return `${formatter.format(mb)} MB`;
}
const kb = bytes / 1024;
return `${formatter.format(kb)} KB`;
};
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4 antialiased">
<div className="w-full max-w-md">
<div className="relative bg-card/80 backdrop-blur-xl text-card-foreground rounded-2xl border border-border/50 shadow-2xl overflow-hidden h-[550px] max-h-4/5 grid grid-rows-[auto_1fr_auto]">
<div className="px-8 py-6">
<div className="space-y-2 text-center py-2">
<h1 className="text-2xl font-semibold tracking-tight text-foreground">Server Stats</h1>
<p className="text-lg text-muted-foreground font-medium -mt-2">Runtime information</p>
</div>
</div>
<div className="px-8 overflow-y-auto">
<div className="space-y-4 pt-4 border-t border-border/30">
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="space-y-1 text-center">
<p className="text-muted-foreground text-xs uppercase tracking-wide">Bun Version</p>
<p className="text-foreground font-medium">{stats.bunVersion}</p>
</div>
<div className="space-y-1 text-center">
<p className="text-muted-foreground text-xs uppercase tracking-wide">Revision</p>
<p className="text-foreground font-medium text-xs font-mono">{stats.bunRevision?.slice(0, 8)}</p>
</div>
<div className="space-y-1 text-center">
<p className="text-muted-foreground text-xs uppercase tracking-wide">Platform</p>
<p className="text-foreground font-medium">{stats.platform}</p>
</div>
<div className="space-y-1 text-center">
<p className="text-muted-foreground text-xs uppercase tracking-wide">Architecture</p>
<p className="text-foreground font-medium">{stats.arch}</p>
</div>
<div className="space-y-1 text-center">
<p className="text-muted-foreground text-xs uppercase tracking-wide">Process ID</p>
<p className="text-foreground font-medium">{stats.pid}</p>
</div>
<div className="space-y-1 text-center">
<p className="text-muted-foreground text-xs uppercase tracking-wide">Uptime</p>
<p className="text-foreground font-medium">{formatUptime(stats.uptime)}</p>
</div>
<div className="space-y-1 text-center">
<p className="text-muted-foreground text-xs uppercase tracking-wide">CPU Cores</p>
<p className="text-foreground font-medium">{stats.cpu.cores}</p>
</div>
<div className="space-y-1 text-center">
<p className="text-muted-foreground text-xs uppercase tracking-wide">CPU Usage</p>
<p className="text-foreground font-medium">{stats.cpu.percentage}%</p>
</div>
</div>
<div className="pt-4 border-t border-border/30">
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="space-y-1 text-center">
<p className="text-muted-foreground text-xs uppercase tracking-wide">CPU Model</p>
<p className="text-foreground font-medium text-xs">{stats.environment.cpuModel}</p>
</div>
<div className="space-y-1 text-center">
<p className="text-muted-foreground text-xs uppercase tracking-wide">Total Memory</p>
<p className="text-foreground font-medium">{formatBytes(stats.environment.totalMemory)}</p>
</div>
</div>
</div>
</div>
</div>
<div className="px-8 pb-6">
<div className="pt-6">
<Link
to="/"
className="block w-full px-4 py-2 bg-foreground text-background rounded-lg font-medium hover:opacity-90 transition-opacity text-center text-sm"
>
Back to Home
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,57 @@
@import "tailwindcss";
:root {
--background: oklch(0.985 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.9 0 0);
--ring: oklch(0.556 0 0);
}
@media (prefers-color-scheme: dark) {
:root {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(1 0 0 / 10%);
--ring: oklch(0.556 0 0);
}
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--color-ring: var(--ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
overflow-x: hidden;
overflow-y: auto;
max-width: 100vw;
width: 100%;
}
body {
@apply bg-background text-foreground;
font-family:
system-ui,
-apple-system,
sans-serif;
overflow-x: hidden;
max-width: 100vw;
width: 100%;
margin: 0;
padding: 0;
}
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"module": "ESNext",
"target": "ES2022",
"skipLibCheck": true,
"strictNullChecks": true
}
}

View File

@@ -0,0 +1,18 @@
import tailwindcss from "@tailwindcss/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import tsConfigPaths from "vite-tsconfig-paths";
export default defineConfig({
server: {
port: 3000,
},
plugins: [
tsConfigPaths(),
tanstackStart(),
// react's vite plugin must come after start's vite plugin
viteReact(),
tailwindcss(),
],
});

45
src/init/rule-tanstack.md Normal file
View File

@@ -0,0 +1,45 @@
---
description: Use Bun with TanStack Start instead of Node.js, npm, pnpm, or vite.
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
alwaysApply: false
---
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## TanStack Start
This project uses TanStack Start, a full-stack React framework powered by Vite.
- Use `createServerFn` from `@tanstack/react-start` for server-side functions
- Use file-based routing in the `src/routes` directory
- Use `createFileRoute` from `@tanstack/react-router` to define routes
- Server functions run on the server and can access Bun APIs directly
- Use loaders for data fetching in routes
For more information, read the [TanStack Start documentation](https://tanstack.com/router/latest/docs/framework/react/start/introduction) and Bun API docs in `node_modules/bun-types/docs/**.md`.

View File

@@ -295,4 +295,31 @@ import path from "path";
expect(fs.existsSync(path.join(temp, "src/components"))).toBe(true);
expect(fs.existsSync(path.join(temp, "src/components/ui"))).toBe(true);
}, 30_000);
test("bun init --react=tanstack works", async () => {
const temp = tempDirWithFiles("bun-init--react=tanstack-works", {});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "--react=tanstack"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).toBe(0);
const pkg = JSON.parse(fs.readFileSync(path.join(temp, "package.json"), "utf8"));
expect(pkg).toHaveProperty("dependencies.react");
expect(pkg).toHaveProperty("dependencies.react-dom");
expect(pkg).toHaveProperty("dependencies.@tanstack/react-router");
expect(pkg).toHaveProperty("dependencies.@tanstack/react-start");
expect(pkg).toHaveProperty("dependencies.@tailwindcss/vite");
expect(pkg).toHaveProperty("dependencies.tailwindcss");
expect(fs.existsSync(path.join(temp, "src"))).toBe(true);
expect(fs.existsSync(path.join(temp, "src/router.tsx"))).toBe(true);
expect(fs.existsSync(path.join(temp, "src/routes"))).toBe(true);
expect(fs.existsSync(path.join(temp, "vite.config.ts"))).toBe(true);
expect(fs.existsSync(path.join(temp, "public"))).toBe(true);
}, 30_000);
});