JavaScript: spawn Java RPC server for TS integration tests#7806
Merged
Conversation
Add `JavaRpcTestServer` to let vitest tests drive real Java recipes through the existing RewriteRpc bidirectional protocol over a spawned `org.openrewrite.maven.rpc.JavaRewriteRpc` process. Mirrors the Python and C# client infrastructure.
Adds a vitest fixture (`testJavaRpc`/`describeJavaRpc`) at the new
`@openrewrite/rewrite/test/java-rpc` subpath so downstream tests can
declare the JVM lifecycle without boilerplate. The JVM is worker-scoped
(one process per vitest worker); state is reset before every test.
Mirrors the Java pattern on RewriteRpc itself: `reset()` clears local
caches AND sends `Reset` to the remote, while the inbound `Reset`
handler only clears local — no recursion.
Wires `:rewrite-javascript:npmTest` to depend on `generateTestClasspath`
so `./gradlew :rewrite-javascript:test` picks up the RPC tests
automatically (matches the C# `csharpTest` setup).
Fixes from the simplify pass on the previous commit:
- removeListener("exit") on the early-exit watchdog so a normal exit
during dispose() doesn't reject an unobserved promise
- dispose(): attach the exit listener before signalling shutdown to
close a TOCTOU race; drop the spurious `child.killed` early-return
- replace the hand-rolled stderr line-buffer with readline so a final
non-newline-terminated line gets flushed on process close
- trim the classpath candidate list to layouts that actually exist
Adds a top-level `prepareJavaRecipe(id, options?)` that resolves the
active RewriteRpc (via the existing static accessor) and calls
prepareRecipe on it. The returned RpcRecipe already extends Recipe, so
it can be used directly or composed inside a TS recipe.
Designed for two use cases:
1. A TS recipe that delegates its editor to a Java recipe — works in
test (with JavaRpcTestServer) and in production (where Java spawned
the TS server). The active connection is resolved via
RewriteRpc.get() in both cases.
2. Top-level convenience functions like:
export function addDependency(opts: AddDependencyOptions): Promise<Recipe> {
return prepareJavaRecipe("org.openrewrite.javascript.AddDependency", opts);
}
Sit naturally on top of this primitive. Those wrappers themselves
are not added here — they belong with the recipes they wrap.
No in-process fallback by design: the Java recipe's editor is
authoritative — we don't reimplement editing recipes in TS. For
preconditions that need to degrade gracefully without RPC, the existing
RecipeRef pattern (usesType / usesMethod) carries a localVisitor and
remains the right tool.
The smoke test exercises both surfaces: one test calls
JavaRpcTestServer.rpc.prepareRecipe directly (low-level API); the other
uses prepareJavaRecipe (recipe-author API).
knutwannheden
added a commit
that referenced
this pull request
May 28, 2026
… recipes Adds addDependency / removeDependency / upgradeDependencyVersion / changeDependency / upgradeTransitiveDependencyVersion as top-level TS functions that delegate to the Java implementations via prepareJavaRecipe (#7806). Lets TS recipes compose the Java dependency recipes without boilerplate.
knutwannheden
added a commit
that referenced
this pull request
May 28, 2026
…s overlay (#7795) * rewrite-javascript: bootstrap Java test sourceset, promote rewrite-yaml to implementation * rewrite-javascript: add PackageManagerExecutor for npm/yarn/pnpm/bun * rewrite-javascript: add LockFileRegeneration with NPM/YARN_CLASSIC/YARN_BERRY/PNPM/BUN * rewrite-javascript: add PackageJsonHelper foundations (lock detection, ctx, reparse) * rewrite-python, rewrite-javascript: wrap Files.walk/list in try-with-resources Stream-returning Files.* APIs hold an underlying DirectoryStream that is only closed when the terminal operation completes normally. If the lambda throws (or short-circuits, in future code), the stream leaks. Wrap the four cleanupDirectory / initializeCacheFromDisk sites in try-with-resources to match the JDK-recommended idiom. * rewrite-javascript: PackageJsonHelper.refreshMarker rebuilds declared-dep lists * rewrite-javascript: PackageJsonHelper.addDependency * rewrite-javascript: fix Space.EMPTY check to use value equality Use isEmpty() on whitespace rather than == against the singleton, so makeMember works correctly when called with literals from a parsed tree (where Space instances may be semantically equal but distinct). * rewrite-javascript: PackageJsonHelper.removeDependency * rewrite-javascript: PackageJsonHelper.upgradeVersion + MatchedDependency * rewrite-javascript: PackageJsonHelper.changeDependency * rewrite-javascript: scope per-iteration changed flag in upgradeVersion/changeDependency Avoid spuriously rewriting unchanged scopes when a previous outer-loop iteration already set the global changed flag. Track per-scope changes locally and roll up to the global flag only when something actually changed in that scope. * rewrite-javascript: PackageJsonHelper.upgradeTransitive + PackageJsonOverrides * rewrite-javascript: PackageJsonHelper.editAndRegenerate orchestration * rewrite-javascript: add AddDependency recipe + tests Also fix PackageJsonHelper.appendMember to handle RPC-parsed JSON trees where whitespace is stored on the key literal's prefix rather than on the member node itself (difference between Java JSON parser and TypeScript PackageJsonParser via RPC). * rewrite-javascript: add RemoveDependency recipe + tests * rewrite-javascript: add ChangeDependency recipe + tests * rewrite-javascript: add UpgradeDependencyVersion recipe + tests * rewrite-javascript: add UpgradeTransitiveDependencyVersion recipe + tests * rewrite-javascript: add cross-recipe chain test for AddDependency → UpgradeDependencyVersion Fix three bugs surfaced by the test: - UpgradeDependencyVersion: re-scan live tree for matches when the scanner found no matches on the original tree (enables cross-recipe chaining) - PackageJsonHelper.addDependency: handle empty {} scope (Json.Empty placeholder) with proper indentation instead of producing {,lodash:...} - DependencyWorkspace.isWorkspaceValid: don't require node_modules (absent for empty-deps packages), preventing FileAlreadyExistsException on reuse * rewrite-javascript: register dependency recipes in recipes.csv * rewrite-javascript: remove TS dependency recipes (replaced by Java implementations) * rewrite-javascript: LockFileParser foundations + top-level parsing * rewrite-javascript: LockFileParser scoped + nested package tests * rewrite-javascript: tighten parsesScopedTopLevelPackage with isSameAs * rewrite-javascript: LockFileParser metadata extraction (transitive deps, engines, license) * rewrite-javascript: LockFileParser robustness tests (forward compat + error paths) * rewrite-javascript: BunLockAdapter (bun.lock -> npm v3) * rewrite-javascript: BunLockAdapter scoped + transitive + JSONC tests * rewrite-javascript: PackageJsonHelper.overlayResolvedDeps with Dependency relinking * rewrite-javascript: editAndRegenerate calls overlayResolvedDeps for npm/Bun * rewrite-javascript: preserve cause chain in lock parse Markup.warn * rewrite-javascript: TS-Java parity tests for LockFileParser (npm) * rewrite-javascript: make LockFileParser and BunLockAdapter public for testability * rewrite-javascript: pin is-even version + use Files.readString in parity test * rewrite-javascript: end-to-end test for v2.0 resolved-deps overlay * rewrite-javascript: YarnClassicLockAdapter foundations + happy-path test * rewrite-javascript: tolerate variable comma spacing in yarn.lock block headers * rewrite-javascript: YarnClassicLockAdapter multi-key + scoped + transitive deps tests * rewrite-javascript: YarnClassicLockAdapter multi-version + edge-case tests * rewrite-javascript: YarnClassicLockAdapter CRLF tolerance test * rewrite-javascript: add SnakeYAML dep + YarnBerryLockAdapter foundation * rewrite-javascript: YarnBerryLockAdapter scoped + multi-version + transitive tests * rewrite-javascript: PnpmLockAdapter foundation + happy-path * rewrite-javascript: PnpmLockAdapter scoped + transitive + peer-dep tests * rewrite-javascript: PnpmLockAdapter parsePackageKey corner-case unit tests * rewrite-javascript: dispatch overlayResolvedDeps to yarn + pnpm adapters * rewrite-javascript: refresh overlayResolvedDeps Javadoc for v2.1 PMs * rewrite-javascript: DependencyWorkspace.getOrCreateWorkspace(String, PackageManager) Add PM-aware overload that keys the cache on hash+"_"+pm.name(), seeds .yarnrc.yml for yarn-berry (nodeLinker: node-modules), and dispatches to the correct install command per PM via the new private runInstall() method. The single-arg overload is preserved and delegates to Npm. Rename hash→key in initializeCacheFromDisk to reflect the broader key shape. * rewrite-javascript: nodePackageManager() helper + yarnBerry/pnpm wrappers * rewrite-javascript: deduplicate per-PM lockfile mapping via LockFileRegeneration * rewrite-javascript: parity test for yarn classic * rewrite-javascript: normalize FQCN imports in parity helper * rewrite-javascript: parity test for yarn berry * rewrite-javascript: parity test for pnpm * rewrite-javascript: e2e overlay test for yarn berry * rewrite-javascript: e2e overlay test for pnpm * rewrite-javascript: yarn berry full-install uses --mode skip-build Matches LockFileRegeneration.YARN_BERRY's args. The previous shared case with YarnClassic used --ignore-scripts, which is a yarn classic flag; yarn berry 4.x uses --mode skip-build for the same intent. * rewrite-javascript: delegate runInstall to PackageManagerExecutor Adds 120s timeout, stream draining, and process destroy-on-failure that the inline ProcessBuilder lacked. Also removes the dead replace('/', '_') / replace('+', '-') calls in hashContent: Base64.getUrlEncoder() already produces URL-safe output. * rewrite-javascript: symlink .yarnrc.yml for YarnBerry workspaces Without .yarnrc.yml in the test directory, any tool re-invoking yarn there defaults to PnP mode and ignores the symlinked node_modules. * rewrite-javascript: guard cachesPerPackageManagerSeparately on npm The test exercises both npm and pnpm but previously only skipped on missing pnpm — on a box without npm it would crash instead of skip. * rewrite-javascript: dedupe parityNpm helpers via ForPm delegates * rewrite-javascript: TS convenience wrappers for dependency-management recipes Adds addDependency / removeDependency / upgradeDependencyVersion / changeDependency / upgradeTransitiveDependencyVersion as top-level TS functions that delegate to the Java implementations via prepareJavaRecipe (#7806). Lets TS recipes compose the Java dependency recipes without boilerplate. * rewrite-docker: regenerate recipes.csv for ChangeFrom description drift * Revert "rewrite-docker: regenerate recipes.csv for ChangeFrom description drift" This reverts commit 6a41c1e. * rewrite-javascript: regenerate recipes.csv * rewrite-javascript: sort recipes.csv alphabetically (#7815)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Both
rewrite-pythonandrewrite-csharpalready ship a small test-only client that spawnsorg.openrewrite.maven.rpc.JavaRewriteRpcand talks to it over JSON-RPC. This gives those languages an easy way to drive real Java recipes (e.g.ChangeType,ChangeMethodName) from their native test runners — and, in production, lets a TS recipe delegate part of its work to a Java recipe.rewrite-javascriptdid not have an equivalent. Adding it unblocks TS-side tests for Java-ported recipes — most immediately the dependency-management recipes on #7795, where the TS-side recipe files have been deleted in favor of Java recipes and the tests need to verify them through RPC. It also gives TS recipes a stable way to delegate to Java recipes that works identically in test (TS spawned the JVM) and production (Java spawned the TS server).The key insight that keeps this small:
RewriteRpcis already symmetric — when given aMessageConnectionwired to a child process's stdio, the same class drives Java as a client. No new sender/receiver/codec logic needed; just the process lifecycle, classpath discovery, a vitest fixture for test ergonomics, and a small helper for the recipe-author surface.Examples
Tests
The intended consumer API uses the vitest fixture from
@openrewrite/rewrite/test/java-rpc:testJavaRpcis atest.extend-based vitest API. ThejavaRpcfixture combines:JavaRpcTestServer— one JVM per vitest worker, spawned lazily on first use, disposed on worker exit.RewriteRpc.reset()before every test so each test starts with both Java-side and TS-side state cleared.describeJavaRpcis adescribethat auto-skips when no Java classpath is configured. No boilerplatebeforeAll/afterAllper file.TS recipes delegating to Java
prepareJavaRecipe(id, options?)resolves the activeRewriteRpcvia the static accessor and routes aPrepareReciperequest to whichever side hosts the Java implementation:testJavaRpcfixturedist/rpc/server.jsSame code, both topologies:
The
RpcRecipereturned byprepareJavaRecipealready extendsRecipe, so it composes naturally — as the editor of acheck(usesType(...), ...)precondition, as a step inside arecipeList(), or assigned directly toRecipeSpec.recipe.Top-level functions per Java recipe (downstream pattern)
For Java recipes that are commonly used as building blocks, the natural pattern is to expose a top-level function:
Consumers compose these as recipes or as recipe-list steps:
This PR ships only the
prepareJavaRecipeprimitive — wrapper functions for specific recipes belong with the recipes they wrap and will land on #7795.Setup
./gradlew :rewrite-javascript:npmTestis wired to depend ongenerateTestClasspath, mirroring the existing C# setup. Devs runningnpx vitestdirectly still need to invoke the Gradle task once (or useREWRITE_JAVASCRIPT_CLASSPATHto override).Summary
:rewrite-javascript:generateTestClasspathGradle task writes the runtime + test classpath torewrite-javascript/rewrite/test-classpath.txt(gitignored). AddstestRuntimeOnly(project(":rewrite-maven"))soJavaRewriteRpc's main class is included.:rewrite-javascript:npmTestnow depends ongenerateTestClasspath, so the standard Gradle test entry point picks up the RPC tests automatically.rewrite-javascript/rewrite/src/rpc/java-rpc-client.ts:JavaRpcTestServer.start(opts?)spawnsjava -cp <classpath> org.openrewrite.maven.rpc.JavaRewriteRpc, wraps stdio in avscode-jsonrpcMessageConnection, and constructs aRewriteRpcagainst it.reset()delegates toRewriteRpc.reset();dispose()ends the connection, waits with a grace period before SIGKILL.[Java RPC]prefix; detects early JVM exit and surfaces it rather than letting the first request hang.findTestClasspath()checksREWRITE_JAVASCRIPT_CLASSPATHthen walks fortest-classpath.txt.RewriteRpc.reset()method mirroring Java's pattern: sendResetto the remote and clear local caches. The existing inboundResethandler still only clears local — no recursion.rewrite-javascript/rewrite/src/rpc/java-recipe.ts—prepareJavaRecipe(id, options?)helper for TS recipes that delegate to a Java recipe. Exported from@openrewrite/rewrite/rpc.rewrite-javascript/rewrite/src/test/java-rpc.ts—testJavaRpc+describeJavaRpcvitest fixture, exposed as the@openrewrite/rewrite/test/java-rpcsubpath export. Worker-scoped JVM, per-test reset, auto-skip when no classpath.rewrite-javascript/rewrite/test/rpc/java-recipe-via-rpc.test.tsexercising both the direct (javaRpc.rpc.prepareRecipe) and recipe-author (prepareJavaRecipe) surfaces againstorg.openrewrite.text.FindAndReplaceend-to-end.rewrite-javascript/rewrite/CLAUDE.md.Out of scope: wrapper functions / classes for specific Java recipes (
addDependency,changeType, etc.) — those land with the recipes themselves.Test plan
./gradlew :rewrite-javascript:generateTestClasspathproducesrewrite-javascript/rewrite/test-classpath.txt../gradlew :rewrite-javascript:npmTestauto-runsgenerateTestClasspathfirst.npx vitest run test/rpc/java-recipe-via-rpc.test.ts).test/rpc/suite still green (43 tests, 1 pre-existing skipped).npm run typecheckis clean.REWRITE_JAVASCRIPT_CLASSPATH= rm -f test-classpath.txt && npx vitest run test/rpc/java-recipe-via-rpc.test.ts).java ... JavaRewriteRpcprocesses after a test run.