mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
966 lines
36 KiB
Zig
966 lines
36 KiB
Zig
// Generate project files based on the entry point and dependencies
|
|
pub const Dependencies = struct {
|
|
deps: std.ArrayList([]const u8),
|
|
dev_deps: std.ArrayList([]const u8),
|
|
|
|
pub fn init(allocator: std.mem.Allocator) Dependencies {
|
|
return .{
|
|
.deps = std.ArrayList([]const u8).init(allocator),
|
|
.dev_deps = std.ArrayList([]const u8).init(allocator),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Dependencies) void {
|
|
self.deps.deinit();
|
|
self.dev_deps.deinit();
|
|
}
|
|
};
|
|
|
|
pub const GenerateOptions = struct {
|
|
tailwind: bool = false,
|
|
shadcn: ?bun.StringSet = null,
|
|
dependencies: ?Dependencies = null,
|
|
};
|
|
|
|
// Original function that analyzes an existing entry point
|
|
pub fn generate(entry_point: string, result: *BundleV2.DependenciesScanner.Result) !void {
|
|
const react_component_export = findReactComponentExport(result.bundle_v2) orelse {
|
|
Output.errGeneric("No component export found in <b>{s}<r>", .{bun.fmt.quote(entry_point)});
|
|
Output.flush();
|
|
const writer = Output.errorWriterBuffered();
|
|
try writer.writeAll(
|
|
\\
|
|
\\Please add an export to your file. For example:
|
|
\\
|
|
\\ export default function MyApp() {{
|
|
\\ return <div>Hello World</div>;
|
|
\\ }};
|
|
\\
|
|
);
|
|
|
|
Output.flush();
|
|
Global.crash();
|
|
};
|
|
|
|
// Check if Tailwind is already in dependencies
|
|
const has_tailwind_in_dependencies = result.dependencies.contains("tailwindcss") or result.dependencies.contains("bun-plugin-tailwind");
|
|
var needs_to_inject_tailwind = false;
|
|
if (!has_tailwind_in_dependencies) {
|
|
// Scan source files for Tailwind classes if not already in dependencies
|
|
needs_to_inject_tailwind = hasAnyTailwindClassesInSourceFiles(result.bundle_v2, result.reachable_files);
|
|
}
|
|
|
|
// Get any shadcn components used in the project
|
|
const shadcn = if (enable_shadcn_ui) try getShadcnComponents(result.bundle_v2, result.reachable_files) else bun.StringSet.init(default_allocator);
|
|
|
|
// Convert dependencies to new format
|
|
var dependencies = Dependencies.init(default_allocator);
|
|
try dependencies.deps.appendSlice(result.dependencies.keys());
|
|
|
|
try generateFromOptions(.{
|
|
.tailwind = has_tailwind_in_dependencies or needs_to_inject_tailwind,
|
|
.shadcn = if (shadcn.keys().len > 0) shadcn else null,
|
|
.dependencies = dependencies,
|
|
}, entry_point, react_component_export);
|
|
}
|
|
|
|
// New function for generating without an existing entry point
|
|
pub fn generateFromOptions(options: GenerateOptions, entry_point: string, react_component_export: ?[]const u8) !void {
|
|
var dependencies = options.dependencies orelse Dependencies.init(default_allocator);
|
|
const needs_to_inject_shadcn_ui = options.shadcn != null;
|
|
const uses_tailwind = options.tailwind;
|
|
|
|
// Add Tailwind dependencies if needed
|
|
if (uses_tailwind) {
|
|
try dependencies.deps.append("tailwindcss");
|
|
try dependencies.deps.append("bun-plugin-tailwind");
|
|
}
|
|
|
|
// Add shadcn-ui dependencies if needed
|
|
if (needs_to_inject_shadcn_ui) {
|
|
// https://ui.shadcn.com/docs/installation/manual
|
|
try dependencies.deps.append("tailwindcss-animate");
|
|
try dependencies.deps.append("class-variance-authority");
|
|
try dependencies.deps.append("clsx");
|
|
try dependencies.deps.append("tailwind-merge");
|
|
try dependencies.deps.append("lucide-react");
|
|
}
|
|
|
|
// We are JSX-only for now.
|
|
// The versions of react & react-dom need to match up, and it's SO easy to mess that up.
|
|
// So we have to be a little opinionated here.
|
|
if (needs_to_inject_shadcn_ui) {
|
|
// Use react 18 instead of 19 if shadcn is in use.
|
|
// Remove any existing react dependencies
|
|
for (dependencies.deps.items, 0..) |dep, i| {
|
|
if (strings.eqlComptime(dep, "react") or strings.eqlComptime(dep, "react-dom")) {
|
|
_ = dependencies.deps.orderedRemove(i);
|
|
}
|
|
}
|
|
try dependencies.deps.append("react@^18");
|
|
try dependencies.deps.append("react-dom@^18");
|
|
try dependencies.dev_deps.append("@types/react@^18");
|
|
try dependencies.dev_deps.append("@types/react-dom@^18");
|
|
} else {
|
|
// Add react-dom if react is used
|
|
// Remove any existing react dependencies
|
|
for (dependencies.deps.items, 0..) |dep, i| {
|
|
if (strings.eqlComptime(dep, "react") or strings.eqlComptime(dep, "react-dom")) {
|
|
_ = dependencies.deps.orderedRemove(i);
|
|
}
|
|
}
|
|
try dependencies.deps.append("react-dom@19");
|
|
try dependencies.deps.append("react@19");
|
|
try dependencies.dev_deps.append("@types/react@19");
|
|
try dependencies.dev_deps.append("@types/react-dom@19");
|
|
}
|
|
|
|
// Add dev dependencies
|
|
try dependencies.dev_deps.append("@types/bun@latest");
|
|
try dependencies.dev_deps.append("typescript@^5.0.0");
|
|
|
|
// Choose template based on dependencies and example type
|
|
const template: Template = brk: {
|
|
if (needs_to_inject_shadcn_ui) {
|
|
break :brk .{ .ReactShadcnSpa = .{ .components = options.shadcn orelse bun.StringSet.init(default_allocator) } };
|
|
} else if (uses_tailwind) {
|
|
break :brk .ReactTailwindSpa;
|
|
} else {
|
|
break :brk .ReactSpa;
|
|
}
|
|
};
|
|
|
|
// Generate project files from template
|
|
try generateFiles(default_allocator, entry_point, dependencies, template, react_component_export);
|
|
|
|
Global.exit(0);
|
|
}
|
|
|
|
// Create a file with given contents, returns if file was newly created
|
|
fn createFile(filename: []const u8, contents: []const u8) bun.JSC.Maybe(bool) {
|
|
// Check if file exists and has same contents
|
|
if (bun.sys.File.readFrom(bun.toFD(std.fs.cwd()), filename, default_allocator).asValue()) |source_contents| {
|
|
defer default_allocator.free(source_contents);
|
|
if (strings.eqlLong(source_contents, contents, true)) {
|
|
return .{ .result = false };
|
|
}
|
|
}
|
|
|
|
// Create parent directories if needed
|
|
if (std.fs.path.dirname(filename)) |dirname| {
|
|
bun.makePath(std.fs.cwd(), dirname) catch {};
|
|
}
|
|
|
|
// Open file for writing
|
|
const fd = switch (bun.sys.openatA(bun.toFD(std.fs.cwd()), filename, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o644)) {
|
|
.result => |fd| fd,
|
|
.err => |err| return .{ .err = err },
|
|
};
|
|
defer _ = bun.sys.close(fd);
|
|
|
|
// Write contents
|
|
switch (bun.sys.File.writeAll(.{ .handle = fd }, contents)) {
|
|
.result => return .{ .result = true },
|
|
.err => |err| return .{ .err = err },
|
|
}
|
|
}
|
|
|
|
// Count number of occurrences to calculate buffer size
|
|
fn countReplaceAllOccurrences(input: []const u8, needle: []const u8, replacement: []const u8) usize {
|
|
var remaining = input;
|
|
var count: usize = 0;
|
|
while (remaining.len > 0) {
|
|
if (std.mem.indexOf(u8, remaining, needle)) |index| {
|
|
remaining = remaining[index + needle.len ..];
|
|
count += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return input.len + (count * (replacement.len -| needle.len));
|
|
}
|
|
|
|
// Replace all occurrences of needle with replacement
|
|
fn replaceAllOccurrencesOfString(allocator: std.mem.Allocator, input: []const u8, needle: []const u8, replacement: []const u8) ![]u8 {
|
|
var result = try std.ArrayList(u8).initCapacity(allocator, countReplaceAllOccurrences(input, needle, replacement));
|
|
var remaining = input;
|
|
while (remaining.len > 0) {
|
|
if (std.mem.indexOf(u8, remaining, needle)) |index| {
|
|
const new_remaining = remaining[index + needle.len ..];
|
|
try result.appendSlice(remaining[0..index]);
|
|
try result.appendSlice(replacement);
|
|
remaining = new_remaining;
|
|
} else {
|
|
try result.appendSlice(remaining);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result.items;
|
|
}
|
|
|
|
// Replace template placeholders with actual values
|
|
fn stringWithReplacements(original_input: []const u8, basename: []const u8, relative_name: []const u8, react_component_export: []const u8, allocator: std.mem.Allocator) ![]const u8 {
|
|
var input = original_input;
|
|
|
|
if (strings.contains(input, "REPLACE_ME_WITH_YOUR_REACT_COMPONENT_EXPORT")) {
|
|
input = try replaceAllOccurrencesOfString(allocator, input, "REPLACE_ME_WITH_YOUR_REACT_COMPONENT_EXPORT", react_component_export);
|
|
}
|
|
|
|
if (strings.contains(input, "REPLACE_ME_WITH_YOUR_APP_BASE_NAME")) {
|
|
input = try replaceAllOccurrencesOfString(allocator, input, "REPLACE_ME_WITH_YOUR_APP_BASE_NAME", basename);
|
|
}
|
|
|
|
if (strings.contains(input, "REPLACE_ME_WITH_YOUR_APP_FILE_NAME")) {
|
|
input = try replaceAllOccurrencesOfString(allocator, input, "REPLACE_ME_WITH_YOUR_APP_FILE_NAME", relative_name);
|
|
}
|
|
|
|
return input;
|
|
}
|
|
|
|
// Generate all project files from template
|
|
fn generateFiles(allocator: std.mem.Allocator, entry_point: string, dependencies: Dependencies, template: Template, react_component_export: ?[]const u8) !void {
|
|
var log = template.logger();
|
|
var basename = std.fs.path.basename(entry_point);
|
|
const extension = std.fs.path.extension(basename);
|
|
if (extension.len > 0) {
|
|
basename = basename[0 .. basename.len - extension.len];
|
|
}
|
|
|
|
// Normalize file paths
|
|
var normalized_buf: bun.PathBuffer = undefined;
|
|
var normalized_name: []const u8 = if (std.fs.path.isAbsolute(entry_point))
|
|
bun.path.relativeNormalizedBuf(&normalized_buf, bun.fs.FileSystem.instance.top_level_dir, entry_point, .posix, true)
|
|
else
|
|
bun.path.normalizeBuf(entry_point, &normalized_buf, .posix);
|
|
|
|
if (extension.len > 0) {
|
|
normalized_name = normalized_name[0 .. normalized_name.len - extension.len];
|
|
}
|
|
|
|
// Generate files based on template type
|
|
switch (@as(Template.Tag, template)) {
|
|
inline else => |active| {
|
|
const current = @field(SourceFileProjectGenerator, @tagName(active));
|
|
const files: []const TemplateFile = current.files;
|
|
|
|
var max_filename_len: usize = 0;
|
|
var filenames: [files.len]string = undefined;
|
|
var created_files: [files.len]bool = .{false} ** files.len;
|
|
|
|
// Create all template files
|
|
inline for (0..files.len) |index| {
|
|
const file = &files[index];
|
|
var file_name = try stringWithReplacements(file.name, basename, normalized_name, react_component_export orelse "App", allocator);
|
|
|
|
if (strings.eqlComptime(file.name, "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.tsx") and !strings.eqlComptime(extension, ".tsx")) {
|
|
// replace the extension with the extension of the file
|
|
file_name = try std.fmt.allocPrint(allocator, "{s}{s}", .{ file_name[0 .. file_name.len - extension.len], extension });
|
|
}
|
|
|
|
if (file.overwrite or !bun.sys.exists(file_name)) {
|
|
switch (createFile(file_name, try stringWithReplacements(file.content, basename, normalized_name, react_component_export orelse "", default_allocator))) {
|
|
.result => |new| {
|
|
if (new) {
|
|
created_files[index] = true;
|
|
filenames[index] = file_name;
|
|
max_filename_len = @max(max_filename_len, file_name.len);
|
|
}
|
|
},
|
|
.err => |err| {
|
|
Output.err(err, "failed to create {s}", .{file_name});
|
|
Global.crash();
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
for (files, filenames, created_files) |*file, filename, created| {
|
|
if (created) {
|
|
log.file(file, filename, max_filename_len);
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
// Install dependencies
|
|
var argv = std.ArrayList([]const u8).init(default_allocator);
|
|
try argv.append("bun");
|
|
try argv.append("--only-missing");
|
|
try argv.append("install");
|
|
try argv.appendSlice(dependencies.deps.items);
|
|
// TODO: add this to --dev flag
|
|
try argv.appendSlice(dependencies.dev_deps.items);
|
|
|
|
if (log.has_written_initial_message) {
|
|
Output.print("\n", .{});
|
|
}
|
|
Output.pretty("<r>📦 <b>Auto-installing {d} detected dependencies<r>\n", .{dependencies.deps.items.len + dependencies.dev_deps.items.len});
|
|
|
|
// print "bun" but use bun.selfExePath()
|
|
Output.commandOut(argv.items);
|
|
|
|
Output.flush();
|
|
|
|
argv.items[0] = try bun.selfExePath();
|
|
|
|
const process = bun.spawnSync(&.{
|
|
.argv = argv.items,
|
|
.envp = null,
|
|
.cwd = bun.fs.FileSystem.instance.top_level_dir,
|
|
.stderr = .inherit,
|
|
.stdout = .inherit,
|
|
.stdin = .inherit,
|
|
|
|
.windows = if (Environment.isWindows) .{
|
|
.loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)),
|
|
},
|
|
}) catch |err| {
|
|
Output.err(err, "failed to install dependencies", .{});
|
|
Global.crash();
|
|
};
|
|
|
|
switch (process) {
|
|
.err => |err| {
|
|
Output.err(err, "failed to install dependencies", .{});
|
|
Global.crash();
|
|
},
|
|
.result => |spawn_result| {
|
|
if (!spawn_result.status.isOK()) {
|
|
if (spawn_result.status.signalCode()) |signal| {
|
|
if (signal.toExitCode()) |exit_code| {
|
|
Global.exit(exit_code);
|
|
}
|
|
}
|
|
|
|
if (spawn_result.status == .exited) {
|
|
Global.exit(spawn_result.status.exited.code);
|
|
}
|
|
|
|
Global.crash();
|
|
}
|
|
},
|
|
}
|
|
|
|
// Show success message and start dev server
|
|
|
|
switch (template) {
|
|
.ReactShadcnSpa => |*shadcn| {
|
|
if (shadcn.components.keys().len > 0) {
|
|
// Add shadcn components
|
|
var shadcn_argv = try std.ArrayList([]const u8).initCapacity(default_allocator, 10);
|
|
try shadcn_argv.append("bun");
|
|
try shadcn_argv.append("x");
|
|
try shadcn_argv.append("shadcn@canary");
|
|
try shadcn_argv.append("add");
|
|
if (strings.contains(normalized_name, "/src")) {
|
|
try shadcn_argv.append("--src-dir");
|
|
}
|
|
try shadcn_argv.append("-y");
|
|
try shadcn_argv.appendSlice(shadcn.components.keys());
|
|
|
|
// print "bun" but use bun.selfExePath()
|
|
Output.prettyln("\n<r>😎 <b>Setting up shadcn/ui components<r>", .{});
|
|
Output.commandOut(shadcn_argv.items);
|
|
Output.flush();
|
|
shadcn_argv.items[0] = try bun.selfExePath();
|
|
|
|
// Now we need to run shadcn to add the components to the project
|
|
const shadcn_process = bun.spawnSync(&.{
|
|
.argv = shadcn_argv.items,
|
|
.envp = null,
|
|
.cwd = bun.fs.FileSystem.instance.top_level_dir,
|
|
.stderr = .inherit,
|
|
.stdout = .inherit,
|
|
.stdin = .inherit,
|
|
}) catch |err| {
|
|
Output.err(err, "failed to add shadcn components", .{});
|
|
Global.crash();
|
|
};
|
|
|
|
switch (shadcn_process) {
|
|
.err => |err| {
|
|
Output.err(err, "failed to add shadcn components", .{});
|
|
Global.crash();
|
|
},
|
|
.result => |spawn_result| {
|
|
if (!spawn_result.status.isOK()) {
|
|
if (spawn_result.status.signalCode()) |signal| {
|
|
if (signal.toExitCode()) |exit_code| {
|
|
Global.exit(exit_code);
|
|
}
|
|
}
|
|
|
|
if (spawn_result.status == .exited) {
|
|
Global.exit(spawn_result.status.exited.code);
|
|
}
|
|
|
|
Global.crash();
|
|
}
|
|
},
|
|
}
|
|
|
|
Output.print("\n", .{});
|
|
}
|
|
},
|
|
.ReactSpa, .ReactTailwindSpa => {},
|
|
}
|
|
const normalized_name_with_ext = try std.fmt.allocPrint(allocator, "./{s}{s}", .{ normalized_name, extension });
|
|
log.ifNew(normalized_name_with_ext);
|
|
|
|
Output.flush();
|
|
|
|
// Start dev server
|
|
const start = bun.spawnSync(&.{
|
|
.argv = &.{
|
|
try bun.selfExePath(),
|
|
"dev",
|
|
},
|
|
.envp = null,
|
|
.cwd = bun.fs.FileSystem.instance.top_level_dir,
|
|
.stderr = .inherit,
|
|
.stdout = .inherit,
|
|
.stdin = .inherit,
|
|
|
|
.windows = if (Environment.isWindows) .{
|
|
.loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)),
|
|
},
|
|
}) catch |err| {
|
|
Output.err(err, "failed to start app", .{});
|
|
Global.crash();
|
|
};
|
|
|
|
switch (start) {
|
|
.err => |err| {
|
|
Output.err(err, "failed to start app", .{});
|
|
Global.crash();
|
|
},
|
|
.result => |spawn_result| {
|
|
if (!spawn_result.status.isOK()) {
|
|
if (spawn_result.status.signalCode()) |signal| {
|
|
if (signal.toExitCode()) |exit_code| {
|
|
Global.exit(exit_code);
|
|
}
|
|
}
|
|
|
|
if (spawn_result.status == .exited) {
|
|
Global.exit(spawn_result.status.exited.code);
|
|
}
|
|
|
|
Global.crash();
|
|
}
|
|
},
|
|
}
|
|
|
|
Global.exit(0);
|
|
}
|
|
|
|
// Check if any source files contain Tailwind classes
|
|
fn hasAnyTailwindClassesInSourceFiles(bundler: *BundleV2, reachable_files: []const js_ast.Index) bool {
|
|
const input_files = bundler.graph.input_files.slice();
|
|
const sources = input_files.items(.source);
|
|
const loaders = input_files.items(.loader);
|
|
|
|
// Common Tailwind class patterns to look for
|
|
const common_tailwind_patterns = [_][]const u8{ "bg-", "text-", "p-", "m-", "flex", "grid", "border", "rounded", "shadow", "hover:", "focus:", "dark:", "sm:", "md:", "lg:", "xl:", "w-", "h-", "space-", "gap-", "items-", "justify-", "font-" };
|
|
|
|
for (reachable_files) |file| {
|
|
switch (loaders[file.get()]) {
|
|
.tsx, .jsx => {
|
|
const source: *const bun.logger.Source = &sources[file.get()];
|
|
var source_code: []const u8 = source.contents;
|
|
|
|
// First check for className=" or className='
|
|
while (strings.indexOf(source_code, "className=")) |index| {
|
|
source_code = source_code[index + "className=".len ..];
|
|
if (source_code.len < 1) return false;
|
|
switch (source_code[0]) {
|
|
'\'', '"' => |quote| {
|
|
source_code = source_code[1..];
|
|
const end_quote = strings.indexOfChar(source_code, quote) orelse continue;
|
|
const class_name = source_code[0..end_quote];
|
|
// search for tailwind patterns
|
|
for (common_tailwind_patterns) |pattern| {
|
|
if (std.mem.indexOf(u8, class_name, pattern) != null) {
|
|
return true;
|
|
}
|
|
}
|
|
},
|
|
else => {
|
|
source_code = source_code[1..];
|
|
},
|
|
}
|
|
}
|
|
},
|
|
.html => {
|
|
const source: *const bun.logger.Source = &sources[file.get()];
|
|
const source_code: []const u8 = source.contents;
|
|
|
|
// Look for class=" or class='
|
|
var i: usize = 0;
|
|
while (i < source_code.len) : (i += 1) {
|
|
if (i + 7 >= source_code.len) break;
|
|
|
|
if (strings.hasPrefixComptime(source_code, "class")) {
|
|
// Skip whitespace
|
|
var j = i + 5;
|
|
while (j < source_code.len and (source_code[j] == ' ' or source_code[j] == '=')) : (j += 1) {}
|
|
if (j < source_code.len and (source_code[j] == '"' or source_code[j] == '\'')) {
|
|
// Found a class attribute, now check for Tailwind patterns
|
|
for (common_tailwind_patterns) |pattern| {
|
|
if (std.mem.indexOf(u8, source_code[j..@min(j + 1000, source_code.len)], pattern) != null) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
i = j;
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Get list of shadcn components used in source files
|
|
fn getShadcnComponents(bundler: *BundleV2, reachable_files: []const js_ast.Index) !bun.StringSet {
|
|
const input_files = bundler.graph.input_files.slice();
|
|
const loaders = input_files.items(.loader);
|
|
const all = bundler.graph.ast.items(.import_records);
|
|
var icons = bun.StringSet.init(default_allocator);
|
|
for (reachable_files) |file| {
|
|
switch (loaders[file.get()]) {
|
|
.tsx, .jsx => {
|
|
const import_records = all[file.get()];
|
|
for (import_records.slice()) |*import_record| {
|
|
if (strings.hasPrefixComptime(import_record.path.text, "@/components/ui/")) {
|
|
try icons.insert(import_record.path.text["@/components/ui/".len..]);
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
return icons;
|
|
}
|
|
|
|
fn findReactComponentExport(bundler: *BundleV2) ?[]const u8 {
|
|
const input_files = bundler.graph.input_files.slice();
|
|
const loaders = input_files.items(.loader);
|
|
const resolved_exports: []const bun.bundle_v2.ResolvedExports = bundler.linker.graph.meta.items(.resolved_exports);
|
|
const sources = input_files.items(.source);
|
|
|
|
const entry_point_ids = bundler.graph.entry_points.items;
|
|
for (entry_point_ids) |entry_point_id| {
|
|
const loader = loaders[entry_point_id.get()];
|
|
if (loader == .jsx or loader == .tsx) {
|
|
const source: *const bun.logger.Source = &sources[entry_point_id.get()];
|
|
const exports = &resolved_exports[entry_point_id.get()];
|
|
|
|
// 1. Prioritize the default export
|
|
if (exports.contains("default")) {
|
|
return "default";
|
|
}
|
|
|
|
const export_names = exports.keys();
|
|
if (export_names.len == 1) {
|
|
// If there's only one export it can only be this.
|
|
return export_names[0];
|
|
}
|
|
|
|
if (export_names.len == 0) {
|
|
// If there are no exports, we can't determine the component name.
|
|
continue;
|
|
}
|
|
|
|
const filename = source.path.name.nonUniqueNameStringBase();
|
|
if (filename.len == 0) {
|
|
@branchHint(.unlikely);
|
|
continue;
|
|
}
|
|
|
|
// 2. Prioritize the export matching the filename with an uppercase first letter
|
|
// such as export const App = () => { ... }
|
|
if (filename[0] >= 'A' and filename[0] <= 'Z') {
|
|
if (bun.js_lexer.isIdentifier(filename)) {
|
|
if (exports.contains(filename)) {
|
|
return filename;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (filename[0] >= 'a' and filename[0] <= 'z') {
|
|
const duped = default_allocator.dupe(u8, filename) catch bun.outOfMemory();
|
|
duped[0] = duped[0] - 32;
|
|
if (bun.js_lexer.isIdentifier(duped)) {
|
|
if (exports.contains(duped)) {
|
|
return duped;
|
|
}
|
|
}
|
|
|
|
{
|
|
// Extremely naive pascal case conversion
|
|
// - Does not handle unicode.
|
|
var input_index: usize = 0;
|
|
var output_index: usize = 0;
|
|
var capitalize_next = false;
|
|
while (input_index < duped.len) : (input_index += 1) {
|
|
if (duped[input_index] == ' ' or duped[input_index] == '-' or duped[input_index] == '_' or (output_index == 0 and !bun.js_lexer.isIdentifierStart(duped[input_index]))) {
|
|
capitalize_next = true;
|
|
continue;
|
|
}
|
|
if (output_index == 0 or capitalize_next) {
|
|
if (duped[input_index] >= 'a' and duped[input_index] <= 'z') {
|
|
duped[output_index] = duped[input_index] - 32;
|
|
} else {
|
|
duped[output_index] = duped[input_index];
|
|
}
|
|
capitalize_next = false;
|
|
output_index += 1;
|
|
} else {
|
|
duped[output_index] = duped[input_index];
|
|
output_index += 1;
|
|
}
|
|
}
|
|
|
|
// Try the pascal case version
|
|
// - "my-app" -> "MyApp"
|
|
// - "my_app" -> "MyApp"
|
|
// - "My-App" -> "MyApp"
|
|
if (exports.contains(duped[0..output_index])) {
|
|
return duped[0..output_index];
|
|
}
|
|
|
|
// Okay that didn't work. Try the version that's the current
|
|
// filename with the first letter capitalized
|
|
// - "my-app" -> "Myapp"
|
|
// - "My-App" -> "Myapp"
|
|
if (output_index > 1) {
|
|
for (duped[1..output_index]) |*c| {
|
|
switch (c.*) {
|
|
'A'...'Z' => {
|
|
c.* = c.* + 32;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
if (exports.contains(duped[0..output_index])) {
|
|
return duped[0..output_index];
|
|
}
|
|
}
|
|
|
|
default_allocator.free(duped);
|
|
}
|
|
|
|
const name_to_try = MutableString.ensureValidIdentifier(filename, default_allocator) catch return null;
|
|
if (exports.contains(name_to_try)) {
|
|
return name_to_try;
|
|
}
|
|
|
|
// Okay we really have no idea now.
|
|
// Let's just pick one that looks like a react component I guess.
|
|
for (export_names) |export_name| {
|
|
if (export_name.len > 0 and export_name[0] >= 'A' and export_name[0] <= 'Z') {
|
|
return export_name;
|
|
}
|
|
}
|
|
|
|
// Okay now we just have to pick one.
|
|
if (export_names.len > 0) {
|
|
return export_names[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const bun = @import("root").bun;
|
|
const string = bun.string;
|
|
const Output = bun.Output;
|
|
const Global = bun.Global;
|
|
const Environment = bun.Environment;
|
|
const strings = bun.strings;
|
|
const MutableString = bun.MutableString;
|
|
const stringZ = bun.stringZ;
|
|
const default_allocator = bun.default_allocator;
|
|
const C = bun.C;
|
|
const std = @import("std");
|
|
const Progress = bun.Progress;
|
|
|
|
const lex = bun.js_lexer;
|
|
const logger = bun.logger;
|
|
|
|
const js_parser = bun.js_parser;
|
|
const js_ast = bun.JSAst;
|
|
const linker = @import("../linker.zig");
|
|
|
|
const Api = @import("../api/schema.zig").Api;
|
|
const resolve_path = @import("../resolver/resolve_path.zig");
|
|
const BundleV2 = bun.bundle_v2.BundleV2;
|
|
const Command = bun.CLI.Command;
|
|
const Example = @import("../cli/create_command.zig").Example;
|
|
|
|
// Disabled until Tailwind v4 is supported.
|
|
const enable_shadcn_ui = true;
|
|
|
|
const TemplateFile = struct {
|
|
name: []const u8,
|
|
content: []const u8,
|
|
reason: Reason,
|
|
overwrite: bool = true,
|
|
};
|
|
|
|
const Reason = enum {
|
|
shadcn,
|
|
bun,
|
|
css,
|
|
tsc,
|
|
build,
|
|
html,
|
|
npm,
|
|
};
|
|
|
|
// Template for React + Tailwind project
|
|
const ReactTailwindSpa = struct {
|
|
pub const files = &[_]TemplateFile{
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.tsx",
|
|
.content = shared_app_tsx,
|
|
.reason = .bun,
|
|
.overwrite = false,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts",
|
|
.content = shared_build_ts,
|
|
.reason = .build,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css",
|
|
.content = @embedFile("projects/react-tailwind-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css"),
|
|
.reason = .css,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html",
|
|
.content = shared_html,
|
|
.reason = .html,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.client.tsx",
|
|
.content = shared_client_tsx,
|
|
.reason = .bun,
|
|
},
|
|
.{
|
|
.name = "bunfig.toml",
|
|
.content = shared_bunfig_toml,
|
|
.reason = .bun,
|
|
.overwrite = false,
|
|
},
|
|
.{
|
|
.name = "package.json",
|
|
.content = shared_package_json,
|
|
.reason = .npm,
|
|
.overwrite = false,
|
|
},
|
|
};
|
|
};
|
|
|
|
const shared_build_ts = @embedFile("projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts");
|
|
const shared_client_tsx = @embedFile("projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.client.tsx");
|
|
const shared_app_tsx = @embedFile("projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.tsx");
|
|
const shared_html = @embedFile("projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html");
|
|
const shared_package_json = @embedFile("projects/react-shadcn-spa/package.json");
|
|
const shared_bunfig_toml = @embedFile("projects/react-shadcn-spa/bunfig.toml");
|
|
|
|
// Template for basic React project
|
|
const ReactSpa = struct {
|
|
pub const files = &[_]TemplateFile{
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.tsx",
|
|
.content = shared_app_tsx,
|
|
.reason = .bun,
|
|
.overwrite = false,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts",
|
|
.content = shared_build_ts,
|
|
.reason = .build,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css",
|
|
.content = @embedFile("projects/react-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css"),
|
|
.reason = .css,
|
|
.overwrite = false,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html",
|
|
.content = shared_html,
|
|
.reason = .html,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.client.tsx",
|
|
.content = shared_client_tsx,
|
|
.reason = .bun,
|
|
},
|
|
|
|
.{
|
|
.name = "package.json",
|
|
.content = @embedFile("projects/react-spa/package.json"),
|
|
.reason = .npm,
|
|
.overwrite = false,
|
|
},
|
|
};
|
|
};
|
|
|
|
// Template for React + Shadcn project
|
|
const ReactShadcnSpa = struct {
|
|
pub const files = &[_]TemplateFile{
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.tsx",
|
|
.content = shared_app_tsx,
|
|
.reason = .bun,
|
|
.overwrite = false,
|
|
},
|
|
.{
|
|
.name = "lib/utils.ts",
|
|
.content = @embedFile("projects/react-shadcn-spa/lib/utils.ts"),
|
|
.reason = .shadcn,
|
|
},
|
|
.{
|
|
.name = "src/index.css",
|
|
.content = @embedFile("projects/react-shadcn-spa/styles/index.css"),
|
|
.reason = .shadcn,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts",
|
|
.content = shared_build_ts,
|
|
.reason = .bun,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.client.tsx",
|
|
.content = shared_client_tsx,
|
|
.reason = .bun,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css",
|
|
.content = @embedFile("projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css"),
|
|
.reason = .css,
|
|
},
|
|
.{
|
|
.name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html",
|
|
.content = shared_html,
|
|
.reason = .html,
|
|
},
|
|
.{
|
|
.name = "styles/globals.css",
|
|
.content = @embedFile("projects/react-shadcn-spa/styles/globals.css"),
|
|
.reason = .shadcn,
|
|
},
|
|
.{
|
|
.name = "bunfig.toml",
|
|
.content = shared_bunfig_toml,
|
|
.reason = .bun,
|
|
.overwrite = false,
|
|
},
|
|
.{
|
|
.name = "package.json",
|
|
.content = shared_package_json,
|
|
.reason = .npm,
|
|
.overwrite = false,
|
|
},
|
|
.{
|
|
.name = "tsconfig.json",
|
|
.content = @embedFile("projects/react-shadcn-spa/tsconfig.json"),
|
|
.reason = .tsc,
|
|
.overwrite = false,
|
|
},
|
|
.{
|
|
.name = "components.json",
|
|
.content = @embedFile("projects/react-shadcn-spa/components.json"),
|
|
.reason = .shadcn,
|
|
.overwrite = false,
|
|
},
|
|
};
|
|
};
|
|
|
|
// Template type to handle different project types
|
|
const Template = union(Tag) {
|
|
ReactTailwindSpa: void,
|
|
ReactSpa: void,
|
|
ReactShadcnSpa: struct {
|
|
components: bun.StringSet,
|
|
},
|
|
|
|
pub const Tag = enum {
|
|
ReactTailwindSpa,
|
|
ReactSpa,
|
|
ReactShadcnSpa,
|
|
|
|
pub fn logger(self: Tag) Logger {
|
|
return Logger{ .template = self };
|
|
}
|
|
|
|
pub fn label(self: Tag) []const u8 {
|
|
return switch (self) {
|
|
.ReactTailwindSpa => "React + Tailwind",
|
|
.ReactSpa => "React",
|
|
.ReactShadcnSpa => "React + shadcn/ui + Tailwind",
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn logger(self: Template) Logger {
|
|
return Logger{ .template = self };
|
|
}
|
|
|
|
pub const Logger = struct {
|
|
has_written_initial_message: bool = false,
|
|
template: Tag,
|
|
|
|
pub fn file(this: *Logger, template_file: *const TemplateFile, name: []const u8, max_name_len: usize) void {
|
|
this.has_written_initial_message = true;
|
|
Output.pretty(" <green>create<r> ", .{});
|
|
Output.pretty("{s}", .{name});
|
|
const name_len = name.len;
|
|
var padding: usize = max_name_len - name_len;
|
|
while (padding > 0) : (padding -= 1) {
|
|
Output.pretty(" ", .{});
|
|
}
|
|
Output.prettyln(" <d>{s}<r>", .{@tagName(template_file.reason)});
|
|
}
|
|
|
|
pub fn ifNew(this: *Logger, main_file_name: []const u8) void {
|
|
// if (!this.has_written_initial_message) return;
|
|
|
|
Output.prettyln(
|
|
\\<r><d>--------------------------------<r>
|
|
\\✨ <b>{s}<r> project configured
|
|
\\
|
|
\\<b><cyan>Development<r><d> - frontend dev server with hot reload<r>
|
|
\\
|
|
\\ <cyan><b>bun dev<r>
|
|
\\
|
|
\\<b><green>Production<r><d> - build optimized assets<r>
|
|
\\
|
|
\\ <green><b>bun run build<r>
|
|
\\
|
|
\\<b><orange>Component<r><d> - your main react component<r>
|
|
\\
|
|
\\ <orange><b>{s}<r>
|
|
\\
|
|
\\ <blue>Happy bunning! 🐇<r>
|
|
\\
|
|
\\
|
|
, .{ this.template.label(), main_file_name });
|
|
}
|
|
};
|
|
};
|
|
|
|
const SourceFileProjectGenerator = @This();
|