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

@@ -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();
}