diff --git a/docs/bundler/executables.md b/docs/bundler/executables.md index c477e6a82c..51c8963a54 100644 --- a/docs/bundler/executables.md +++ b/docs/bundler/executables.md @@ -196,6 +196,8 @@ import icon from "./icon.png" with { type: "file" }; import { file } from "bun"; const bytes = await file(icon).arrayBuffer(); +// await fs.promises.readFile(icon) +// fs.readFileSync(icon) ``` ### Embed SQLite databases diff --git a/docs/bundler/loaders.md b/docs/bundler/loaders.md index 2e399c9849..06cd4e01d8 100644 --- a/docs/bundler/loaders.md +++ b/docs/bundler/loaders.md @@ -192,8 +192,6 @@ Otherwise, the database to embed is copied into the `outdir` with a hashed filen ### `html` -**HTML loader**. Default for `.html` after Bun v1.2.0. - The html loader processes HTML files and bundles any referenced assets. It will: - Bundle and hash referenced JavaScript files (` + + diff --git a/src/create/projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_NAME.html b/src/create/projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_NAME.html new file mode 100644 index 0000000000..6fe77fb57a --- /dev/null +++ b/src/create/projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_NAME.html @@ -0,0 +1,14 @@ + + + + + + REPLACE_ME_WITH_YOUR_APP_FILE_NAME | Powered by Bun + + + + +
+ + + diff --git a/src/create/projects/react-shadcn-spa/bunfig.toml b/src/create/projects/react-shadcn-spa/bunfig.toml new file mode 100644 index 0000000000..55db1c435b --- /dev/null +++ b/src/create/projects/react-shadcn-spa/bunfig.toml @@ -0,0 +1,2 @@ +[serve.static] +plugins = ["bun-plugin-tailwind"] \ No newline at end of file diff --git a/src/create/projects/react-shadcn-spa/components.json b/src/create/projects/react-shadcn-spa/components.json new file mode 100644 index 0000000000..c196761cdb --- /dev/null +++ b/src/create/projects/react-shadcn-spa/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "styles/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/src/create/projects/react-shadcn-spa/lib/utils.ts b/src/create/projects/react-shadcn-spa/lib/utils.ts new file mode 100644 index 0000000000..a5ef193506 --- /dev/null +++ b/src/create/projects/react-shadcn-spa/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/create/projects/react-shadcn-spa/package.json b/src/create/projects/react-shadcn-spa/package.json new file mode 100644 index 0000000000..bc1f1f35bc --- /dev/null +++ b/src/create/projects/react-shadcn-spa/package.json @@ -0,0 +1,9 @@ +{ + "name": "react-tailwind-spa", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "bun './**/*.html'", + "build": "bun 'REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts'" + } +} diff --git a/src/create/projects/react-shadcn-spa/styles/globals.css b/src/create/projects/react-shadcn-spa/styles/globals.css new file mode 100644 index 0000000000..dd34342a71 --- /dev/null +++ b/src/create/projects/react-shadcn-spa/styles/globals.css @@ -0,0 +1,144 @@ +@import "tailwindcss"; + +@plugin "tailwindcss-animate"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: hsl(0 0% 100%); + --foreground: hsl(240 10% 3.9%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(240 10% 3.9%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(240 10% 3.9%); + --primary: hsl(240 5.9% 10%); + --primary-foreground: hsl(0 0% 98%); + --secondary: hsl(240 4.8% 95.9%); + --secondary-foreground: hsl(240 5.9% 10%); + --muted: hsl(240 4.8% 95.9%); + --muted-foreground: hsl(240 3.8% 46.1%); + --accent: hsl(240 4.8% 95.9%); + --accent-foreground: hsl(240 5.9% 10%); + --destructive: hsl(0 84.2% 60.2%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 5.9% 90%); + --input: hsl(240 5.9% 90%); + --ring: hsl(240 10% 3.9%); + --chart-1: hsl(12 76% 61%); + --chart-2: hsl(173 58% 39%); + --chart-3: hsl(197 37% 24%); + --chart-4: hsl(43 74% 66%); + --chart-5: hsl(27 87% 67%); + --radius: 0.6rem; + --sidebar-background: hsl(0 0% 98%); + --sidebar-foreground: hsl(240 5.3% 26.1%); + --sidebar-primary: hsl(240 5.9% 10%); + --sidebar-primary-foreground: hsl(0 0% 98%); + --sidebar-accent: hsl(240 4.8% 95.9%); + --sidebar-accent-foreground: hsl(240 5.9% 10%); + --sidebar-border: hsl(220 13% 91%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); +} + +.dark { + --background: hsl(240 10% 3.9%); + --foreground: hsl(0 0% 98%); + --card: hsl(240 10% 3.9%); + --card-foreground: hsl(0 0% 98%); + --popover: hsl(240 10% 3.9%); + --popover-foreground: hsl(0 0% 98%); + --primary: hsl(0 0% 98%); + --primary-foreground: hsl(240 5.9% 10%); + --secondary: hsl(240 3.7% 15.9%); + --secondary-foreground: hsl(0 0% 98%); + --muted: hsl(240 3.7% 15.9%); + --muted-foreground: hsl(240 5% 64.9%); + --accent: hsl(240 3.7% 15.9%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(0 62.8% 30.6%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 3.7% 15.9%); + --input: hsl(240 3.7% 15.9%); + --ring: hsl(240 4.9% 83.9%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); + --sidebar-background: hsl(240 5.9% 10%); + --sidebar-foreground: hsl(240 4.8% 95.9%); + --sidebar-primary: hsl(224.3 76.3% 48%); + --sidebar-primary-foreground: hsl(0 0% 100%); + --sidebar-accent: hsl(240 3.7% 15.9%); + --sidebar-accent-foreground: hsl(240 4.8% 95.9%); + --sidebar-border: hsl(240 3.7% 15.9%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar-background); + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/src/create/projects/react-shadcn-spa/styles/index.css b/src/create/projects/react-shadcn-spa/styles/index.css new file mode 100644 index 0000000000..465ba76d34 --- /dev/null +++ b/src/create/projects/react-shadcn-spa/styles/index.css @@ -0,0 +1 @@ +@import "../styles/globals.css"; diff --git a/src/create/projects/react-shadcn-spa/tsconfig.json b/src/create/projects/react-shadcn-spa/tsconfig.json new file mode 100644 index 0000000000..2c4128af6b --- /dev/null +++ b/src/create/projects/react-shadcn-spa/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "module": "preserve", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + }, + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/src/create/projects/react-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css b/src/create/projects/react-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css new file mode 100644 index 0000000000..15c3b8c346 --- /dev/null +++ b/src/create/projects/react-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css @@ -0,0 +1,25 @@ +* { + box-sizing: border-box; +} + +:root { + --font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen; + --mono-font-family: "Fira Code", "Hack", Menlo, Monaco, "Lucida Console", "Liberation Mono", "Courier New", monospace; +} + +body { + margin: 0; + padding: 0; + + font-family: var(--font-family); +} + +#root { + width: 100%; + height: 100%; +} + +code, +pre { + font-family: var(--mono-font-family); +} diff --git a/src/create/projects/react-spa/package.json b/src/create/projects/react-spa/package.json new file mode 100644 index 0000000000..638cc803e9 --- /dev/null +++ b/src/create/projects/react-spa/package.json @@ -0,0 +1,9 @@ +{ + "name": "react-spa", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "bun './**/*.html'", + "build": "bun build --target=browser REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html --outdir dist" + } +} diff --git a/src/create/projects/react-tailwind-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css b/src/create/projects/react-tailwind-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css new file mode 100644 index 0000000000..f1d8c73cdc --- /dev/null +++ b/src/create/projects/react-tailwind-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/src/css/error.zig b/src/css/error.zig index 67152e7cfd..4f5cbd4029 100644 --- a/src/css/error.zig +++ b/src/css/error.zig @@ -77,12 +77,12 @@ pub fn Err(comptime T: type) type { }; } - pub fn addToLogger(this: @This(), log: *logger.Log, source: *const logger.Source) !void { + pub fn addToLogger(this: @This(), log: *logger.Log, source: *const logger.Source, allocator: std.mem.Allocator) !void { try log.addMsg(.{ .kind = .err, .data = .{ - .location = if (this.loc) |*loc| try loc.toLocation(source, log.msgs.allocator) else null, - .text = try std.fmt.allocPrint(log.msgs.allocator, "{}", .{this.kind}), + .location = if (this.loc) |*loc| try loc.toLocation(source, allocator) else null, + .text = try std.fmt.allocPrint(allocator, "{}", .{this.kind}), }, }); diff --git a/src/install/install.zig b/src/install/install.zig index f5a2688715..ce8ad53dae 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -2844,12 +2844,12 @@ pub const PackageManager = struct { const package_name = active_lifecycle_script_running_for_the_longest_amount_of_time.package_name; if (!(package_name.len > 1 and package_name[package_name.len - 1] == 's')) { - Output.warn("{s}'s postinstall has costed you {}\n", .{ + Output.warn("{s}'s postinstall cost you {}\n", .{ package_name, bun.fmt.fmtDurationOneDecimal(time_running), }); } else { - Output.warn("{s}' postinstall has costed you {}\n", .{ + Output.warn("{s}' postinstall cost you {}\n", .{ package_name, bun.fmt.fmtDurationOneDecimal(time_running), }); @@ -7334,6 +7334,7 @@ pub const PackageManager = struct { base = registry; } } + if (base.url.len == 0) base.url = Npm.Registry.default_url; this.scope = try Npm.Registry.Scope.fromAPI("", base, allocator, env); defer { @@ -7541,6 +7542,9 @@ pub const PackageManager = struct { } if (maybe_cli) |cli| { + this.do.analyze = cli.analyze; + this.enable.only_missing = cli.only_missing or cli.analyze; + if (cli.registry.len > 0) { this.scope.url = URL.parse(cli.registry); } @@ -7727,6 +7731,7 @@ pub const PackageManager = struct { summary: bool = true, trust_dependencies_from_args: bool = false, update_to_latest: bool = false, + analyze: bool = false, }; pub const Enable = packed struct { @@ -7743,6 +7748,7 @@ pub const PackageManager = struct { force_install: bool = false, exact_versions: bool = false, + only_missing: bool = false, }; }; @@ -8157,7 +8163,7 @@ pub const PackageManager = struct { /// if options.add_trusted_dependencies is true, gets list from PackageManager.trusted_deps_to_add_to_package_json pub fn edit( manager: *PackageManager, - updates: []UpdateRequest, + updates: *[]UpdateRequest, current_package_json: *Expr, dependency_list: string, options: EditOptions, @@ -8171,6 +8177,7 @@ pub const PackageManager = struct { const allocator = manager.allocator; var remaining = updates.len; var replacing: usize = 0; + const only_add_missing = manager.options.enable.only_missing; // There are three possible scenarios here // 1. There is no "dependencies" (or equivalent list) or it is empty @@ -8201,61 +8208,74 @@ pub const PackageManager = struct { } } } + { + var i: usize = 0; + loop: while (i < updates.len) { + var request = &updates.*[i]; + inline for ([_]string{ "dependencies", "devDependencies", "optionalDependencies", "peerDependencies" }) |list| { + if (current_package_json.asProperty(list)) |query| { + if (query.expr.data == .e_object) { + const name = if (request.is_aliased) + request.name + else + request.version.literal.slice(request.version_buf); - for (updates) |*request| { - inline for ([_]string{ "dependencies", "devDependencies", "optionalDependencies", "peerDependencies" }) |list| { - if (current_package_json.asProperty(list)) |query| { - if (query.expr.data == .e_object) { - const name = if (request.is_aliased) - request.name - else - request.version.literal.slice(request.version_buf); + if (query.expr.asProperty(name)) |value| { + if (value.expr.data == .e_string) { + if (request.package_id != invalid_package_id and strings.eqlLong(list, dependency_list, true)) { + replacing += 1; + } else { + if (manager.subcommand == .update and options.before_install) add_packages_to_update: { + const version_literal = try value.expr.asStringCloned(allocator) orelse break :add_packages_to_update; + var tag = Dependency.Version.Tag.infer(version_literal); - if (query.expr.asProperty(name)) |value| { - if (value.expr.data == .e_string) { - if (request.package_id != invalid_package_id and strings.eqlLong(list, dependency_list, true)) { - replacing += 1; - } else { - if (manager.subcommand == .update and options.before_install) add_packages_to_update: { - const version_literal = try value.expr.asStringCloned(allocator) orelse break :add_packages_to_update; - var tag = Dependency.Version.Tag.infer(version_literal); + if (tag != .npm and tag != .dist_tag) break :add_packages_to_update; - if (tag != .npm and tag != .dist_tag) break :add_packages_to_update; + const entry = manager.updating_packages.getOrPut(allocator, name) catch bun.outOfMemory(); - const entry = manager.updating_packages.getOrPut(allocator, name) catch bun.outOfMemory(); + // first come, first serve + if (entry.found_existing) break :add_packages_to_update; - // first come, first serve - if (entry.found_existing) break :add_packages_to_update; - - var is_alias = false; - if (strings.hasPrefixComptime(strings.trim(version_literal, &strings.whitespace_chars), "npm:")) { - if (strings.lastIndexOfChar(version_literal, '@')) |at_index| { - tag = Dependency.Version.Tag.infer(version_literal[at_index + 1 ..]); - if (tag != .npm and tag != .dist_tag) break :add_packages_to_update; - is_alias = true; + var is_alias = false; + if (strings.hasPrefixComptime(strings.trim(version_literal, &strings.whitespace_chars), "npm:")) { + if (strings.lastIndexOfChar(version_literal, '@')) |at_index| { + tag = Dependency.Version.Tag.infer(version_literal[at_index + 1 ..]); + if (tag != .npm and tag != .dist_tag) break :add_packages_to_update; + is_alias = true; + } } - } - entry.value_ptr.* = .{ - .original_version_literal = version_literal, - .is_alias = is_alias, - .original_version = null, - }; - } - request.e_string = value.expr.data.e_string; - remaining -= 1; - } - } - break; - } else { - if (request.version.tag == .github or request.version.tag == .git) { - for (query.expr.data.e_object.properties.slice()) |item| { - if (item.value) |v| { - const url = request.version.literal.slice(request.version_buf); - if (v.data == .e_string and v.data.e_string.eql(string, url)) { - request.e_string = v.data.e_string; + entry.value_ptr.* = .{ + .original_version_literal = version_literal, + .is_alias = is_alias, + .original_version = null, + }; + } + if (!only_add_missing) { + request.e_string = value.expr.data.e_string; remaining -= 1; - break; + } else { + if (i < updates.*.len - 1) { + updates.*[i] = updates.*[updates.*.len - 1]; + } + + updates.*.len -= 1; + remaining -= 1; + continue :loop; + } + } + } + break; + } else { + if (request.version.tag == .github or request.version.tag == .git) { + for (query.expr.data.e_object.properties.slice()) |item| { + if (item.value) |v| { + const url = request.version.literal.slice(request.version_buf); + if (v.data == .e_string and v.data.e_string.eql(string, url)) { + request.e_string = v.data.e_string; + remaining -= 1; + break; + } } } } @@ -8263,6 +8283,7 @@ pub const PackageManager = struct { } } } + i += 1; } } } @@ -8324,7 +8345,7 @@ pub const PackageManager = struct { break :brk deps; }; - outer: for (updates) |*request| { + outer: for (updates.*) |*request| { if (request.e_string != null) continue; defer if (comptime Environment.allow_assert) bun.assert(request.e_string != null); @@ -8491,7 +8512,7 @@ pub const PackageManager = struct { } const resolutions = if (!options.before_install) manager.lockfile.packages.items(.resolution) else &.{}; - for (updates) |*request| { + for (updates.*) |*request| { if (request.e_string) |e_string| { if (request.package_id >= resolutions.len or resolutions[request.package_id].tag == .uninitialized) { e_string.data = uninitialized: { @@ -9610,6 +9631,8 @@ pub const PackageManager = struct { clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable, clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable, clap.parseParam("--filter ... Install packages for the matching workspaces") catch unreachable, + clap.parseParam("-a, --analyze Analyze & install all dependencies of files passed as arguments recursively (using Bun's bundler)") catch unreachable, + clap.parseParam("--only-missing Only add dependencies to package.json if they are not already present") catch unreachable, clap.parseParam(" ... ") catch unreachable, }); @@ -9632,6 +9655,8 @@ pub const PackageManager = struct { clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable, clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable, clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable, + clap.parseParam("-a, --analyze Recursively analyze & install dependencies of files passed as arguments (using Bun's bundler)") catch unreachable, + clap.parseParam("--only-missing Only add dependencies to package.json if they are not already present") catch unreachable, clap.parseParam(" ... \"name\" or \"name@version\" of package(s) to install") catch unreachable, }); @@ -9688,7 +9713,8 @@ pub const PackageManager = struct { config: ?string = null, network_concurrency: ?u16 = null, backend: ?PackageInstall.Method = null, - + analyze: bool = false, + only_missing: bool = false, positionals: []const string = &[_]string{}, yarn: bool = false, @@ -10201,6 +10227,8 @@ pub const PackageManager = struct { cli.optional = args.flag("--optional"); cli.peer = args.flag("--peer"); cli.exact = args.flag("--exact"); + cli.analyze = args.flag("--analyze"); + cli.only_missing = args.flag("--only-missing"); } if (args.option("--concurrent-scripts")) |concurrency| { @@ -10275,6 +10303,11 @@ pub const PackageManager = struct { Global.crash(); } + if (cli.analyze and cli.positionals.len == 0) { + Output.errGeneric("Missing script(s) to analyze. Pass paths to scripts to analyze their dependencies and add any missing ones to the lockfile.\n", .{}); + Global.crash(); + } + return cli; } }; @@ -10507,9 +10540,59 @@ pub const PackageManager = struct { ctx: Command.Context, subcommand: Subcommand, ) !void { - const cli = switch (subcommand) { + var cli = switch (subcommand) { inline else => |cmd| try PackageManager.CommandLineArguments.parse(ctx.allocator, cmd), }; + + // The way this works: + // 1. Run the bundler on source files + // 2. Rewrite positional arguments to act identically to the developer + // typing in the dependency names + // 3. Run the install command + if (cli.analyze) { + const Analyzer = struct { + ctx: Command.Context, + cli: *PackageManager.CommandLineArguments, + subcommand: Subcommand, + pub fn onAnalyze( + this: *@This(), + result: *bun.bundle_v2.BundleV2.DependenciesScanner.Result, + ) anyerror!void { + // TODO: add separate argument that makes it so positionals[1..] is not done and instead the positionals are passed + var positionals = bun.default_allocator.alloc(string, result.dependencies.keys().len + 1) catch bun.outOfMemory(); + positionals[0] = "add"; + bun.copy(string, positionals[1..], result.dependencies.keys()); + this.cli.positionals = positionals; + + try updatePackageJSONAndInstallAndCLI(this.ctx, this.subcommand, this.cli.*); + + Global.exit(0); + } + }; + var analyzer = Analyzer{ + .ctx = ctx, + .cli = &cli, + .subcommand = subcommand, + }; + var fetcher = bun.bundle_v2.BundleV2.DependenciesScanner{ + .ctx = &analyzer, + .entry_points = cli.positionals[1..], + .onFetch = @ptrCast(&Analyzer.onAnalyze), + }; + + // This runs the bundler. + try bun.CLI.BuildCommand.exec(bun.CLI.Command.get(), &fetcher); + return; + } + + return updatePackageJSONAndInstallAndCLI(ctx, subcommand, cli); + } + + fn updatePackageJSONAndInstallAndCLI( + ctx: Command.Context, + subcommand: Subcommand, + cli: CommandLineArguments, + ) !void { var manager, const original_cwd = init(ctx, cli, subcommand) catch |err| brk: { if (err == error.MissingPackageJSON) { switch (subcommand) { @@ -10707,23 +10790,40 @@ pub const PackageManager = struct { } } - const updates: []UpdateRequest = if (manager.subcommand == .@"patch-commit" or manager.subcommand == .patch) + return try updatePackageJSONAndInstallWithManagerWithUpdatesAndUpdateRequests( + manager, + ctx, + original_cwd, + manager.options.positionals[1..], + &update_requests, + log_level, + ); + } + + fn updatePackageJSONAndInstallWithManagerWithUpdatesAndUpdateRequests( + manager: *PackageManager, + ctx: Command.Context, + original_cwd: string, + positionals: []const string, + update_requests: *UpdateRequest.Array, + comptime log_level: Options.LogLevel, + ) !void { + var updates: []UpdateRequest = if (manager.subcommand == .@"patch-commit" or manager.subcommand == .patch) &[_]UpdateRequest{} else - UpdateRequest.parse(ctx.allocator, manager, ctx.log, manager.options.positionals[1..], &update_requests, manager.subcommand); + UpdateRequest.parse(ctx.allocator, manager, ctx.log, positionals, update_requests, manager.subcommand); try manager.updatePackageJSONAndInstallWithManagerWithUpdates( ctx, - updates, + &updates, manager.subcommand, original_cwd, log_level, ); } - fn updatePackageJSONAndInstallWithManagerWithUpdates( manager: *PackageManager, ctx: Command.Context, - updates: []UpdateRequest, + updates: *[]UpdateRequest, subcommand: Subcommand, original_cwd: string, comptime log_level: Options.LogLevel, @@ -10800,7 +10900,7 @@ pub const PackageManager = struct { .remove => { // if we're removing, they don't have to specify where it is installed in the dependencies list // they can even put it multiple times and we will just remove all of them - for (updates) |request| { + for (updates.*) |request| { inline for ([_]string{ "dependencies", "devDependencies", "optionalDependencies", "peerDependencies" }) |list| { if (current_package_json.root.asProperty(list)) |query| { if (query.expr.data == .e_object) { @@ -10892,7 +10992,12 @@ pub const PackageManager = struct { } manager.to_update = subcommand == .update; - manager.update_requests = updates; + + { + // Incase it's a pointer to self. Avoid RLS. + const cloned = updates.*; + manager.update_requests = cloned; + } var buffer_writer = try JSPrinter.BufferWriter.init(manager.allocator); try buffer_writer.buffer.list.ensureTotalCapacity(manager.allocator, current_package_json.source.contents.len + 1); @@ -10996,7 +11101,7 @@ pub const PackageManager = struct { try manager.installWithManager(ctx, root_package_json_source, original_cwd, log_level); if (subcommand == .update or subcommand == .add or subcommand == .link) { - for (updates) |request| { + for (updates.*) |request| { if (request.failed) { Global.exit(1); return; @@ -11085,7 +11190,7 @@ pub const PackageManager = struct { bun.copy(u8, &node_modules_buf, "node_modules" ++ std.fs.path.sep_str); const offset_buf = node_modules_buf["node_modules/".len..]; const name_hashes = manager.lockfile.packages.items(.name_hash); - for (updates) |request| { + for (updates.*) |request| { // If the package no longer exists in the updated lockfile, delete the directory // This is not thorough. // It does not handle nested dependencies @@ -12223,8 +12328,48 @@ pub const PackageManager = struct { pub var package_json_cwd: string = ""; pub fn install(ctx: Command.Context) !void { - const cli = try CommandLineArguments.parse(ctx.allocator, .install); + var cli = try CommandLineArguments.parse(ctx.allocator, .install); + // The way this works: + // 1. Run the bundler on source files + // 2. Rewrite positional arguments to act identically to the developer + // typing in the dependency names + // 3. Run the install command + if (cli.analyze) { + const Analyzer = struct { + ctx: Command.Context, + cli: *CommandLineArguments, + pub fn onAnalyze(this: *@This(), result: *bun.bundle_v2.BundleV2.DependenciesScanner.Result) anyerror!void { + // TODO: add separate argument that makes it so positionals[1..] is not done and instead the positionals are passed + var positionals = bun.default_allocator.alloc(string, result.dependencies.keys().len + 1) catch bun.outOfMemory(); + positionals[0] = "install"; + bun.copy(string, positionals[1..], result.dependencies.keys()); + this.cli.positionals = positionals; + + try installWithCLI(this.ctx, this.cli.*); + + Global.exit(0); + } + }; + var analyzer = Analyzer{ + .ctx = ctx, + .cli = &cli, + }; + + var fetcher = bun.bundle_v2.BundleV2.DependenciesScanner{ + .ctx = &analyzer, + .entry_points = cli.positionals[1..], + .onFetch = @ptrCast(&Analyzer.onAnalyze), + }; + + try bun.CLI.BuildCommand.exec(bun.CLI.Command.get(), &fetcher); + return; + } + + return installWithCLI(ctx, cli); + } + + pub fn installWithCLI(ctx: Command.Context, cli: CommandLineArguments) !void { const subcommand: Subcommand = if (cli.positionals.len > 1) .add else .install; // TODO(dylan-conway): print `bun install ` or `bun add ` before logs from `init`. @@ -15247,13 +15392,15 @@ pub const PackageManager = struct { } else if (install_summary.skipped > 0 and install_summary.fail == 0 and this.update_requests.len == 0) { const count = @as(PackageID, @truncate(this.lockfile.packages.len)); if (count != install_summary.skipped) { - Output.pretty("Checked {d} install{s} across {d} package{s} (no changes) ", .{ - install_summary.skipped, - if (install_summary.skipped == 1) "" else "s", - count, - if (count == 1) "" else "s", - }); - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + if (!this.options.enable.only_missing) { + Output.pretty("Checked {d} install{s} across {d} package{s} (no changes) ", .{ + install_summary.skipped, + if (install_summary.skipped == 1) "" else "s", + count, + if (count == 1) "" else "s", + }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + } printed_timestamp = true; printBlockedPackagesInfo(install_summary, this.options.global); } else { diff --git a/src/install/lifecycle_script_runner.zig b/src/install/lifecycle_script_runner.zig index 52f10c548c..0ba772b9df 100644 --- a/src/install/lifecycle_script_runner.zig +++ b/src/install/lifecycle_script_runner.zig @@ -146,8 +146,7 @@ pub const LifecycleScriptSubprocess = struct { const combined_script: [:0]u8 = copy_script.items[0 .. copy_script.items.len - 1 :0]; if (this.foreground and this.manager.options.log_level != .silent) { - Output.prettyError("$ {s}\n", .{combined_script}); - Output.flush(); + Output.command(combined_script); } else if (manager.scripts_node) |scripts_node| { manager.setNodeName( scripts_node, diff --git a/src/js/internal/html.ts b/src/js/internal/html.ts index d1f2f9cf67..f054afd4f1 100644 --- a/src/js/internal/html.ts +++ b/src/js/internal/html.ts @@ -75,12 +75,19 @@ yourself with Bun.serve(). for (const file of glob.scanSync(cwd)) { let resolved = path.resolve(cwd, file); + if (resolved.includes(path.sep + "node_modules" + path.sep)) { + continue; + } try { resolved = Bun.resolveSync(resolved, cwd); } catch { resolved = Bun.resolveSync("./" + resolved, cwd); } + if (resolved.includes(path.sep + "node_modules" + path.sep)) { + continue; + } + args.push(resolved); } } else { @@ -91,6 +98,10 @@ yourself with Bun.serve(). resolved = Bun.resolveSync("./" + arg, cwd); } + if (resolved.includes(path.sep + "node_modules" + path.sep)) { + continue; + } + args.push(resolved); } @@ -103,6 +114,10 @@ yourself with Bun.serve(). throw new Error("No HTML files found matching " + JSON.stringify(Bun.main)); } + args.sort((a, b) => { + return a.localeCompare(b); + }); + // Add cwd to find longest common path let needsPop = false; if (args.length === 1) { @@ -181,7 +196,7 @@ yourself with Bun.serve(). // If you're only providing one entry point, then match everything to it. // (except for assets, which have higher precedence) - if (htmlImports.length === 1 && servePaths[0] === "") { + if (htmlImports.length === 1) { servePaths[0] = "*"; } @@ -247,13 +262,28 @@ yourself with Bun.serve(). const elapsed = (performance.now() - initial).toFixed(2); const enableANSIColors = Bun.enableANSIColors; function printInitialMessage(isFirst: boolean) { + let pathnameToPrint; + if (servePaths.length === 1) { + pathnameToPrint = servePaths[0]; + } else { + const indexRoute = servePaths.find(a => { + return a === "index" || a === "" || a === "/"; + }); + pathnameToPrint = indexRoute !== undefined ? indexRoute : servePaths[0]; + } + + pathnameToPrint ||= "/"; + if (pathnameToPrint === "*") { + pathnameToPrint = "/"; + } + if (enableANSIColors) { let topLine = `${server.development ? "\x1b[34;7m DEV \x1b[0m " : ""}\x1b[1;34m\x1b[5mBun\x1b[0m \x1b[1;34mv${Bun.version}\x1b[0m`; if (isFirst) { topLine += ` \x1b[2mready in\x1b[0m \x1b[1m${elapsed}\x1b[0m ms`; } console.log(topLine + "\n"); - console.log(`\x1b[1;34mโžœ\x1b[0m \x1b[36m${server!.url.href}\x1b[0m`); + console.log(`\x1b[1;34mโžœ\x1b[0m \x1b[36m${new URL(pathnameToPrint, server!.url)}\x1b[0m`); } else { let topLine = `Bun v${Bun.version}`; if (isFirst) { @@ -263,7 +293,7 @@ yourself with Bun.serve(). topLine += ` ready in ${elapsed} ms`; } console.log(topLine + "\n"); - console.log(`url: ${server!.url.href}`); + console.log(`url: ${new URL(pathnameToPrint, server!.url)}`); } if (htmlImports.length > 1 || (servePaths[0] !== "" && servePaths[0] !== "*")) { console.log("\nRoutes:"); diff --git a/src/options.zig b/src/options.zig index 5388884980..a8338ad524 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1590,6 +1590,8 @@ pub const BundleOptions = struct { /// "react-jsx" or "react-jsx-dev-runtime") force_node_env: ForceNodeEnv = .unspecified, + ignore_module_resolution_errors: bool = false, + pub const ForceNodeEnv = enum { unspecified, development, diff --git a/src/output.zig b/src/output.zig index 5f793d4c7b..86c0976492 100644 --- a/src/output.zig +++ b/src/output.zig @@ -997,6 +997,41 @@ pub fn prettyErrorln(comptime fmt: string, args: anytype) void { prettyWithPrinter(fmt, args, printErrorln, .stderr); } +/// Pretty-print a command that will be run. +/// $ bun run foo +fn printCommand(argv: anytype, comptime destination: Destination) void { + const prettyFn = if (destination == .stdout) pretty else prettyError; + const printFn = if (destination == .stdout) print else printError; + switch (@TypeOf(argv)) { + [][:0]const u8, []const []const u8, []const []u8, [][]const u8 => { + prettyFn("$ ", .{}); + printFn("{s}", .{argv[0]}); + if (argv.len > 1) { + for (argv[1..]) |arg| { + printFn(" {s}", .{arg}); + } + } + prettyFn("\n", .{}); + }, + []const u8, []u8, [:0]const u8, [:0]u8 => { + prettyFn("$ {s}\n", .{argv}); + }, + else => { + @compileLog(argv); + @compileError("command() was given unsupported type: " ++ @typeName(@TypeOf(argv))); + }, + } + flush(); +} + +pub fn commandOut(argv: anytype) void { + printCommand(argv, .stdout); +} + +pub fn command(argv: anytype) void { + printCommand(argv, .stderr); +} + pub const Destination = enum(u8) { stderr, stdout, diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 365d1acda0..7f85bb7e78 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -190,8 +190,12 @@ pub inline fn containsAny(in: anytype, target: string) bool { /// a folder name. Therefore, the name can't contain any non-URL-safe /// characters. pub fn isNPMPackageName(target: string) bool { - if (target.len == 0) return false; if (target.len > 214) return false; + return isNPMPackageNameIgnoreLength(target); +} + +pub fn isNPMPackageNameIgnoreLength(target: string) bool { + if (target.len == 0) return false; const scoped = switch (target[0]) { // Old packages may have capital letters diff --git a/src/sys.zig b/src/sys.zig index 1062fcffbc..7221d32973 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -1671,7 +1671,7 @@ pub fn openatWindowsA( pub fn openatOSPath(dirfd: bun.FileDescriptor, file_path: bun.OSPathSliceZ, flags: i32, perm: bun.Mode) Maybe(bun.FileDescriptor) { if (comptime Environment.isMac) { // https://opensource.apple.com/source/xnu/xnu-7195.81.3/libsyscall/wrappers/open-base.c - const rc = syscall.@"openat$NOCANCEL"(dirfd.cast(), file_path.ptr, @as(c_uint, @intCast(flags)), @as(c_int, @intCast(perm))); + const rc = syscall.@"openat$NOCANCEL"(dirfd.cast(), file_path.ptr, @bitCast(bun.O.toPacked(flags)), perm); if (comptime Environment.allow_assert) log("openat({}, {s}, {d}) = {d}", .{ dirfd, bun.sliceTo(file_path, 0), flags, rc }); @@ -3161,81 +3161,13 @@ pub fn faccessat(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { pub fn directoryExistsAt(dir: anytype, subpath: anytype) JSC.Maybe(bool) { const dir_fd = bun.toFD(dir); - if (comptime Environment.isWindows) { - const wbuf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(wbuf); - const path = if (std.meta.Child(@TypeOf(subpath)) == u16) - bun.strings.toNTPath16(wbuf, subpath) + return switch (existsAtType(dir_fd, subpath)) { + // + .err => |err| if (err.getErrno() == .NOENT) + .{ .result = false } else - bun.strings.toNTPath(wbuf, subpath); - - const path_len_bytes: u16 = @truncate(path.len * 2); - var nt_name = w.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @constCast(path.ptr), - }; - var attr = w.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(path)) - null - else if (dir_fd == bun.invalid_fd) - std.fs.cwd().fd - else - dir_fd.cast(), - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - var basic_info: w.FILE_BASIC_INFORMATION = undefined; - const rc = kernel32.NtQueryAttributesFile(&attr, &basic_info); - if (rc == .OBJECT_NAME_INVALID or rc == .BAD_NETWORK_PATH) { - bun.Output.warn("internal error: {s}: {}", .{ @tagName(rc), bun.fmt.fmtOSPath(path, .{}) }); - } - if (JSC.Maybe(bool).errnoSys(rc, .access)) |err| { - syslog("NtQueryAttributesFile({}, {}, O_DIRECTORY | O_RDONLY, 0) = {} {d}", .{ dir_fd, bun.fmt.fmtOSPath(path, .{}), err, rc }); - return err; - } - - const is_dir = basic_info.FileAttributes != kernel32.INVALID_FILE_ATTRIBUTES and - basic_info.FileAttributes & kernel32.FILE_ATTRIBUTE_DIRECTORY != 0 and - basic_info.FileAttributes & kernel32.FILE_ATTRIBUTE_READONLY == 0; - syslog("NtQueryAttributesFile({}, {}, O_DIRECTORY | O_RDONLY, 0) = {d}", .{ dir_fd, bun.fmt.fmtOSPath(path, .{}), @intFromBool(is_dir) }); - - return .{ .result = is_dir }; - } - - // TODO: use statx to query less information. this path is currently broken - // const have_statx = Environment.isLinux; - // if (have_statx) brk: { - // var statx: std.os.linux.Statx = undefined; - // if (Maybe(bool).errnoSys(bun.C.linux.statx( - // dir_fd.cast(), - // subpath, - // // Don't follow symlinks, don't automount, minimize permissions needed - // std.os.linux.AT.SYMLINK_NOFOLLOW | std.os.linux.AT.NO_AUTOMOUNT, - // // We only need the file type to check if it's a directory - // std.os.linux.STATX_TYPE, - // &statx, - // ), .statx)) |err| { - // switch (err.err.getErrno()) { - // .OPNOTSUPP, .NOSYS => break :brk, // Linux < 4.11 - // // truly doesn't exist. - // .NOENT => return .{ .result = false }, - // else => return err, - // } - // return err; - // } - // return .{ .result = S.ISDIR(statx.mode) }; - // } - - return switch (fstatat(dir_fd, subpath)) { - .err => |err| switch (err.getErrno()) { - .NOENT => .{ .result = false }, - else => .{ .err = err }, - }, - .result => |result| .{ .result = S.ISDIR(result.mode) }, + .{ .err = err }, + .result => |result| .{ .result = result == .directory }, }; } @@ -3331,15 +3263,19 @@ pub fn updateNonblocking(fd: bun.FileDescriptor, nonblocking: bool) Maybe(void) return Maybe(void).success; } -pub fn existsAt(fd: bun.FileDescriptor, subpath: [:0]const u8) bool { - if (comptime Environment.isPosix) { - return faccessat(fd, subpath).result; - } - +pub const ExistsAtType = enum { + file, + directory, +}; +pub fn existsAtType(fd: bun.FileDescriptor, subpath: anytype) Maybe(ExistsAtType) { if (comptime Environment.isWindows) { const wbuf = bun.WPathBufferPool.get(); defer bun.WPathBufferPool.put(wbuf); - const path = bun.strings.toNTPath(wbuf, subpath); + const path = if (std.meta.Child(@TypeOf(subpath)) == u16) + bun.strings.toNTPath16(wbuf, subpath) + else + bun.strings.toNTPath(wbuf, subpath); + const path_len_bytes: u16 = @truncate(path.len * 2); var nt_name = w.UNICODE_STRING{ .Length = path_len_bytes, @@ -3361,9 +3297,9 @@ pub fn existsAt(fd: bun.FileDescriptor, subpath: [:0]const u8) bool { }; var basic_info: w.FILE_BASIC_INFORMATION = undefined; const rc = kernel32.NtQueryAttributesFile(&attr, &basic_info); - if (JSC.Maybe(bool).errnoSysP(rc, .access, subpath)) |err| { + if (JSC.Maybe(bool).errnoSys(rc, .access)) |err| { syslog("NtQueryAttributesFile({}, O_RDONLY, 0) = {}", .{ bun.fmt.fmtOSPath(path, .{}), err }); - return false; + return .{ .err = err.err }; } const is_regular_file = basic_info.FileAttributes != kernel32.INVALID_FILE_ATTRIBUTES and @@ -3371,9 +3307,51 @@ pub fn existsAt(fd: bun.FileDescriptor, subpath: [:0]const u8) bool { // https://github.com/libuv/libuv/blob/eb5af8e3c0ea19a6b0196d5db3212dae1785739b/src/win/fs.c#L2144-L2146 (basic_info.FileAttributes & kernel32.FILE_ATTRIBUTE_DIRECTORY == 0 or basic_info.FileAttributes & kernel32.FILE_ATTRIBUTE_READONLY == 0); - syslog("NtQueryAttributesFile({}, O_RDONLY, 0) = {d}", .{ bun.fmt.fmtOSPath(path, .{}), @intFromBool(is_regular_file) }); - return is_regular_file; + const is_dir = basic_info.FileAttributes != kernel32.INVALID_FILE_ATTRIBUTES and + basic_info.FileAttributes & kernel32.FILE_ATTRIBUTE_DIRECTORY != 0 and + basic_info.FileAttributes & kernel32.FILE_ATTRIBUTE_READONLY == 0; + + return if (is_dir) { + syslog("NtQueryAttributesFile({}, O_RDONLY, 0) = directory", .{bun.fmt.fmtOSPath(path, .{})}); + return .{ .result = .directory }; + } else if (is_regular_file) { + syslog("NtQueryAttributesFile({}, O_RDONLY, 0) = file", .{bun.fmt.fmtOSPath(path, .{})}); + return .{ .result = .file }; + } else { + syslog("NtQueryAttributesFile({}, O_RDONLY, 0) = {d}", .{ bun.fmt.fmtOSPath(path, .{}), basic_info.FileAttributes }); + return .{ .err = bun.sys.Error.fromCode(.UNKNOWN, .access) }; + }; + } + + if (std.meta.sentinel(@TypeOf(subpath)) == null) { + const path_buf = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(path_buf); + @memcpy(path_buf, subpath); + path_buf[subpath.len] = 0; + const slice: [:0]const u8 = @ptrCast(path_buf); + return existsAtType(fd, slice); + } + + return switch (fstatat(fd, subpath)) { + .err => |err| .{ .err = err }, + .result => |result| if (S.ISDIR(result.mode)) .{ .result = .directory } else .{ .result = .file }, + }; +} + +pub fn existsAt(fd: bun.FileDescriptor, subpath: [:0]const u8) bool { + if (comptime Environment.isPosix) { + return switch (faccessat(fd, subpath)) { + .err => false, + .result => |r| r, + }; + } + + if (comptime Environment.isWindows) { + if (existsAtType(fd, subpath).asValue()) |exists_at_type| { + return exists_at_type == .file; + } + return false; } @compileError("TODO: existsAtOSPath"); diff --git a/test/cli/create/__snapshots__/create-jsx.test.ts.snap b/test/cli/create/__snapshots__/create-jsx.test.ts.snap new file mode 100644 index 0000000000..56b869570e --- /dev/null +++ b/test/cli/create/__snapshots__/create-jsx.test.ts.snap @@ -0,0 +1,231 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`development: true react spa (no tailwind) dev server 1`] = ` +" + + +index | Powered by Bun + + +

Welcome to Bun

The all-in-one JavaScript runtime & toolkit designed for speed

3xBun Bun Bun
0.5sAverage Install Time
ExtremelyNode.js Compatible

Why Choose Bun?

โšก๏ธ

Lightning Fast

Built from scratch in Zig, Bun is focused on performance and developer experience

๐ŸŽฏ

All-in-One

Bundler, test runner, and npm-compatible package manager in a single tool

๐Ÿš€

JavaScript Runtime

Drop-in replacement for Node.js with 3x faster startup time

๐Ÿ“ฆ

Package Management

Native package manager that can install dependencies up to 30x faster than npm

๐Ÿงช

Testing Made Simple

Built-in test runner with Jest-compatible API and snapshot testing

๐Ÿ”ฅ

Hot Reloading

Lightning-fast hot module replacement (HMR) for rapid development

+ +" +`; + +exports[`development: true react spa (no tailwind) dev server 2`] = ` +"create index.build.ts build +create index.css css +create index.html html +create index.client.tsx bun +create package.json npm +๐Ÿ“ฆ Auto-installing 3 detected dependencies +$ bun --only-missing install classnames react-dom@19 react@19 +bun add v*.*.* +installed classnames@*.*.* +installed react-dom@*.*.* +installed react@*.*.* +4 packages installed [*ms] +-------------------------------- +โœจ React project configured +Development - frontend dev server with hot reload +bun dev +Production - build optimized assets +bun run build +Happy bunning! ๐Ÿ‡ +Bun v*.*.* dev server ready in *.** ms +url: http://[SERVER_URL]/" +`; + +exports[`development: true react spa (tailwind) dev server 1`] = ` +" + + +index | Powered by Bun + + +

bun create for React

Start a React dev server instantly from a single component file

bun create ./MyComponent.tsx

Zero Config

Just write your React component and run. No setup needed.

Auto Dependencies

Automatically detects and installs required npm packages.

Tool Detection

Recognizes Tailwind, animations, and UI libraries automatically.

How it Works

1

Create Component

Write your React component in a .tsx file

2

Run Command

Execute bun create with your file path

3

Start Developing

Dev server starts instantly with hot reload

+ +" +`; + +exports[`development: true react spa (tailwind) dev server 2`] = ` +"create index.build.ts build +create index.css css +create index.html html +create index.client.tsx bun +create bunfig.toml bun +create package.json npm +๐Ÿ“ฆ Auto-installing 4 detected dependencies +$ bun --only-missing install tailwindcss bun-plugin-tailwind react-dom@19 react@19 +bun add v*.*.* +installed tailwindcss@*.*.* +installed bun-plugin-tailwind@*.*.* +installed react-dom@*.*.* +installed react@*.*.* +7 packages installed [*ms] +-------------------------------- +โœจ React + Tailwind project configured +Development - frontend dev server with hot reload +bun dev +Production - build optimized assets +bun run build +Happy bunning! ๐Ÿ‡ +Bun v*.*.* dev server ready in *.** ms +url: http://[SERVER_URL]/" +`; + +exports[`development: true shadcn/ui dev server 1`] = ` +"create lib/utils.ts shadcn +create src/index.css shadcn +create index.build.ts bun +create index.client.tsx bun +create index.css css +create index.html html +create styles/globals.css shadcn +create bunfig.toml bun +create package.json npm +create tsconfig.json tsc +create components.json shadcn +๐Ÿ“ฆ Auto-installing 9 detected dependencies +$ bun --only-missing install lucide-react tailwindcss bun-plugin-tailwind tailwindcss-animate class-variance-authority clsx tailwind-merge react@^18 react-dom@^18 +bun add v*.*.* +installed lucide-react@*.*.* +installed tailwindcss@*.*.* +installed bun-plugin-tailwind@*.*.* +installed tailwindcss-animate@*.*.* +installed class-variance-authority@*.*.* +installed clsx@*.*.* +installed tailwind-merge@*.*.* +installed react@*.*.* +installed react-dom@*.*.* +14 packages installed [*ms] +๐Ÿ˜Ž Setting up shadcn/ui components +$ bun x shadcn@canary add -y button badge card +- components/ui/button.tsx +- components/ui/badge.tsx +- components/ui/card.tsx +-------------------------------- +โœจ React + shadcn/ui + Tailwind project configured +Development - frontend dev server with hot reload +bun dev +Production - build optimized assets +bun run build +Happy bunning! ๐Ÿ‡ +Bun v*.*.* dev server ready in *.** ms +url: http://[SERVER_URL]/" +`; + +exports[`development: false react spa (no tailwind) dev server 1`] = ` +" + + +index | Powered by Bun + + + +

Welcome to Bun

The all-in-one JavaScript runtime & toolkit designed for speed

3xBun Bun Bun
0.5sAverage Install Time
ExtremelyNode.js Compatible

Why Choose Bun?

โšก๏ธ

Lightning Fast

Built from scratch in Zig, Bun is focused on performance and developer experience

๐ŸŽฏ

All-in-One

Bundler, test runner, and npm-compatible package manager in a single tool

๐Ÿš€

JavaScript Runtime

Drop-in replacement for Node.js with 3x faster startup time

๐Ÿ“ฆ

Package Management

Native package manager that can install dependencies up to 30x faster than npm

๐Ÿงช

Testing Made Simple

Built-in test runner with Jest-compatible API and snapshot testing

๐Ÿ”ฅ

Hot Reloading

Lightning-fast hot module replacement (HMR) for rapid development

+" +`; + +exports[`development: false react spa (no tailwind) dev server 2`] = ` +"create index.build.ts build +create index.css css +create index.html html +create index.client.tsx bun +create package.json npm +๐Ÿ“ฆ Auto-installing 3 detected dependencies +$ bun --only-missing install classnames react-dom@19 react@19 +bun add v*.*.* +installed classnames@*.*.* +installed react-dom@*.*.* +installed react@*.*.* +4 packages installed [*ms] +-------------------------------- +โœจ React project configured +Development - frontend dev server with hot reload +bun dev +Production - build optimized assets +bun run build +Happy bunning! ๐Ÿ‡ +Bun v*.*.* ready in *.** ms +url: http://[SERVER_URL]/" +`; + +exports[`development: false react spa (tailwind) dev server 1`] = ` +" + + +index | Powered by Bun + + + +

bun create for React

Start a React dev server instantly from a single component file

bun create ./MyComponent.tsx

Zero Config

Just write your React component and run. No setup needed.

Auto Dependencies

Automatically detects and installs required npm packages.

Tool Detection

Recognizes Tailwind, animations, and UI libraries automatically.

How it Works

1

Create Component

Write your React component in a .tsx file

2

Run Command

Execute bun create with your file path

3

Start Developing

Dev server starts instantly with hot reload

+" +`; + +exports[`development: false react spa (tailwind) dev server 2`] = ` +"create index.build.ts build +create index.css css +create index.html html +create index.client.tsx bun +create bunfig.toml bun +create package.json npm +๐Ÿ“ฆ Auto-installing 4 detected dependencies +$ bun --only-missing install tailwindcss bun-plugin-tailwind react-dom@19 react@19 +bun add v*.*.* +installed tailwindcss@*.*.* +installed bun-plugin-tailwind@*.*.* +installed react-dom@*.*.* +installed react@*.*.* +7 packages installed [*ms] +-------------------------------- +โœจ React + Tailwind project configured +Development - frontend dev server with hot reload +bun dev +Production - build optimized assets +bun run build +Happy bunning! ๐Ÿ‡ +Bun v*.*.* ready in *.** ms +url: http://[SERVER_URL]/" +`; + +exports[`development: false shadcn/ui dev server 1`] = ` +"create lib/utils.ts shadcn +create src/index.css shadcn +create index.build.ts bun +create index.client.tsx bun +create index.css css +create index.html html +create styles/globals.css shadcn +create bunfig.toml bun +create package.json npm +create tsconfig.json tsc +create components.json shadcn +๐Ÿ“ฆ Auto-installing 9 detected dependencies +$ bun --only-missing install lucide-react tailwindcss bun-plugin-tailwind tailwindcss-animate class-variance-authority clsx tailwind-merge react@^18 react-dom@^18 +bun add v*.*.* +installed lucide-react@*.*.* +installed tailwindcss@*.*.* +installed bun-plugin-tailwind@*.*.* +installed tailwindcss-animate@*.*.* +installed class-variance-authority@*.*.* +installed clsx@*.*.* +installed tailwind-merge@*.*.* +installed react@*.*.* +installed react-dom@*.*.* +14 packages installed [*ms] +๐Ÿ˜Ž Setting up shadcn/ui components +$ bun x shadcn@canary add -y button badge card +- components/ui/button.tsx +- components/ui/badge.tsx +- components/ui/card.tsx +-------------------------------- +โœจ React + shadcn/ui + Tailwind project configured +Development - frontend dev server with hot reload +bun dev +Production - build optimized assets +bun run build +Happy bunning! ๐Ÿ‡ +Bun v*.*.* ready in *.** ms +url: http://[SERVER_URL]/" +`; diff --git a/test/cli/create/create-jsx.test.ts b/test/cli/create/create-jsx.test.ts new file mode 100644 index 0000000000..e6c76645fc --- /dev/null +++ b/test/cli/create/create-jsx.test.ts @@ -0,0 +1,366 @@ +import "bun"; +import { expect, test, describe, beforeEach, afterAll } from "bun:test"; +import { tempDirWithFiles as tempDir, bunExe, bunEnv, isCI, isWindows } from "harness"; +import { cp, readdir } from "fs/promises"; +import path from "path"; +import puppeteer, { type Browser } from "puppeteer"; +import type { Subprocess } from "bun"; +import * as vm from "vm"; +const env = { + ...bunEnv, +}; +const baseOptions = { + dumpio: !!process.env.CI_DEBUG, + + args: [ + "--disable-gpu", + "--disable-dev-shm-usage", + "--disable-setuid-sandbox", + "--no-sandbox", + "--ignore-certificate-errors", + "--use-fake-ui-for-media-stream", + "--use-fake-device-for-media-stream", + "--disable-sync", + ], + executablePath: process.env.BROWSER_EXECUTABLE, + headless: true, +}; +let puppeteerBrowser: Browser | null = null; +async function getPuppeteerBrowser() { + if (!puppeteerBrowser) { + puppeteerBrowser = await puppeteer.launch(baseOptions); + } + return puppeteerBrowser; +} + +afterAll(async () => { + if (puppeteerBrowser) { + await puppeteerBrowser.close(); + } +}); + +async function getServerUrl(process: Subprocess, all = { text: "" }) { + // Read the port number from stdout + const decoder = new TextDecoder(); + let serverUrl = ""; + all.text = ""; + + const reader = process.stdout.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const textChunk = decoder.decode(value, { stream: true }); + all.text += textChunk; + console.log(textChunk); + + if (all.text.includes("http://")) { + serverUrl = all.text.trim(); + serverUrl = serverUrl.slice(serverUrl.indexOf("http://")); + + serverUrl = serverUrl.slice(0, serverUrl.indexOf("\n")); + if (URL.canParse(serverUrl)) { + break; + } + + serverUrl = serverUrl.slice(0, serverUrl.indexOf("/n")); + serverUrl = serverUrl.slice(0, serverUrl.lastIndexOf("/")); + serverUrl = serverUrl.trim(); + + if (URL.canParse(serverUrl)) { + break; + } + } + } + reader.releaseLock(); + + if (!serverUrl) { + throw new Error("Could not find server URL in stdout"); + } + + return serverUrl; +} + +async function checkBuildOutput(dir: string) { + const distDir = path.join(dir, "dist"); + const files = await readdir(distDir); + expect(files.some(f => f.endsWith(".js"))).toBe(true); + expect(files.some(f => f.endsWith(".html"))).toBe(true); + expect(files.some(f => f.endsWith(".css"))).toBe(true); +} + +describe.each(["true", "false"])("development: %s", developmentString => { + const development = developmentString === "true"; + const tempDirWithFiles = (name: string, files: Record) => + tempDir(name + (development ? "-dev" : "-prod"), files); + const normalizeHTML = normalizeHTMLFn(development); + const env = { + ...bunEnv, + NODE_PORT: "0", + NODE_ENV: development ? undefined : "production", + }; + + const devServerLabel = development ? " dev server" : ""; + describe("react spa (no tailwind)", async () => { + let dir: string; + beforeEach(async () => { + dir = tempDirWithFiles("react-spa-no-tailwind", { + "README.md": "Hello, world!", + }); + + await cp(path.join(__dirname, "react-spa-no-tailwind"), dir, { + recursive: true, + force: true, + }); + }); + + test.todoIf(isCI)("dev server", async () => { + await using process = Bun.spawn([bunExe(), "create", "./index.jsx"], { + cwd: dir, + env: env, + stdout: "pipe", + stdin: "ignore", + }); + const all = { text: "" }; + const serverUrl = await getServerUrl(process, all); + + try { + const browser = await getPuppeteerBrowser(); + var page = await browser.newPage(); + await page.goto(serverUrl, { waitUntil: "networkidle0" }); + + const content = await page.evaluate(() => document.documentElement.innerHTML); + + expect(normalizeHTML(content)).toMatchSnapshot(); + + expect( + all.text + .replace(/v\d+\.\d+\.\d+(?:\s*\([a-f0-9]+\))?/g, "v*.*.*") // Handle version with git hash + .replace(/\[\d+\.?\d*m?s\]/g, "[*ms]") + .replace(/@\d+\.\d+\.\d+/g, "@*.*.*") + .replace(/\d+\.\d+\s*ms/g, "*.** ms") + .replace(/^\s+/gm, "") // Remove leading spaces + .replace(/installed react(-dom)?@\d+\.\d+\.\d+/g, "installed react$1@*.*.*") // Handle react versions + .trim() + .replaceAll(serverUrl, "http://[SERVER_URL]"), + ).toMatchSnapshot(); + } finally { + process.kill(); + await Promise.resolve(page!?.close?.({ runBeforeUnload: false })); + } + }); + + test.todoIf(isWindows)("build", async () => { + { + const process = Bun.spawn([bunExe(), "create", "./index.jsx"], { + cwd: dir, + env: env, + stdout: "pipe", + stdin: "ignore", + }); + const all = { text: "" }; + const serverUrl = await getServerUrl(process, all); + process.kill(); + } + + const process = Bun.spawn([bunExe(), "run", "build"], { + cwd: dir, + env: env, + stdout: "pipe", + }); + + await process.exited; + await checkBuildOutput(dir); + }); + }); + + describe("react spa (tailwind)", async () => { + let dir: string; + beforeEach(async () => { + dir = tempDirWithFiles("react-spa-tailwind", { + "index.tsx": await Bun.file(path.join(__dirname, "tailwind.tsx")).text(), + }); + }); + + test.todoIf(isCI)("dev server", async () => { + const process = Bun.spawn([bunExe(), "create", "./index.tsx"], { + cwd: dir, + env: env, + stdout: "pipe", + stdin: "ignore", + }); + const all = { text: "" }; + const serverUrl = await getServerUrl(process, all); + console.log(serverUrl); + + try { + var page = await (await getPuppeteerBrowser()).newPage(); + await page.goto(serverUrl, { waitUntil: "networkidle0" }); + + // Check that React root exists and has Tailwind classes + const root = await page.$("#root"); + expect(root).toBeTruthy(); + + const content = await page.evaluate(() => document.documentElement.outerHTML); + expect(normalizeHTML(content)).toMatchSnapshot(); + + expect( + all.text + .replace(/v\d+\.\d+\.\d+(?:\s*\([a-f0-9]+\))?/g, "v*.*.*") + .replace(/\[\d+\.?\d*m?s\]/g, "[*ms]") + .replace(/@\d+\.\d+\.\d+/g, "@*.*.*") + .replace(/\d+\.\d+\s*ms/g, "*.** ms") + .replace(/^\s+/gm, "") + .replace(/installed (react(-dom)?|tailwindcss)@\d+\.\d+\.\d+/g, "installed $1@*.*.*") + .trim() + .replaceAll(serverUrl, "http://[SERVER_URL]"), + ).toMatchSnapshot(); + } finally { + process.kill(); + await Promise.resolve(page!?.close?.({ runBeforeUnload: false })); + } + }); + + test.todoIf(isWindows)("build", async () => { + { + const process = Bun.spawn([bunExe(), "create", "./index.tsx"], { + cwd: dir, + env: env, + stdout: "pipe", + stdin: "ignore", + }); + const all = { text: "" }; + const serverUrl = await getServerUrl(process, all); + process.kill(); + } + + const process = Bun.spawn([bunExe(), "run", "build"], { + cwd: dir, + env: env, + stdout: "pipe", + }); + + await process.exited; + await checkBuildOutput(dir); + }); + }); + + describe( + "shadcn/ui", + async () => { + let dir: string; + beforeEach(async () => { + dir = tempDirWithFiles("shadcn-ui", { + "index.tsx": await Bun.file(path.join(__dirname, "shadcn.tsx")).text(), + }); + }); + + test( + "dev server", + async () => { + const process = Bun.spawn([bunExe(), "create", "./index.tsx"], { + cwd: dir, + env: env, + stdout: "pipe", + stdin: "ignore", + }); + const all = { text: "" }; + const serverUrl = await getServerUrl(process, all); + console.log(serverUrl); + console.log(dir); + try { + var page = await (await getPuppeteerBrowser()).newPage(); + await page.goto(serverUrl, { waitUntil: "networkidle0" }); + + // Check that React root exists and has Shadcn components + const root = await page.$("#root"); + expect(root).toBeTruthy(); + + const content = await page.evaluate(() => document.documentElement.innerHTML); + expect(content).toContain("shadcn"); // Basic check for Shadcn classes + + // Check for components.json + const componentsJson = await Bun.file(path.join(dir, "components.json")).exists(); + expect(componentsJson).toBe(true); + + expect( + all.text + .replace(/v\d+\.\d+\.\d+(?:\s*\([a-f0-9]+\))?/g, "v*.*.*") + .replace(/\[\d+\.?\d*m?s\]/g, "[*ms]") + .replace(/@\d+\.\d+\.\d+/g, "@*.*.*") + .replace(/\d+\.\d+\s*ms/g, "*.** ms") + .replace(/^\s+/gm, "") + .replace( + /installed (react(-dom)?|@radix-ui\/.*|tailwindcss|class-variance-authority|clsx|lucide-react|tailwind-merge)@\d+\.\d+\.\d+/g, + "installed $1@*.*.*", + ) + .trim() + .replaceAll(serverUrl, "http://[SERVER_URL]"), + ).toMatchSnapshot(); + } finally { + process.kill(); + await Promise.resolve(page!?.close?.({ runBeforeUnload: false })); + } + }, + 1000 * 100, + ); + + test.todoIf(isWindows)("build", async () => { + { + const process = Bun.spawn([bunExe(), "create", "./index.tsx"], { + cwd: dir, + env: env, + stdout: "pipe", + stdin: "ignore", + }); + const all = { text: "" }; + const serverUrl = await getServerUrl(process, all); + process.kill(); + } + + const process = Bun.spawn([bunExe(), "run", "build"], { + cwd: dir, + env: env, + stdout: "pipe", + }); + + await process.exited; + await checkBuildOutput(dir); + }); + }, + 1000 * 100, + ); +}); + +function normalizeHTMLFn(development: boolean = true) { + return (html: string) => + html + .split("\n") + .map(line => { + // First trim the line + const trimmed = line.trim(); + if (!trimmed) return ""; + + if (!development) { + // Replace chunk hashes in stylesheet and script tags + return trimmed.replace( + /<(link rel="stylesheet" crossorigin="" href|script type="module" crossorigin="" src)="\/chunk-[a-zA-Z0-9]+\.(css|js)("><\/script>|">)/g, + (_, tagStart, ext) => { + if (ext === "css") { + return `<${tagStart}="/chunk-[HASH].css">`; + } + return `<${tagStart}="/chunk-[HASH].js">`; + }, + ); + } + + // In development mode, replace generational IDs in script/link tags + return trimmed.replace( + /<(link rel="stylesheet" href|script type="module" src)="\/_bun\/(client|asset)\/[^"]+\.(?:css|js)("><\/script>|">)/g, + (_, tagStart, path, end) => `<${tagStart}="/_bun/${path}/[GENERATION_ID]${end}`, + ); + }) + .filter(Boolean) + .join("\n") + .trim(); +} diff --git a/test/cli/create/react-spa-no-tailwind/components/Feature.jsx b/test/cli/create/react-spa-no-tailwind/components/Feature.jsx new file mode 100644 index 0000000000..0d1cd5fa9e --- /dev/null +++ b/test/cli/create/react-spa-no-tailwind/components/Feature.jsx @@ -0,0 +1,27 @@ +import React from "react"; +import classNames from "classnames"; + +export default function Feature({ icon, title, description, highlight }) { + return ( +
+
{icon}
+

{title}

+

+ {highlight ? ( + <> + {description.split(highlight).map((part, i, arr) => ( + + {part} + {i < arr.length - 1 && ( + {highlight} + )} + + ))} + + ) : ( + description + )} +

+
+ ); +} diff --git a/test/cli/create/react-spa-no-tailwind/components/Features.jsx b/test/cli/create/react-spa-no-tailwind/components/Features.jsx new file mode 100644 index 0000000000..3b5e492ae5 --- /dev/null +++ b/test/cli/create/react-spa-no-tailwind/components/Features.jsx @@ -0,0 +1,56 @@ +import React from "react"; +import Feature from "./Feature"; + +const FEATURES = [ + { + icon: "โšก๏ธ", + title: "Lightning Fast", + description: + "Built from scratch in Zig, Bun is focused on performance and developer experience", + highlight: "Zig", + }, + { + icon: "๐ŸŽฏ", + title: "All-in-One", + description: + "Bundler, test runner, and npm-compatible package manager in a single tool", + }, + { + icon: "๐Ÿš€", + title: "JavaScript Runtime", + description: "Drop-in replacement for Node.js with 3x faster startup time", + highlight: "3x faster", + }, + { + icon: "๐Ÿ“ฆ", + title: "Package Management", + description: + "Native package manager that can install dependencies up to 30x faster than npm", + highlight: "30x faster", + }, + { + icon: "๐Ÿงช", + title: "Testing Made Simple", + description: + "Built-in test runner with Jest-compatible API and snapshot testing", + }, + { + icon: "๐Ÿ”ฅ", + title: "Hot Reloading", + description: + "Lightning-fast hot module replacement (HMR) for rapid development", + }, +]; + +export default function Features() { + return ( +
+

Why Choose Bun?

+
+ {FEATURES.map((feature, index) => ( + + ))} +
+
+ ); +} diff --git a/test/cli/create/react-spa-no-tailwind/components/Footer.jsx b/test/cli/create/react-spa-no-tailwind/components/Footer.jsx new file mode 100644 index 0000000000..20d63f7966 --- /dev/null +++ b/test/cli/create/react-spa-no-tailwind/components/Footer.jsx @@ -0,0 +1,35 @@ +import React from "react"; +import classNames from "classnames"; + +const LINKS = [ + { text: "Documentation", url: "https://bun.sh/docs" }, + { text: "GitHub", url: "https://github.com/oven-sh/bun" }, + { text: "Discord", url: "https://bun.sh/discord" }, + { text: "Blog", url: "https://bun.sh/blog" }, +]; + +export default function Footer() { + return ( +
+
+
+ ๐ŸฅŸ + Built with Bun +
+ +
+
+ ); +} diff --git a/test/cli/create/react-spa-no-tailwind/components/Hero.jsx b/test/cli/create/react-spa-no-tailwind/components/Hero.jsx new file mode 100644 index 0000000000..27bd415b7d --- /dev/null +++ b/test/cli/create/react-spa-no-tailwind/components/Hero.jsx @@ -0,0 +1,48 @@ +import React from "react"; +import classNames from "classnames"; + +export default function Hero() { + return ( +
+
๐ŸฅŸ
+

+ Welcome to Bun +

+

+ The all-in-one JavaScript runtime & toolkit designed for speed +

+ +
+
+ 3x + Bun Bun Bun +
+
+ 0.5s + Average Install Time +
+
+ Extremely + Node.js Compatible +
+
+
+ ); +} diff --git a/test/cli/create/react-spa-no-tailwind/index.html b/test/cli/create/react-spa-no-tailwind/index.html new file mode 100644 index 0000000000..78c0dcd870 --- /dev/null +++ b/test/cli/create/react-spa-no-tailwind/index.html @@ -0,0 +1,12 @@ + + + + + + Bun - The Modern JavaScript Runtime + + +
+ + + diff --git a/test/cli/create/react-spa-no-tailwind/index.jsx b/test/cli/create/react-spa-no-tailwind/index.jsx new file mode 100644 index 0000000000..7f38d302b0 --- /dev/null +++ b/test/cli/create/react-spa-no-tailwind/index.jsx @@ -0,0 +1,22 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import classNames from "classnames"; +import "./styles.css"; + +import Hero from "./components/Hero"; +import Features from "./components/Features"; +import Footer from "./components/Footer"; + +function App() { + return ( +
+
+ + +
+
+
+ ); +} + +export default App; diff --git a/test/cli/create/react-spa-no-tailwind/styles.css b/test/cli/create/react-spa-no-tailwind/styles.css new file mode 100644 index 0000000000..8a143902f0 --- /dev/null +++ b/test/cli/create/react-spa-no-tailwind/styles.css @@ -0,0 +1,278 @@ +:root { + --primary-color: #fbf0ff; + --accent-color: #7c3aed; + --text-color: #1a1a1a; + --secondary-color: #4c1d95; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-700: #374151; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, sans-serif; + background: var(--primary-color); + color: var(--text-color); + line-height: 1.6; +} + +.app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + flex: 1; +} + +/* Hero Section */ +.hero { + margin: 4rem 0; + text-align: center; +} + +.logo { + font-size: 5rem; + margin-bottom: 1rem; + display: inline-block; +} + +.animate-bounce { + animation: bounce 2s infinite; +} + +@keyframes bounce { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-20px); + } +} + +h1 { + font-size: 3.5rem; + margin-bottom: 1rem; + background: linear-gradient( + 120deg, + var(--accent-color), + var(--secondary-color) + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.gradient-text { + background: linear-gradient( + 120deg, + var(--accent-color), + var(--secondary-color) + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.description { + font-size: 1.5rem; + margin-bottom: 2rem; + color: var(--gray-700); + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +/* CTA Buttons */ +.cta-buttons { + display: flex; + gap: 1rem; + justify-content: center; + margin-bottom: 3rem; +} + +.button { + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-weight: 600; + text-decoration: none; + transition: transform 0.2s, box-shadow 0.2s; +} + +.button:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.button.primary { + background: var(--accent-color); + color: white; +} + +.button.secondary { + background: white; + color: var(--accent-color); + border: 2px solid var(--accent-color); +} + +/* Stats */ +.stats { + display: flex; + justify-content: center; + gap: 3rem; + margin-top: 3rem; +} + +.stat { + text-align: center; +} + +.stat-value { + font-size: 2.5rem; + font-weight: bold; + color: var(--accent-color); + display: block; +} + +.stat-label { + color: var(--gray-700); + font-size: 1rem; +} + +/* Features Section */ +.features-section { + padding: 4rem 0; +} + +.features-section h2 { + text-align: center; + font-size: 2.5rem; + margin-bottom: 3rem; + color: var(--accent-color); +} + +.features { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; +} + +.feature { + padding: 2rem; + background: white; + border-radius: 12px; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} + +.feature:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-md); +} + +.feature-icon { + font-size: 2.5rem; + margin-bottom: 1rem; +} + +.feature h3 { + color: var(--accent-color); + margin-bottom: 1rem; + font-size: 1.5rem; +} + +.highlight { + background: linear-gradient(120deg, #7c3aed20 0%, #7c3aed10 100%); + padding: 0.2em 0.4em; + border-radius: 4px; + font-weight: bold; +} + +/* Footer */ +.footer { + background: white; + padding: 2rem 0; + margin-top: 4rem; + border-top: 1px solid var(--gray-200); +} + +.footer-content { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.footer-logo { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.logo-small { + font-size: 1.5rem; +} + +.footer-text { + font-weight: 500; + color: var(--gray-700); +} + +.footer-links { + display: flex; + gap: 2rem; +} + +.footer-link { + color: var(--gray-700); + text-decoration: none; + transition: color 0.2s; +} + +.footer-link:hover { + color: var(--accent-color); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + h1 { + font-size: 2.5rem; + } + + .description { + font-size: 1.2rem; + } + + .stats { + flex-direction: column; + gap: 2rem; + } + + .footer-content { + flex-direction: column; + gap: 1rem; + text-align: center; + } + + .footer-links { + flex-wrap: wrap; + justify-content: center; + } +} diff --git a/test/cli/create/shadcn.tsx b/test/cli/create/shadcn.tsx new file mode 100644 index 0000000000..71eb1f1faa --- /dev/null +++ b/test/cli/create/shadcn.tsx @@ -0,0 +1,105 @@ +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { CheckCircle } from "lucide-react"; + +export default function LandingPage() { + const features = [ + { + title: "Auto Dependencies", + description: + "Automatically detects and installs required dependencies for your component", + }, + { + title: "Tool Detection", + description: + "Seamlessly integrates with Tailwind CSS, shadcn/ui, and other popular tools", + }, + { + title: "Zero Config", + description: + "No setup required. Start developing instantly with hot reload enabled", + }, + ]; + + return ( +
+
+ {/* Hero Section */} +
+ + New in Bun 1.2.3 + +

+ From Component to App in + Seconds +

+

+ Start a complete dev server from a single React component. No config + needed. +

+ +
+ + +
+
+ + {/* Code Preview */} + +
+
+              
+                $ bun create ./MyComponent.tsx
+                
+ ๐Ÿ“ฆ Installing dependencies... +
+ ๐Ÿ” Detected Tailwind CSS +
+ ๐ŸŽจ Detected shadcn/ui +
โœจ Dev server running at http://localhost:3000 +
+
+
+
+ + {/* Features Grid */} +
+ {features.map((feature, index) => ( + +
+ +

{feature.title}

+
+

{feature.description}

+
+ ))} +
+ + {/* CTA Section */} +
+ +

+ Ready to streamline your React development? +

+

+ Get started with Bun's powerful component development workflow + today. +

+ +
+
+
+
+ ); +} + diff --git a/test/cli/create/tailwind.tsx b/test/cli/create/tailwind.tsx new file mode 100644 index 0000000000..ca2a550b2f --- /dev/null +++ b/test/cli/create/tailwind.tsx @@ -0,0 +1,88 @@ +export default function LandingPage() { + let copied = false; + const handleCopy = () => { + navigator.clipboard.writeText("bun create ./MyComponent.tsx"); + }; + + return ( +
+
+
+

+ bun create for React +

+

Start a React dev server instantly from a single component file

+ +
+ bun create ./MyComponent.tsx + +
+
+ +
+
+

Zero Config

+

Just write your React component and run. No setup needed.

+
+ +
+

Auto Dependencies

+

Automatically detects and installs required npm packages.

+
+ +
+

Tool Detection

+

Recognizes Tailwind, animations, and UI libraries automatically.

+
+
+ +
+

How it Works

+
+
+
1
+
+

Create Component

+

Write your React component in a .tsx file

+
+
+
+
2
+
+

Run Command

+

Execute bun create with your file path

+
+
+
+
3
+
+

Start Developing

+

Dev server starts instantly with hot reload

+
+
+
+
+ +
+

Ready to Try?

+ +
+
+
+ ); +} diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts index d2deadd510..c8ef1e2591 100644 --- a/test/cli/install/bun-add.test.ts +++ b/test/cli/install/bun-add.test.ts @@ -125,6 +125,151 @@ it("should reject missing package", async () => { ); }); +it("bun add --only-missing should not install existing package", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + + // First time: install succesfully. + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "--only-missing", "bar"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); + + { + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "bar", "--only-missing"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + }); + const out = await new Response(stdout).text(); + expect(out).not.toContain("Saved lockfile"); + expect(out).not.toContain("Installed"); + expect(out.split("\n").filter(Boolean)).toStrictEqual([ + expect.stringContaining("bun add v" + Bun.version.replaceAll("-debug", "")), + ]); + } +}); + +it("bun add --analyze should scan dependencies", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + await writeFile(join(package_dir, "entry-point.ts"), `import "./local-file.ts";`); + await writeFile(join(package_dir, "local-file.ts"), `export * from "bar";`); + console.log(package_dir); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "./entry-point.ts", "--analyze"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); + + { + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "bar", "--only-missing"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + }); + const out = await new Response(stdout).text(); + expect(out).not.toContain("Saved lockfile"); + expect(out).not.toContain("Installed"); + expect(out.split("\n").filter(Boolean)).toStrictEqual([ + expect.stringContaining("bun add v" + Bun.version.replaceAll("-debug", "")), + ]); + } +}); + for (const pathType of ["absolute", "relative"]) { it.each(["file:///", "file://", "file:/", "file:", "", "//////"])( `should accept ${pathType} file protocol with prefix "%s"`, @@ -456,7 +601,7 @@ it("should add to devDependencies with --dev", async () => { ); await access(join(package_dir, "bun.lockb")); }); -it("should add to optionalDependencies with --optional", async () => { +it.only("should add to optionalDependencies with --optional", async () => { const urls: string[] = []; setHandler(dummyRegistry(urls)); await writeFile( @@ -466,6 +611,7 @@ it("should add to optionalDependencies with --optional", async () => { version: "0.0.1", }), ); + console.log(package_dir); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "--optional", "BaR"], cwd: package_dir, diff --git a/test/js/bun/http/bun-serve-html-entry.test.ts b/test/js/bun/http/bun-serve-html-entry.test.ts index b31c9e88fb..91da22627d 100644 --- a/test/js/bun/http/bun-serve-html-entry.test.ts +++ b/test/js/bun/http/bun-serve-html-entry.test.ts @@ -1,7 +1,6 @@ -import type { Subprocess, Server } from "bun"; -import { describe, test, expect } from "bun:test"; +import type { Subprocess } from "bun"; +import { expect, test } from "bun:test"; import { bunEnv, bunExe, tempDirWithFiles } from "harness"; -import { join } from "path"; async function getServerUrl(process: Subprocess) { // Read the port number from stdout diff --git a/test/js/bun/http/bun-serve-html.test.ts b/test/js/bun/http/bun-serve-html.test.ts index f7206fc280..468506bef6 100644 --- a/test/js/bun/http/bun-serve-html.test.ts +++ b/test/js/bun/http/bun-serve-html.test.ts @@ -600,7 +600,7 @@ async function waitForServer( hostname: string; }>(); const process = Bun.spawn({ - cmd: [bunExe(), "--no-hmr", join(import.meta.dir, "bun-serve-static-fixture.js")], + cmd: [bunExe(), join(import.meta.dir, "bun-serve-static-fixture.js")], env: { ...bunEnv, NODE_ENV: undefined, diff --git a/test/js/bun/http/bun-serve-static-fixture.js b/test/js/bun/http/bun-serve-static-fixture.js index 67e499f527..aafa908e0f 100644 --- a/test/js/bun/http/bun-serve-static-fixture.js +++ b/test/js/bun/http/bun-serve-static-fixture.js @@ -2,7 +2,9 @@ import { serve } from "bun"; let server = Bun.serve({ port: 0, - development: true, + development: { + hmr: false, + }, async fetch(req) { return new Response("Hello World", { status: 404, @@ -20,7 +22,9 @@ process.on("message", async message => { server.reload({ // omit "fetch" to check we can do server.reload without passing fetch static: routes, - development: true, + development: { + hmr: false, + }, }); }); diff --git a/test/js/bun/util/bun-file.test.ts b/test/js/bun/util/bun-file.test.ts index 0d2d111c07..5071caac8e 100644 --- a/test/js/bun/util/bun-file.test.ts +++ b/test/js/bun/util/bun-file.test.ts @@ -1,11 +1,13 @@ import { test, expect } from "bun:test"; -import { tmpdirSync } from "harness"; +import { tempDirWithFiles } from "harness"; import { join } from "path"; import fsPromises from "fs/promises"; test("delete() and stat() should work with unicode paths", async () => { - const testDir = tmpdirSync(); - const filename = join(testDir, "๐ŸŒŸ.txt"); + const dir = tempDirWithFiles("delete-stat-unicode-path", { + "another-file.txt": "HEY", + }); + const filename = join(dir, "๐ŸŒŸ.txt"); expect(async () => { await Bun.file(filename).delete(); @@ -24,15 +26,18 @@ test("delete() and stat() should work with unicode paths", async () => { }); test("writer.end() should not close the fd if it does not own the fd", async () => { - const testDir = tmpdirSync(); + const dir = tempDirWithFiles("writer-end-fd", { + "tmp.txt": "HI", + }); + const filename = join(dir, "tmp.txt"); + for (let i = 0; i < 30; i++) { - const fileHandle = await fsPromises.open(testDir + "/tmp.txt", "w", 0o666); + const fileHandle = await fsPromises.open(filename, "w", 0o666); const fd = fileHandle.fd; await Bun.file(fd).writer().end(); // @ts-ignore await fsPromises.close(fd); - expect(await Bun.file(testDir + "/tmp.txt").text()).toBe(""); - await Bun.sleep(50); + expect(await Bun.file(filename).text()).toBe(""); } }); diff --git a/test/js/third_party/http2-wrapper/http2-wrapper.test.ts b/test/js/third_party/http2-wrapper/http2-wrapper.test.ts index 7faad0efb2..3a185d490c 100644 --- a/test/js/third_party/http2-wrapper/http2-wrapper.test.ts +++ b/test/js/third_party/http2-wrapper/http2-wrapper.test.ts @@ -31,6 +31,7 @@ async function doRequest(options: AutoRequestOptions) { test("should allow http/1.1 when using http2-wrapper", async () => { { using server = Bun.serve({ + port: 0, async fetch(req) { return new Response( JSON.stringify({ @@ -60,6 +61,7 @@ test("should allow http/1.1 when using http2-wrapper", async () => { { using server = Bun.serve({ tls, + port: 0, hostname: "localhost", async fetch(req) { return new Response(