mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Add Tanstack Start to bun init (#24648)
Co-authored-by: Alistair Smith <hi@alistair.sh>
This commit is contained in:
35
src/cli/init/README-tanstack.default.md
Normal file
35
src/cli/init/README-tanstack.default.md
Normal 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).
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
28
src/init/react-tanstack/package.json
Normal file
28
src/init/react-tanstack/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
src/init/react-tanstack/public/favicon.ico
Normal file
BIN
src/init/react-tanstack/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
src/init/react-tanstack/public/header.webp
Normal file
BIN
src/init/react-tanstack/public/header.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
84
src/init/react-tanstack/src/routeTree.gen.ts
Normal file
84
src/init/react-tanstack/src/routeTree.gen.ts
Normal 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>>;
|
||||
}
|
||||
}
|
||||
11
src/init/react-tanstack/src/router.tsx
Normal file
11
src/init/react-tanstack/src/router.tsx
Normal 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;
|
||||
}
|
||||
87
src/init/react-tanstack/src/routes/__root.tsx
Normal file
87
src/init/react-tanstack/src/routes/__root.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
118
src/init/react-tanstack/src/routes/index.tsx
Normal file
118
src/init/react-tanstack/src/routes/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
157
src/init/react-tanstack/src/routes/stats.tsx
Normal file
157
src/init/react-tanstack/src/routes/stats.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
src/init/react-tanstack/styles.css
Normal file
57
src/init/react-tanstack/styles.css
Normal 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;
|
||||
}
|
||||
}
|
||||
10
src/init/react-tanstack/tsconfig.json
Normal file
10
src/init/react-tanstack/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "Bundler",
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
||||
18
src/init/react-tanstack/vite.config.ts
Normal file
18
src/init/react-tanstack/vite.config.ts
Normal 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
45
src/init/rule-tanstack.md
Normal 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`.
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user