diff --git a/src/bun_core/output.rs b/src/bun_core/output.rs index 3274e5c074cb..6fdf8d11e4d1 100644 --- a/src/bun_core/output.rs +++ b/src/bun_core/output.rs @@ -1991,6 +1991,13 @@ pub fn pretty_fmt_runtime(fmt: &[u8], is_enabled: bool) -> Vec { i += 1; } b'{' => { + // `{{` is an escaped literal brace, not the start of a + // placeholder: the text after it is still subject to markup. + if fmt.get(i + 1) == Some(&b'{') { + out.extend_from_slice(b"{{"); + i += 2; + continue; + } while i < fmt.len() && fmt[i] != b'}' { out.push(fmt[i]); i += 1; @@ -2940,6 +2947,55 @@ mod pretty_fmt_tests { assert_eq!(pretty_fmt!("{?}", true), "{:?}"); } + #[test] + fn markup_after_escaped_brace_is_rewritten() { + // `{{` is a literal brace, not the start of a spec: tags between it + // and the next `}` must still be converted / stripped. + assert_eq!( + pretty_fmt!( + "\"{}\": {{\n \"bun\": \"latest\"\n}}", + false + ), + "\"{}\": {{\n \"bun\": \"latest\"\n}}" + ); + assert_eq!(pretty_fmt!("{{x}}", true), "{{\x1b[32mx\x1b[0m}}"); + assert_eq!(pretty_fmt!("{{x}}", false), "{{x}}"); + assert_eq!(pretty_fmt!("{{{{", false), "{{{{"); + // The escapes pass through untouched, so the output is still a valid + // `format_args!` template, including when an escape abuts a spec. + assert_eq!( + format!(pretty_fmt!("{{ {s} }}", false), "bun"), + "{ bun }" + ); + assert_eq!(pretty_fmt!("{{{s}}}", false), "{{{}}}"); + assert_eq!(format!(pretty_fmt!("{{{s}}}", false), "bun"), "{bun}"); + // A spec after the escape is still shielded from the tag parser. + assert_eq!(pretty_fmt!("{{{:<4}}}", false), "{{{:<4}}}"); + } + + #[test] + fn runtime_markup_after_escaped_brace_is_rewritten() { + assert_eq!( + pretty_fmt_runtime(b"{{x}}", true), + b"{{\x1b[32mx\x1b[0m}}" + ); + assert_eq!(pretty_fmt_runtime(b"{{x}}", false), b"{{x}}"); + assert_eq!( + format!( + "{}", + super::pretty_fmt_args("{{ {s} }}", false, ("bun",)) + ), + "{ bun }" + ); + assert_eq!( + format!( + "{}", + super::pretty_fmt_args("{{ {s} }}", true, ("bun",)) + ), + "{ \x1b[32mbun\x1b[0m }" + ); + } + #[test] fn concat_and_stringify_inputs() { assert_eq!( @@ -3008,5 +3064,7 @@ mod pretty_fmt_tests { check!( " link [\\] Register or link a local npm package\n" ); + check!("\"{}\": {{\n \"bun\": \"latest\"\n}}"); + check!("{{{{{{{:<4}}}"); } } diff --git a/src/bun_core_macros/lib.rs b/src/bun_core_macros/lib.rs index 1e8e0b2e85db..a37f6707bdd0 100644 --- a/src/bun_core_macros/lib.rs +++ b/src/bun_core_macros/lib.rs @@ -114,6 +114,13 @@ fn rewrite(fmt: &str, is_enabled: bool) -> Result { i += 1; } b'{' => { + // `{{` is `format_args!`'s escaped literal brace, not the start + // of a spec: the text after it is still subject to markup. + if bytes.get(i + 1) == Some(&b'{') { + out.push_str("{{"); + i += 2; + continue; + } // copy `{ ... }` verbatim, optionally rewriting legacy specs let start = i; while i < bytes.len() && bytes[i] != b'}' { diff --git a/src/clap_macros/lib.rs b/src/clap_macros/lib.rs index 0969a83031f5..92a46fcdda37 100644 --- a/src/clap_macros/lib.rs +++ b/src/clap_macros/lib.rs @@ -298,6 +298,11 @@ fn pretty_rewrite(fmt: &[u8], is_enabled: bool) -> Vec { i += 1; } b'{' => { + if fmt.get(i + 1) == Some(&b'{') { + out.extend_from_slice(b"{{"); + i += 2; + continue; + } while i < fmt.len() && fmt[i] != b'}' { out.push(fmt[i]); i += 1; diff --git a/test/cli/install/__snapshots__/bun-install.test.ts.snap b/test/cli/install/__snapshots__/bun-install.test.ts.snap index a0e509edac2b..9a7db97f6e22 100644 --- a/test/cli/install/__snapshots__/bun-install.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install.test.ts.snap @@ -14,7 +14,7 @@ exports[`should report error on invalid format for dependencies 1`] = ` ^ error: dependencies expects a map of specifiers, e.g. "dependencies": { - "bun": "latest" + "bun": "latest" } at [dir]/package.json:1:33 " @@ -49,7 +49,7 @@ exports[`bun-install should report error on invalid format for dependencies 1`] ^ error: dependencies expects a map of specifiers, e.g. "dependencies": { - "bun": "latest" + "bun": "latest" } at [dir]/package.json:1:33 " diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 5170edcc68f0..53add48ee679 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -8,6 +8,7 @@ import { bunEnv as env, isWindows, joinP, + normalizeBunSnapshot, readdirSorted, runBunInstall, tempDir, @@ -4965,7 +4966,7 @@ describe.concurrent("bun-install", () => { ` ^`, `error: optionalDependencies expects a map of specifiers, e.g.`, ` "optionalDependencies": {`, - ` "bun": "latest"`, + ` "bun": "latest"`, ` }`, ` at [dir]/package.json:1:33`, ]); @@ -4975,6 +4976,52 @@ describe.concurrent("bun-install", () => { }); }); + it("should report error on non-string specifier in dependencies", async () => { + using dir = tempDir("invalid-dependency-specifier", { + "package.json": JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { bun: 1 } }), + }); + + await using proc = spawn({ + cmd: [bunExe(), "install"], + cwd: String(dir), + env, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(normalizeBunSnapshot(stderr, String(dir))).toMatchInlineSnapshot(` + "1 | {"name":"foo","version":"0.0.1","dependencies":{"bun":1}} + ^ + error: dependencies expects a map of specifiers, e.g. + "dependencies": { + "bun": "latest" + } + at /package.json:1:55" + `); + expect(stdout).toContain("bun install v1."); + expect(exitCode).toBe(1); + }); + + it("should color the example in the invalid dependencies error when colors are enabled", async () => { + using dir = tempDir("invalid-dependencies-color", { + "package.json": JSON.stringify({ name: "foo", version: "0.0.1", dependencies: [] }), + }); + + await using proc = spawn({ + cmd: [bunExe(), "install"], + cwd: String(dir), + env: { ...env, FORCE_COLOR: "1" }, + stdout: "ignore", + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + expect(stderr).toContain(`\n \x1b[32m"bun"\x1b[0m: \x1b[32m"latest"\x1b[0m\n`); + expect(stderr).not.toContain(""); + expect(exitCode).toBe(1); + }); + test.serial("should report error on invalid format for workspaces", async () => { await withContext(defaultOpts, async ctx => { await writeFile(