diff options
| -rw-r--r-- | .github/workflows/bun.yml | 27 | ||||
| -rw-r--r-- | Makefile | 24 | ||||
| -rw-r--r-- | README.md | 29 | ||||
| -rw-r--r-- | src/cli.zig | 2 | ||||
| -rw-r--r-- | src/cli/install.sh | 10 | ||||
| -rw-r--r-- | src/cli/upgrade_command.zig | 169 | ||||
| -rw-r--r-- | src/http.zig | 6 | ||||
| -rw-r--r-- | src/http_client_async.zig | 2 | ||||
| -rw-r--r-- | src/install/install.zig | 56 | ||||
| -rw-r--r-- | src/runtime.footer.bun.js | 1 | ||||
| -rw-r--r-- | src/string_immutable.zig | 2 |
11 files changed, 275 insertions, 53 deletions
diff --git a/.github/workflows/bun.yml b/.github/workflows/bun.yml index 54830aa56..43780ced6 100644 --- a/.github/workflows/bun.yml +++ b/.github/workflows/bun.yml @@ -28,8 +28,6 @@ jobs: cpu: [native, sandybridge] steps: - - name: Cleanup submodules - run: git rm --cached -r bench/ffi/plus100/plus100-napi || echo "" - uses: actions/checkout@v3 - name: Checkout submodules run: git -c submodule."src/bun.js/WebKit".update=none submodule update --init --recursive --depth=1 --progress -j $(nproc) @@ -40,13 +38,20 @@ jobs: - name: Run run: | rm -rf ${{runner.temp}}/release + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v3 with: context: . push: false - cache-from: type=gha - cache-to: type=gha,mode=max + tags: ghcr.io/oven-sh/bun:canary,ghcr.io/oven-sh/bun:${{github.sha}} + cache-from: type=registry,ref=ghcr.io/oven-sh/bun:buildcache-${{matrix.cpu}}-amd64 + cache-to: type=registry,ref=ghcr.io/oven-sh/bun:buildcache-${{matrix.cpu}}-amd64,mode=max build-args: | ARCH=x86_64 BUILDARCH=amd64 @@ -99,8 +104,6 @@ jobs: runs-on: linux-arm64 timeout-minutes: 90 steps: - - name: Cleanup submodules - run: git rm --cached -r bench/ffi/plus100/plus100-napi || echo "" - uses: actions/checkout@v3 - name: Checkout submodules run: git -c submodule."src/bun.js/WebKit".update=none submodule update --init --recursive --depth=1 --progress -j $(nproc) @@ -111,13 +114,20 @@ jobs: - name: Run run: | rm -rf ${{runner.temp}}/release + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v3 with: context: . + tags: ghcr.io/oven-sh/bun:canary,ghcr.io/oven-sh/bun:${{github.sha}} push: false - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=registry,ref=ghcr.io/oven-sh/bun:buildcache-aarch64 + cache-to: type=registry,ref=ghcr.io/oven-sh/bun:buildcache-aarch64,mode=max build-args: | ARCH=aarch64 BUILDARCH=arm64 @@ -146,6 +156,7 @@ jobs: name: release runs-on: ubuntu-18.04 timeout-minutes: 90 + if: github.event_name == 'push' needs: - linux-x64 - linux-aarch64 @@ -4,6 +4,9 @@ OS_NAME := $(shell uname -s | tr '[:upper:]' '[:lower:]') ARCH_NAME_RAW := $(shell uname -m) BUN_AUTO_UPDATER_REPO = Jarred-Sumner/bun-releases-for-updater +# 'make' command will trigger the help target +.DEFAULT_GOAL := help + # On Linux ARM64, uname -m reports aarch64 ifeq ($(ARCH_NAME_RAW),aarch64) ARCH_NAME_RAW = arm64 @@ -419,7 +422,7 @@ tinycc: make -j10 && \ cp $(TINYCC_DIR)/*.a $(BUN_DEPS_OUT_DIR) -generate-builtins: +generate-builtins: ## to generate builtins rm -f src/bun.js/bindings/*Builtin*.cpp src/bun.js/bindings/*Builtin*.h src/bun.js/bindings/*Builtin*.cpp rm -rf src/bun.js/builtins/cpp mkdir -p src/bun.js/builtins/cpp @@ -446,7 +449,7 @@ release-types: @npm --version >/dev/null 2>&1 || (echo -e "ERROR: npm is required."; exit 1) cd packages/bun-types && npm publish -format: +format: ## to format the code $(PRETTIER) --write test/bun.js/*.js $(PRETTIER) --write test/bun.js/solid-dom-fixtures/**/*.js @@ -873,7 +876,7 @@ release-bin: release-bin-without-push release-bin-push test/wiptest/run.o: test/wiptest/run.cpp - $(CXX) -Wall -g -c -std=c++17 -o test/wiptest/run.o test/wiptest/run.cpp + $(CXX) -Wall -g -c -std=c++2a -lc -o test/wiptest/run.o test/wiptest/run.cpp test/wiptest/run: test/wiptest/run.o $(CXX) -Wall -g -o test/wiptest/run test/wiptest/run.o @@ -917,7 +920,7 @@ test-dev-bun-snapshot: test-bun-wiptest: test/wiptest/run cd test/wiptest && BUN_BIN=$(DEBUG_BUN) ./run ./fixtures -test-all: test-install test-bun-snapshot test-with-hmr test-no-hmr test-create-next test-create-react test-bun-run test-bun-install test-bun-dev test-bun-wiptest +test-all: test-install test-bun-snapshot test-with-hmr test-no-hmr test-create-next test-create-react test-bun-run test-bun-install test-bun-dev copy-test-node-modules: rm -rf test/snippets/package-json-exports/node_modules || echo ""; @@ -1246,7 +1249,7 @@ bun-link-lld-release-no-lto: ifeq ($(OS_NAME),darwin) bun-link-lld-release-dsym: bun-release-copy-obj $(DSYMUTIL) -o $(BUN_RELEASE_BIN).dSYM $(BUN_RELEASE_BIN) - -$(STRIP) -s $(BUN_RELEASE_BIN) --wildcard -K _napi\* + -$(STRIP) $(BUN_RELEASE_BIN) copy-to-bun-release-dir-dsym: gzip --keep -c $(PACKAGE_DIR)/bun.dSYM > $(BUN_RELEASE_DIR)/bun.dSYM.gz @@ -1255,7 +1258,7 @@ endif ifeq ($(OS_NAME),linux) bun-link-lld-release-dsym: bun-release-copy-obj mv $(BUN_RELEASE_BIN).o /tmp/bun-$(PACKAGE_JSON_VERSION).o - -$(STRIP) -s $(BUN_RELEASE_BIN) --wildcard -K _napi\* + -$(STRIP) $(BUN_RELEASE_BIN) --wildcard -K _napi\* copy-to-bun-release-dir-dsym: endif @@ -1422,7 +1425,7 @@ endif endif -build-unit: +build-unit: ## to build your unit tests @rm -rf zig-out/bin/$(testname) @mkdir -p zig-out/bin zig test $(realpath $(testpath)) \ @@ -1439,7 +1442,7 @@ build-unit: && \ cp zig-out/bin/$(testname) $(testbinpath) -run-all-unit-tests: +run-all-unit-tests: ## to run your unit tests @rm -rf zig-out/bin/__main_test @mkdir -p zig-out/bin zig test src/main.zig \ @@ -1458,11 +1461,12 @@ run-all-unit-tests: run-unit: @zig-out/bin/$(testname) $(ZIG) - +help: ## to print this help + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {gsub("\\\\n",sprintf("\n%22c",""), $$2);printf "\033[36m%-20s\033[0m \t\t%s\n", $$1, $$2}' $(MAKEFILE_LIST) test: build-unit run-unit -integration-test-dev: +integration-test-dev: ## to run integration tests USE_EXISTING_PROCESS=true TEST_SERVER_URL=http://localhost:3000 node test/scripts/browser.js copy-install: @@ -1,4 +1,5 @@ # bun + <p align="center"> <a href="https://bun.sh"><img src="https://bun.sh/logo@2x.png" alt="Logo"></a> </p> @@ -540,7 +541,7 @@ You can see [Bun's Roadmap](https://github.com/oven-sh/bun/issues/159), but here | `@jsxPragma` comments | JS Transpiler | | Sharing `.bun` files | bun | | Dates & timestamps | TOML parser | -| [Hash components for Fast Refresh](https://github.com/oven-sh/bun/issues/18) | JSX Transpiler | +| [Hash components for Fast Refresh](https://github.com/oven-sh/bun/issues/18) | JSX Transpiler | <small> JS Transpiler == JavaScript Transpiler @@ -929,24 +930,29 @@ To fix this issue: 3. Try again, and if that still doesn’t fix it, open an issue ### Unzip is required + Unzip is required to install bun on Linux. You can use one of the following commands to install `unzip`: #### Debian / Ubuntu / Mint + ```sh sudo apt install unzip ``` #### RedHat / CentOS / Fedora + ```sh sudo dnf install unzip ``` #### Arch / Manjaro + ```sh sudo pacman -S unzip ``` #### OpenSUSE + ```sh sudo zypper install unzip ``` @@ -1571,6 +1577,26 @@ bun is distributed as a single binary file, so you can also do this manually: - Unzip the folder - Move the `bun` binary to `~/.bun/bin` (or anywhere) +### Canary builds + +[Canary](https://github.com/oven-sh/bun/releases/tag/canary) builds are generated on every commit. At the time of writing, only Linux x64 & Linux arm64 are generated. + +To install a [canary](https://github.com/oven-sh/bun/releases/tag/canary) build of bun, run: + +```bash +bun upgrade --canary +``` + +This flag is not persistent (though that might change in the future). If you want to always run the canary build of bun, set the `BUN_CANARY` environment variable to `1` in your shell's startup script. + +This will download the release zip from https://github.com/oven-sh/bun/releases/tag/canary. + +To revert to the latest published version of bun, run: + +```bash +bun upgrade +``` + ### `bun completions` This command installs completions for `zsh` and/or `fish`. It runs automatically on every `bun upgrade` and on install. It reads from `$SHELL` to determine which shell to install for. It tries several common shell completion directories for your shell and OS. @@ -3243,7 +3269,6 @@ You’ll want to make sure `zig` is in `$PATH`. The specific version of Zig expe If you're building on a macOS device, you'll need to have a valid Developer Certificate, or else the code signing step will fail. To check if you have one, open the `Keychain Access` app, go to the `login` profile and search for `Apple Development`. You should have at least one certificate with a name like `Apple Development: user@example.com (WDYABC123)`. If you don't have one, follow [this guide](https://ioscodesigning.com/generating-code-signing-files/#generate-a-code-signing-certificate-using-xcode) to get one. - In `bun`: ```bash diff --git a/src/cli.zig b/src/cli.zig index 7bdf75dbb..e849d258f 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -55,7 +55,7 @@ const UpgradeCommand = @import("./cli/upgrade_command.zig").UpgradeCommand; const MacroMap = @import("./resolver/package_json.zig").MacroMap; const Reporter = @import("./report.zig"); -var start_time: i128 = undefined; +pub var start_time: i128 = undefined; const Bunfig = @import("./bunfig.zig").Bunfig; pub const Cli = struct { diff --git a/src/cli/install.sh b/src/cli/install.sh index 48ec15660..43f63264c 100644 --- a/src/cli/install.sh +++ b/src/cli/install.sh @@ -29,7 +29,7 @@ if [[ -t 1 ]]; then # Bold Bold_Green='\033[1;32m' # Bold Green - Bold_White='\033[1;37m' # Bold White + Bold_White='\033[1m' # Bold White fi error() { @@ -205,10 +205,12 @@ zsh) esac echo -echo -e "To get started, run:$Bold_White" +echo -e "To get started, run:" +echo if [[ $refresh_command ]]; then - echo -e " $refresh_command" + echo -e "$Bold_White $refresh_command$Color_Off" fi -echo -e " bun --help$Color_Off" +echo -e " $Bold_White bun --help$Color_Off" + diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index a28749849..fd101d775 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -56,11 +56,28 @@ pub const Version = struct { size: u32 = 0, pub fn name(this: Version) ?string { - if (this.tag.len > "bun-v".len and strings.eqlComptime(this.tag[0.."bun-v".len], "bun-v")) { - return this.tag[("bun-v".len)..]; - } else { - return null; + if (this.tag.len <= "bun-v".len or !strings.hasPrefixComptime(this.tag, "bun-v")) { + if (strings.eqlComptime(this.tag, "canary")) { + const Cli = @import("../cli.zig"); + + return std.fmt.allocPrint( + bun.default_allocator, + "bun-canary-timestamp-{any}", + .{ + std.fmt.fmtSliceHexLower( + std.mem.asBytes( + &bun.hash( + std.mem.asBytes(&Cli.start_time), + ), + ), + ), + }, + ) catch unreachable; + } + return this.tag; } + + return this.tag["bun-v".len..]; } pub const platform_label = if (Environment.isMac) "darwin" else "linux"; @@ -80,7 +97,10 @@ pub const Version = struct { pub const UpgradeCheckerThread = struct { var update_checker_thread: std.Thread = undefined; pub fn spawn(env_loader: *DotEnv.Loader) void { - if (env_loader.map.get("BUN_DISABLE_UPGRADE_CHECK") != null or env_loader.map.get("CI") != null) return; + if (env_loader.map.get("BUN_DISABLE_UPGRADE_CHECK") != null or + env_loader.map.get("CI") != null or + strings.eqlComptime(env_loader.get("BUN_CANARY") orelse "0", "1")) + return; update_checker_thread = std.Thread.spawn(.{}, run, .{env_loader}) catch return; update_checker_thread.detach(); } @@ -353,12 +373,14 @@ pub const UpgradeCommand = struct { break :brk DotEnv.Loader.init(map, ctx.allocator); }; - env_loader.loadProcess(); var version: Version = undefined; + const use_canary = strings.eqlComptime(env_loader.map.get("BUN_CANARY") orelse "0", "1") or + // TODO: add separate args just for "bun upgrade"? + strings.containsAny(bun.span(std.os.argv), "--canary"); - { + if (!use_canary) { var refresher = std.Progress{}; var progress = refresher.start("Fetching version tags", 0); @@ -384,12 +406,19 @@ pub const UpgradeCommand = struct { ); Global.exit(1); } - } - { Output.prettyErrorln("<r><b>bun <cyan>v{s}<r> is out<r>! You're on <blue>{s}<r>\n", .{ version.name().?, Global.package_json_version }); Output.flush(); + } else { + version = Version{ + .tag = "canary", + .zip_url = "https://github.com/oven-sh/bun/releases/download/canary/" ++ Version.zip_filename, + .size = 0, + .buf = MutableString.initEmpty(bun.default_allocator), + }; + } + { var refresher = std.Progress{}; var progress = refresher.start("Downloading", version.size); refresher.refresh(); @@ -413,7 +442,22 @@ pub const UpgradeCommand = struct { const response = try async_http.sendSync(true); switch (response.status_code) { - 404 => return error.HTTP404, + 404 => { + if (use_canary) { + Output.prettyErrorln( + \\<r><red>error:<r> Canary builds are not available for this platform yet + \\ + \\ Release: <cyan>https://github.com/oven-sh/bun/releases/tag/canary<r> + \\ Filename: <b>{s}<r> + \\ + , .{ + Version.zip_filename, + }); + Global.exit(1); + } + + return error.HTTP404; + }, 403 => return error.HTTPForbidden, 429 => return error.HTTPTooManyRequests, 499...599 => return error.GitHubIsDown, @@ -503,7 +547,6 @@ pub const UpgradeCommand = struct { Global.exit(1); } } - { var verify_argv = [_]string{ exe_subpath, @@ -527,17 +570,21 @@ pub const UpgradeCommand = struct { Global.exit(1); } - if (!strings.eql(std.mem.trim(u8, result.stdout, " \n\r\t"), version_name)) { - save_dir_.deleteTree(version_name) catch {}; - - Output.prettyErrorln( - "<r><red>error<r>: The downloaded version of bun (<red>{s}<r>) doesn't match the expected version (<b>{s}<r>)<r>. Cancelled upgrade", - .{ - result.stdout[0..@minimum(result.stdout.len, 128)], - version_name, - }, - ); - Global.exit(1); + // It should run successfully + // but we don't care about the version number if we're doing a canary build + if (!use_canary) { + if (!strings.eql(std.mem.trim(u8, result.stdout, " \n\r\t"), version_name)) { + save_dir_.deleteTree(version_name) catch {}; + + Output.prettyErrorln( + "<r><red>error<r>: The downloaded version of bun (<red>{s}<r>) doesn't match the expected version (<b>{s}<r>)<r>. Cancelled upgrade", + .{ + result.stdout[0..@minimum(result.stdout.len, 128)], + version_name, + }, + ); + Global.exit(1); + } } } @@ -556,6 +603,47 @@ pub const UpgradeCommand = struct { Global.exit(1); }; + if (use_canary) { + + // Check if the versions are the same + const target_stat = target_dir.statFile(target_filename) catch |err| { + save_dir_.deleteTree(version_name) catch {}; + Output.prettyErrorln("<r><red>error:<r> Failed to stat target bun {s}", .{@errorName(err)}); + Global.exit(1); + }; + + const dest_stat = save_dir.statFile(exe_subpath) catch |err| { + save_dir_.deleteTree(version_name) catch {}; + Output.prettyErrorln("<r><red>error:<r> Failed to stat source bun {s}", .{@errorName(err)}); + Global.exit(1); + }; + + if (target_stat.size == dest_stat.size and target_stat.size > 0) { + var input_buf = try ctx.allocator.alloc(u8, target_stat.size); + + const target_hash = std.hash.Wyhash.hash(0, target_dir.readFile(target_filename, input_buf) catch |err| { + save_dir_.deleteTree(version_name) catch {}; + Output.prettyErrorln("<r><red>error:<r> Failed to read target bun {s}", .{@errorName(err)}); + Global.exit(1); + }); + + const source_hash = std.hash.Wyhash.hash(0, save_dir.readFile(exe_subpath, input_buf) catch |err| { + save_dir_.deleteTree(version_name) catch {}; + Output.prettyErrorln("<r><red>error:<r> Failed to read source bun {s}", .{@errorName(err)}); + Global.exit(1); + }); + + if (target_hash == source_hash) { + save_dir_.deleteTree(version_name) catch {}; + Output.prettyErrorln( + "<r><green>Congrats!<r> You're already on the latest canary build of bun", + .{}, + ); + Global.exit(0); + } + } + } + if (env_loader.map.get("BUN_DRY_RUN") == null) { C.moveFileZ(save_dir.fd, exe_subpath, target_dir.fd, target_filename) catch |err| { save_dir_.deleteTree(version_name) catch {}; @@ -584,7 +672,42 @@ pub const UpgradeCommand = struct { Output.printStartEnd(ctx.start_time, std.time.nanoTimestamp()); - Output.prettyErrorln("<r> Upgraded.\n\n<b><green>Welcome to bun v{s}!<r>\n\n Report any bugs:\n https://github.com/oven-sh/bun/issues\n\n What's new:\n https://github.com/oven-sh/bun/releases/tag/{s}<r>", .{ version_name, version.tag }); + if (use_canary) { + Output.prettyErrorln( + \\<r> Upgraded. + \\ + \\<b><green>Welcome to bun's latest canary build!<r> + \\ + \\Report any bugs: + \\ + \\ https://github.com/oven-sh/bun/issues + \\ + \\What's new: + \\ + \\ <cyan>https://github.com/oven-sh/bun/releases/tag/{s}<r> + \\ + , + .{version.tag}, + ); + } else { + Output.prettyErrorln( + \\<r> Upgraded. + \\ + \\<b><green>Welcome to bun v{s}!<r> + \\ + \\Report any bugs: + \\ + \\ https://github.com/oven-sh/bun/issues + \\ + \\What's new: + \\ + \\ <cyan>https://github.com/oven-sh/bun/releases/tag/{s}<r> + \\ + , + .{ version_name, version.tag }, + ); + } + Output.flush(); return; } diff --git a/src/http.zig b/src/http.zig index 1515f976f..605bd1840 100644 --- a/src/http.zig +++ b/src/http.zig @@ -1881,9 +1881,9 @@ pub const RequestContext = struct { var reloader = Api.Reloader.disable; if (ctx.bundler.options.hot_module_reloading) { reloader = Api.Reloader.live; - // if (ctx.bundler.options.jsx.supports_fast_refresh) { - // reloader = Api.Reloader.fast_refresh; - // } + if (ctx.bundler.options.jsx.supports_fast_refresh and ctx.bundler.env.get("BUN_FORCE_HMR") != null) { + reloader = Api.Reloader.fast_refresh; + } } const welcome_message = Api.WebsocketMessageWelcome{ diff --git a/src/http_client_async.zig b/src/http_client_async.zig index 2da75b652..7b376c986 100644 --- a/src/http_client_async.zig +++ b/src/http_client_async.zig @@ -56,6 +56,8 @@ pub fn onThreadStart(_: ?*anyopaque) ?*anyopaque { \\ <cyan>wsl --update<r> \\ <cyan>wsl --shutdown<r> \\ + \\ Please make sure you're using WSL version 2 (not WSL 1). + \\ \\If that doesn't work (and you're on a Windows machine), try this: \\ 1. Open Windows Update \\ 2. Download any updates to Windows Subsystem for Linux diff --git a/src/install/install.zig b/src/install/install.zig index 8ae032022..7373cf90d 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -199,7 +199,61 @@ const NetworkTask = struct { scope: *const Npm.Registry.Scope, loaded_manifest: ?Npm.PackageManifest, ) !void { - this.url_buf = try std.fmt.allocPrint(allocator, "{s}://{s}/{s}/{s}", .{ scope.url.displayProtocol(), scope.url.displayHostname(), strings.trim(scope.url.path, "/"), name }); + const pathname: string = if (!strings.eqlComptime(scope.url.pathname, "/")) + scope.url.pathname + else + @as(string, ""); + + if (pathname.len > 0) { + if (scope.url.getPort()) |port_number| { + this.url_buf = try std.fmt.allocPrint( + allocator, + "{s}://{s}:{d}/{s}/{s}", + .{ + scope.url.displayProtocol(), + scope.url.displayHostname(), + port_number, + pathname, + name, + }, + ); + } else { + this.url_buf = try std.fmt.allocPrint( + allocator, + "{s}://{s}/{s}/{s}", + .{ + scope.url.displayProtocol(), + scope.url.displayHostname(), + pathname, + name, + }, + ); + } + } else { + if (scope.url.getPort()) |port_number| { + this.url_buf = try std.fmt.allocPrint( + allocator, + "{s}://{s}:{d}/{s}", + .{ + scope.url.displayProtocol(), + scope.url.displayHostname(), + port_number, + name, + }, + ); + } else { + this.url_buf = try std.fmt.allocPrint( + allocator, + "{s}://{s}/{s}", + .{ + scope.url.displayProtocol(), + scope.url.displayHostname(), + name, + }, + ); + } + } + var last_modified: string = ""; var etag: string = ""; if (loaded_manifest) |manifest| { diff --git a/src/runtime.footer.bun.js b/src/runtime.footer.bun.js index 72a2703e9..a5466f53a 100644 --- a/src/runtime.footer.bun.js +++ b/src/runtime.footer.bun.js @@ -22,5 +22,6 @@ export var __require = (globalThis.require ||= function (moduleId) { return BUN_RUNTIME.__require(moduleId); }); +__require.d ||= BUN_RUNTIME.__require.d; globalThis.__internalIsCommonJSNamespace ||= BUN_RUNTIME.__internalIsCommonJSNamespace; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index ec3a2ecbe..801eacd51 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -78,7 +78,7 @@ pub inline fn containsComptime(self: string, comptime str: string) bool { pub const includes = contains; pub inline fn containsAny(in: anytype, target: string) bool { - for (in) |str| if (contains(str, target)) return true; + for (in) |str| if (contains(bun.span(str), target)) return true; return false; } |
