From c0c56dac8a8dc670c335dcd694e4e98ced781baf Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 2 Nov 2026 23:37:52 +0800 Subject: [PATCH] fix(run): enable Spring console colours with manual classpaths (#888) Spring Boot runs with a manual classpath (or no classpath placeholder) skip Java classpath resolution and so also skipped the console colour VM option. The shared console VM options are now applied on that launch path too; explicit spring.output.ansi.enabled settings are kept and resolved classpaths get the option once. --- src/features/run/javaModule.test.ts | 38 +++++++++++++++++++++++++++++ src/features/run/store.ts | 10 ++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/features/run/javaModule.test.ts b/src/features/run/javaModule.test.ts index d0193958..e5b270f6 100644 --- a/src/features/run/javaModule.test.ts +++ b/src/features/run/javaModule.test.ts @@ -87,6 +87,44 @@ it("finds the source module for an old saved configuration without a module", as expect(ipc.runStart).toHaveBeenCalledWith(expect.objectContaining({ cwd: "/outside/modules" }), expect.anything()); }); +it.each([["-cp", "lib/*"], []])("colours Spring Boot runs without classpath resolution (%j)", async (...classpath) => { + const args = ["-Xmx256m", ...classpath, "a.Main", "--spring.profiles.active=dev"]; + const boot = { ...config, kind: "spring-boot", args }; + const session = await startRun(boot); + expect(session?.status).toBe("running"); + expect(ipc.runStart).toHaveBeenCalledWith( + expect.objectContaining({ args: ["-Dspring.output.ansi.enabled=always", ...args] }), + expect.anything(), + ); + expect(ipc.javaResolveLaunch).not.toHaveBeenCalled(); + expect(boot.args).toBe(args); + expect(args).not.toContain("-Dspring.output.ansi.enabled=always"); +}); + +it.each(["never", "detect", "always"])("preserves an explicit Spring Boot console setting (%s)", async (value) => { + const args = [`-Dspring.output.ansi.enabled=${value}`, "-cp", "lib/*", "a.Main"]; + await startRun({ ...config, kind: "spring-boot", args }); + expect(ipc.runStart).toHaveBeenCalledWith(expect.objectContaining({ args }), expect.anything()); + expect(ipc.javaResolveLaunch).not.toHaveBeenCalled(); +}); + +it("keeps plain Java runs with an explicit classpath free of Spring Boot options", async () => { + const args = ["-cp", "lib/*", "a.Main"]; + await startRun({ ...config, args }); + expect(ipc.runStart).toHaveBeenCalledWith(expect.objectContaining({ args }), expect.anything()); + expect(ipc.javaResolveLaunch).not.toHaveBeenCalled(); +}); + +it("adds the Spring Boot console option once when resolving the module classpath", async () => { + await startRun({ ...config, kind: "spring-boot", module: "app" }); + expect(ipc.runStart).toHaveBeenCalledWith( + expect.objectContaining({ + args: ["-Dspring.output.ansi.enabled=always", "-cp", "/p/app/target/classes", "a.Main"], + }), + expect.anything(), + ); +}); + it("keeps an explicitly stored root module", async () => { expect(await findJavaModule({ ...config, module: "", cwd: "sibling" })).toBe(""); expect(ipc.findFiles).not.toHaveBeenCalled(); diff --git a/src/features/run/store.ts b/src/features/run/store.ts index 3d7f3552..0e038ae6 100644 --- a/src/features/run/store.ts +++ b/src/features/run/store.ts @@ -17,7 +17,13 @@ import { type RunId, type TestCaseReport, } from "@/platform/ipc"; -import { findJavaModule, isJvmApplication, parseJavaArgs, prepareJavaLaunch } from "@/platform/javaLaunch"; +import { + consoleVmArgs, + findJavaModule, + isJvmApplication, + parseJavaArgs, + prepareJavaLaunch, +} from "@/platform/javaLaunch"; import { downloadJdkActions } from "@/platform/jdk"; import { useProject } from "@/platform/project"; import { executeAction } from "@/platform/actions"; @@ -721,7 +727,7 @@ async function resolveJvmApplication(sessionId: string, config: RunConfig): Prom say(`No main class in '${config.name}': expected java [options] -cp \${classpath}
[args]\n`, "stderr"); return null; } - if (!parsed.classpath) return config; + if (!parsed.classpath) return { ...config, args: [...consoleVmArgs(config), ...config.args] }; const rt = runtime(sessionId)!; const request = { id: nextRequestId(), controller: new AbortController() }; rt.resolveRequest = request;