@openrewrite/rewrite 8.91.3 → 8.91.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/javascript/index.d.ts +1 -1
  2. package/dist/javascript/index.d.ts.map +1 -1
  3. package/dist/javascript/index.js +2 -1
  4. package/dist/javascript/index.js.map +1 -1
  5. package/dist/javascript/parser.d.ts +3 -2
  6. package/dist/javascript/parser.d.ts.map +1 -1
  7. package/dist/javascript/parser.js +154 -114
  8. package/dist/javascript/parser.js.map +1 -1
  9. package/dist/javascript/remove-import.d.ts +0 -18
  10. package/dist/javascript/remove-import.d.ts.map +1 -1
  11. package/dist/javascript/remove-import.js +50 -438
  12. package/dist/javascript/remove-import.js.map +1 -1
  13. package/dist/javascript/scope.d.ts +6 -0
  14. package/dist/javascript/scope.d.ts.map +1 -1
  15. package/dist/javascript/scope.js +87 -7
  16. package/dist/javascript/scope.js.map +1 -1
  17. package/dist/javascript/tsconfig.d.ts +32 -0
  18. package/dist/javascript/tsconfig.d.ts.map +1 -0
  19. package/dist/javascript/tsconfig.js +205 -0
  20. package/dist/javascript/tsconfig.js.map +1 -0
  21. package/dist/rewrite-javascript-version.txt +1 -1
  22. package/dist/rpc/index.d.ts +1 -1
  23. package/dist/rpc/index.d.ts.map +1 -1
  24. package/dist/rpc/index.js +2 -1
  25. package/dist/rpc/index.js.map +1 -1
  26. package/dist/rpc/java-recipe.d.ts +32 -22
  27. package/dist/rpc/java-recipe.d.ts.map +1 -1
  28. package/dist/rpc/java-recipe.js +91 -30
  29. package/dist/rpc/java-recipe.js.map +1 -1
  30. package/dist/rpc/request/prepare-recipe.d.ts +10 -14
  31. package/dist/rpc/request/prepare-recipe.d.ts.map +1 -1
  32. package/dist/rpc/request/prepare-recipe.js +53 -88
  33. package/dist/rpc/request/prepare-recipe.js.map +1 -1
  34. package/dist/rpc/rewrite-rpc.d.ts +5 -12
  35. package/dist/rpc/rewrite-rpc.d.ts.map +1 -1
  36. package/dist/rpc/rewrite-rpc.js +5 -12
  37. package/dist/rpc/rewrite-rpc.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/javascript/index.ts +1 -1
  40. package/src/javascript/parser.ts +104 -58
  41. package/src/javascript/remove-import.ts +50 -424
  42. package/src/javascript/scope.ts +103 -7
  43. package/src/javascript/tsconfig.ts +177 -0
  44. package/src/rpc/index.ts +1 -1
  45. package/src/rpc/java-recipe.ts +107 -33
  46. package/src/rpc/request/prepare-recipe.ts +55 -90
  47. package/src/rpc/rewrite-rpc.ts +5 -12
@@ -152,6 +152,69 @@ export function namesDeclaredWithin(node: unknown, cacheKey: object = node as ob
152
152
  return names;
153
153
  }
154
154
 
155
+ /**
156
+ * The names `node` reads and nothing within it binds, so each one reaches a binding further out.
157
+ * A name its parent introduces rather than reads is not one, nor is a name an inner scope rebinds:
158
+ * that reference reads the rebinding.
159
+ */
160
+ export function namesReferencedWithin(node: unknown, cacheKey: object = node as object): ReadonlySet<string> {
161
+ if (cacheKey === null || typeof cacheKey !== 'object') {
162
+ return noNames;
163
+ }
164
+ const cached = referenced.get(cacheKey);
165
+ if (cached) {
166
+ return cached;
167
+ }
168
+ const names = new Set<string>();
169
+ collectReferences(node, undefined, undefined, names);
170
+ referenced.set(cacheKey, names);
171
+ return names;
172
+ }
173
+
174
+ /** The scopes enclosing a node, innermost first, each paired with what {@link frameBindings} reads. */
175
+ interface Frames {
176
+ node: unknown;
177
+ parent: unknown;
178
+ outer?: Frames;
179
+ }
180
+
181
+ /** Whether a scope between the name and where the walk began binds it. */
182
+ function shadowed(scopes: Frames | undefined, name: string): boolean {
183
+ for (let scope = scopes; scope; scope = scope.outer) {
184
+ if (frameBindings(scope.node, scope.parent).has(name)) {
185
+ return true;
186
+ }
187
+ }
188
+ return false;
189
+ }
190
+
191
+ function collectReferences(node: unknown, parent: unknown, scopes: Frames | undefined, names: Set<string>): void {
192
+ if (Array.isArray(node)) {
193
+ node.forEach(child => collectReferences(child, parent, scopes, names));
194
+ return;
195
+ }
196
+ const kind = (node as { kind?: string } | undefined)?.kind;
197
+ if (kind === J.Kind.RightPadded || kind === J.Kind.LeftPadded) {
198
+ // A naming position reads against the tree parent, so padding forwards the one it was handed.
199
+ collectReferences((node as J.RightPadded<any>).element, parent, scopes, names);
200
+ return;
201
+ }
202
+ if (kind === J.Kind.Container) {
203
+ collectReferences((node as J.Container<any>).elements, parent, scopes, names);
204
+ return;
205
+ }
206
+ if (!isTree(node)) {
207
+ return;
208
+ }
209
+ const name = kind === J.Kind.Identifier ? (node as J.Identifier).simpleName : undefined;
210
+ if (name && reads(node as J.Identifier, parent) && !shadowed(scopes, name)) {
211
+ names.add(name);
212
+ }
213
+ const within = scopeKinds.has(kind!) ? {node, parent, outer: scopes} : scopes;
214
+ Object.entries(node as object).forEach(([key, value]) =>
215
+ key !== 'markers' && collectReferences(value, node, within, names));
216
+ }
217
+
155
218
  /**
156
219
  * Every name a binding pattern introduces. `member` is the property a name takes its value from,
157
220
  * which only a name an object pattern binds directly has: anything deeper reads a property of a
@@ -213,8 +276,12 @@ function frameBindings(node: any, parent: any): ReadonlySet<string> {
213
276
  // A function expression is the only function whose own name its body reaches: a declaration's
214
277
  // name belongs to the enclosing block, a method's to an instance.
215
278
  if (node?.kind === J.Kind.MethodDeclaration && parent?.kind === JS.Kind.StatementExpression) {
216
- const self = bindingNames((node as J.MethodDeclaration).name).map(bound => bound.name);
217
- return new Set([...self, ...ownBindings(node)]);
279
+ let names = selfNamed.get(node);
280
+ if (!names) {
281
+ const self = bindingNames((node as J.MethodDeclaration).name).map(bound => bound.name);
282
+ selfNamed.set(node, names = new Set([...self, ...ownBindings(node)]));
283
+ }
284
+ return names;
218
285
  }
219
286
  return ownBindings(node);
220
287
  }
@@ -369,7 +436,9 @@ const blockScoped = new Set(['let', 'const', 'using']);
369
436
  const hoisted = new WeakMap<object, string[]>();
370
437
  const declared = new WeakMap<object, ReadonlySet<string>>();
371
438
  const used = new WeakMap<object, ReadonlySet<string>>();
439
+ const referenced = new WeakMap<object, ReadonlySet<string>>();
372
440
  const frames = new WeakMap<object, ReadonlySet<string>>();
441
+ const selfNamed = new WeakMap<object, ReadonlySet<string>>();
373
442
 
374
443
  /**
375
444
  * The names blocks under `scope` hoist out to it. A `var` or function declaration reaches the whole
@@ -496,19 +565,46 @@ export function isValueReference(cursor: Cursor, identifier: J.Identifier): bool
496
565
  while (c && isPadding(c.value)) {
497
566
  c = c.parent;
498
567
  }
499
- const parent = c?.value as
500
- { kind?: string; name?: unknown; key?: unknown; label?: unknown; select?: unknown } | undefined;
568
+ return references(identifier, c?.value);
569
+ }
570
+
571
+ /** The position half of {@link isValueReference}, against a parent already found. */
572
+ function references(identifier: J.Identifier, parent: unknown): boolean {
573
+ const owner = parent as {
574
+ kind?: string; name?: unknown; key?: unknown; label?: unknown; select?: unknown;
575
+ propertyName?: unknown;
576
+ } | undefined;
501
577
 
502
578
  // A call names a member of whatever it selects from. With nothing selected there is no member,
503
579
  // and its `name` is a reference to the function being called.
504
- if (parent?.kind === J.Kind.MethodInvocation && !parent.select) {
580
+ if (owner?.kind === J.Kind.MethodInvocation && !owner.select) {
505
581
  return true;
506
582
  }
507
583
 
584
+ // A binding element names the property it destructures and binds under `name`, so both slots
585
+ // name rather than reference.
586
+ if (owner?.kind === JS.Kind.BindingElement && holds(owner.propertyName, identifier)) {
587
+ return false;
588
+ }
589
+
508
590
  // A JSX attribute keeps its prop name on `key`; the three label-bearing nodes keep theirs on
509
591
  // `label`. Every other kind that names rather than references keeps it on `name`.
510
- const named = parent?.kind === JS.Kind.JsxAttribute ? parent.key : (parent?.name ?? parent?.label);
511
- return named !== identifier && (named as { element?: unknown } | undefined)?.element !== identifier;
592
+ return !holds(owner?.kind === JS.Kind.JsxAttribute ? owner.key : (owner?.name ?? owner?.label), identifier);
593
+ }
594
+
595
+ /** Whether a node slot, padded or not, is the identifier itself. */
596
+ function holds(slot: unknown, identifier: J.Identifier): boolean {
597
+ return slot === identifier || (slot as { element?: unknown } | undefined)?.element === identifier;
598
+ }
599
+
600
+ /**
601
+ * As {@link references}, for a collector rather than a renamer: a shorthand property's name slot is
602
+ * also the value it reads, which a rename has to expand rather than follow.
603
+ */
604
+ function reads(identifier: J.Identifier, parent: unknown): boolean {
605
+ const owner = parent as { kind?: string; initializer?: unknown } | undefined;
606
+ return (owner?.kind === JS.Kind.PropertyAssignment && owner.initializer === undefined) ||
607
+ references(identifier, parent);
512
608
  }
513
609
 
514
610
  function isPadding(value: unknown): boolean {
@@ -0,0 +1,177 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import ts from "typescript";
17
+ import * as path from "path";
18
+
19
+ /**
20
+ * What a project gets to decide: where a module specifier resolves to, and which ambient
21
+ * declarations are in scope. The shape of the types themselves stays with the parser — `target`
22
+ * and `lib` bound the global scope, `strict` adds a `null` to every nullable type — which pins
23
+ * the widest view it can rather than the one a project compiles against.
24
+ */
25
+ const RESOLUTION_OPTIONS: string[] = [
26
+ "baseUrl",
27
+ "paths",
28
+ // Internal companion to `paths`: the directory its entries resolve against.
29
+ "pathsBasePath",
30
+ "rootDirs",
31
+ "types",
32
+ "typeRoots",
33
+ "moduleResolution",
34
+ "moduleSuffixes",
35
+ "customConditions",
36
+ "resolvePackageJsonExports",
37
+ "resolvePackageJsonImports",
38
+ "resolveJsonModule",
39
+ ];
40
+
41
+ /**
42
+ * Config file names in precedence order. TypeScript reads jsconfig.json for JavaScript
43
+ * projects, which is where a JS-only codebase states its `paths`.
44
+ */
45
+ const CONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
46
+
47
+ export interface ResolvedTsConfig {
48
+ /**
49
+ * Absolute path of the config file the options came from. Undefined when no project
50
+ * config applied, in which case {@link options} are the parser's defaults.
51
+ */
52
+ readonly configFilePath?: string;
53
+ readonly options: ts.CompilerOptions;
54
+ }
55
+
56
+ /**
57
+ * Finds the config file governing each file and merges its resolution options over a set of
58
+ * parser defaults, so that type-aware recipes see the same modules `tsc` would.
59
+ */
60
+ export class TsConfigResolver {
61
+ private readonly configByDir = new Map<string, string | undefined>();
62
+ private readonly optionsByConfig = new Map<string, ts.CompilerOptions | undefined>();
63
+ private readonly root?: string;
64
+
65
+ /**
66
+ * @param defaults Options to fall back on, and to merge a project config over.
67
+ * @param relativeTo Root of the project being parsed, bounding the search. Without one,
68
+ * walking up from the process working directory picks up whatever config sits above it.
69
+ */
70
+ constructor(private readonly defaults: ts.CompilerOptions, relativeTo?: string) {
71
+ this.root = relativeTo ? path.resolve(relativeTo) : undefined;
72
+ }
73
+
74
+ forFile(filePath: string): ResolvedTsConfig {
75
+ const configFilePath = this.findConfig(path.dirname(path.resolve(filePath)));
76
+ const options = configFilePath === undefined ? undefined : this.optionsFor(configFilePath);
77
+ return options === undefined ? {options: this.defaults} : {configFilePath, options};
78
+ }
79
+
80
+ /** The nearest config file at or below the project root, searching upwards. */
81
+ private findConfig(from: string): string | undefined {
82
+ if (!this.root || !isAtOrUnder(from, this.root)) {
83
+ return undefined;
84
+ }
85
+
86
+ const searched: string[] = [];
87
+ const memoize = (found: string | undefined) => {
88
+ for (const dir of searched) {
89
+ this.configByDir.set(dir, found);
90
+ }
91
+ return found;
92
+ };
93
+
94
+ for (let dir = from; ; dir = path.dirname(dir)) {
95
+ if (this.configByDir.has(dir)) {
96
+ return memoize(this.configByDir.get(dir));
97
+ }
98
+ searched.push(dir);
99
+
100
+ for (const configFileName of CONFIG_FILE_NAMES) {
101
+ const candidate = path.join(dir, configFileName);
102
+ if (ts.sys.fileExists(candidate)) {
103
+ return memoize(candidate);
104
+ }
105
+ }
106
+ if (dir === this.root || path.dirname(dir) === dir) {
107
+ return memoize(undefined);
108
+ }
109
+ }
110
+ }
111
+
112
+ private optionsFor(configFilePath: string): ts.CompilerOptions | undefined {
113
+ if (!this.optionsByConfig.has(configFilePath)) {
114
+ this.optionsByConfig.set(configFilePath, this.merge(configFilePath));
115
+ }
116
+ return this.optionsByConfig.get(configFilePath);
117
+ }
118
+
119
+ /** Undefined where the config cannot be read, leaving the caller on its defaults. */
120
+ private merge(configFilePath: string): ts.CompilerOptions | undefined {
121
+ let projectOptions: ts.CompilerOptions;
122
+ try {
123
+ const {config, error} = ts.readConfigFile(configFilePath, ts.sys.readFile);
124
+ if (error || !config) {
125
+ return undefined;
126
+ }
127
+ // `readDirectory` answers empty because only `options` is read here; left to `ts.sys` it
128
+ // walks the whole tree under the config to build a file list, once per config found.
129
+ const parseConfigHost: ts.ParseConfigHost = {...ts.sys, readDirectory: () => []};
130
+ projectOptions = ts.parseJsonConfigFileContent(
131
+ config, parseConfigHost, path.dirname(configFilePath), undefined, configFilePath).options;
132
+ } catch {
133
+ return undefined;
134
+ }
135
+
136
+ // A project's `baseUrl` stands alone, including where it states none: `baseUrl` takes
137
+ // precedence over `pathsBasePath`, so a default rooted at the repository would resolve a
138
+ // nested project's `paths` against the wrong directory.
139
+ const {baseUrl: _, ...defaults} = this.defaults;
140
+
141
+ const resolution: ts.CompilerOptions = {};
142
+ for (const option of RESOLUTION_OPTIONS) {
143
+ if (projectOptions[option] !== undefined) {
144
+ resolution[option] = projectOptions[option];
145
+ }
146
+ }
147
+ if (resolution.moduleResolution === undefined) {
148
+ const paired = pairedModuleResolution(projectOptions);
149
+ if (paired !== undefined) {
150
+ resolution.moduleResolution = paired;
151
+ }
152
+ }
153
+ return {...defaults, ...resolution};
154
+ }
155
+ }
156
+
157
+ /**
158
+ * The resolution TypeScript infers for a project that states only its `module`. Only the
159
+ * inference is adopted, since a module kind such as `node16` classifies files as CJS or ESM,
160
+ * and an ESM-only import from a CJS file is then the error TS1479, costing the file its LST.
161
+ * `Classic` is declined as well, searching no `node_modules`, so the default stands in.
162
+ */
163
+ function pairedModuleResolution(projectOptions: ts.CompilerOptions): ts.ModuleResolutionKind | undefined {
164
+ const infer = (ts as Partial<{ getEmitModuleResolutionKind(options: ts.CompilerOptions): ts.ModuleResolutionKind }>)
165
+ .getEmitModuleResolutionKind;
166
+ if (projectOptions.module === undefined || !infer) {
167
+ return undefined;
168
+ }
169
+ const inferred = infer(projectOptions);
170
+ return inferred === ts.ModuleResolutionKind.Classic ? undefined : inferred;
171
+ }
172
+
173
+ function isAtOrUnder(dir: string, root: string): boolean {
174
+ const relative = path.relative(root, dir);
175
+ return relative === "" ||
176
+ (relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative));
177
+ }
package/src/rpc/index.ts CHANGED
@@ -33,7 +33,7 @@ export * from "./queue";
33
33
  export * from "../reference";
34
34
  export {RewriteRpc} from "./rewrite-rpc";
35
35
  export {RpcRecipe, RpcVisitor} from "./recipe";
36
- export {prepareJavaRecipe} from "./java-recipe";
36
+ export {DelegatingRecipe, prepareJavaRecipe} from "./java-recipe";
37
37
  export {registerVisitor} from "./request/visitor-registry";
38
38
 
39
39
  RpcCodecs.registerCodec(TreeKind.Checksum, {
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2025 the original author or authors.
2
+ * Copyright 2026 the original author or authors.
3
3
  * <p>
4
4
  * Licensed under the Moderne Source Available License (the "License");
5
5
  * you may not use this file except in compliance with the License.
@@ -15,23 +15,110 @@
15
15
  */
16
16
  import {RewriteRpc} from "./rewrite-rpc";
17
17
  import type {RpcRecipe} from "./recipe";
18
+ import {Recipe, RecipeDescriptor, ScanningRecipe} from "../recipe";
19
+ import {TreeVisitor} from "../visitor";
20
+ import {ExecutionContext} from "../execution";
21
+ import {SourceFile} from "../tree";
18
22
 
19
23
  /**
20
- * Prepare a Java recipe via the active {@link RewriteRpc} connection.
21
- *
22
- * Routes a `PrepareRecipe` request to whichever side hosts the Java
23
- * implementation:
24
- * - in production, the Java host that spawned the TS server (`dist/rpc/server.js`);
25
- * - in tests, the JVM spawned via `JavaRpcTestServer` (see
26
- * `@openrewrite/rewrite/test/java-rpc`).
27
- *
28
- * The returned {@link RpcRecipe} extends `Recipe`, so it can be used directly
29
- * as a recipe — e.g. assigned to `RecipeSpec.recipe` — or composed inside a
30
- * larger TS recipe (as the editor of a `check(usesType(...), ...)`
31
- * precondition, or as a step in `recipeList()`).
32
- *
33
- * Top-level convenience functions for specific Java recipes (e.g.
34
- * `addDependency`, `changeType`) sit naturally on top of this primitive:
24
+ * A stand-in for a Java recipe, composable like any other recipe (a `recipeList()` entry, a
25
+ * `check(...)` gate, a `RecipeSpec.recipe`). The Java host resolves it by name: from `delegatesTo`
26
+ * when it prepares the enclosing recipe, or by class name when it gates a visitor, like a `RecipeRef`.
27
+ * Only when this process runs it itself (a test, a JS-hosted run) is it prepared over the active
28
+ * {@link RewriteRpc} connection, on first use.
29
+ */
30
+ export class DelegatingRecipe extends ScanningRecipe<number> {
31
+ readonly name: string;
32
+ readonly displayName: string;
33
+ readonly description: string;
34
+ private delegate?: {rpc: RewriteRpc, recipe: Promise<RpcRecipe>};
35
+
36
+ constructor(readonly javaRecipeName: string,
37
+ readonly delegatesToOptions: Record<string, any>) {
38
+ super();
39
+ this.name = javaRecipeName;
40
+ this.displayName = javaRecipeName;
41
+ this.description = `Delegates to the Java recipe \`${javaRecipeName}\`.`;
42
+ }
43
+
44
+ // Local, with an empty recipe list: the host fills the subtree in, and recipeList() prepares the delegate.
45
+ async descriptor(): Promise<RecipeDescriptor> {
46
+ return {
47
+ name: this.name,
48
+ displayName: this.displayName,
49
+ instanceName: this.instanceName(),
50
+ description: this.description,
51
+ tags: this.tags,
52
+ estimatedEffortPerOccurrence: this.estimatedEffortPerOccurrence,
53
+ options: Object.entries(this.delegatesToOptions).map(([name, value]) => ({
54
+ name,
55
+ value,
56
+ displayName: name,
57
+ description: "",
58
+ required: false
59
+ })),
60
+ preconditions: [],
61
+ recipeList: [],
62
+ dataTables: this.dataTables,
63
+ maintainers: [],
64
+ contributors: [],
65
+ examples: []
66
+ };
67
+ }
68
+
69
+ async recipeList(): Promise<Recipe[]> {
70
+ return (await this.prepared()).recipeList();
71
+ }
72
+
73
+ initialValue(_ctx: ExecutionContext): number {
74
+ return 0;
75
+ }
76
+
77
+ async editorWithData(acc: number): Promise<TreeVisitor<any, ExecutionContext>> {
78
+ return (await this.prepared()).editorWithData(acc);
79
+ }
80
+
81
+ async scanner(acc: number): Promise<TreeVisitor<any, ExecutionContext>> {
82
+ return (await this.prepared()).scanner(acc);
83
+ }
84
+
85
+ async generate(acc: number, ctx: ExecutionContext): Promise<SourceFile[]> {
86
+ return (await this.prepared()).generate(acc, ctx);
87
+ }
88
+
89
+ async onComplete(ctx: ExecutionContext): Promise<void> {
90
+ if (this.delegate) {
91
+ await (await this.delegate.recipe).onComplete(ctx);
92
+ }
93
+ }
94
+
95
+ private prepared(): Promise<RpcRecipe> {
96
+ const rpc = RewriteRpc.get();
97
+ if (!rpc) {
98
+ throw new Error(
99
+ `Cannot run Java recipe "${this.javaRecipeName}": no active RewriteRpc connection.\n` +
100
+ " • Tests: spawn one via JavaRpcTestServer.start() — see " +
101
+ "@openrewrite/rewrite/test/java-rpc.\n" +
102
+ " • Production: the Java host provides one automatically when it " +
103
+ "loads the TS recipe artifact."
104
+ );
105
+ }
106
+ // Prepared once per connection; a failed prepare is dropped so the next use retries.
107
+ if (!this.delegate || this.delegate.rpc !== rpc) {
108
+ const recipe = rpc.prepareRecipe(this.javaRecipeName, this.delegatesToOptions);
109
+ this.delegate = {rpc, recipe};
110
+ recipe.catch(() => {
111
+ if (this.delegate?.recipe === recipe) {
112
+ this.delegate = undefined;
113
+ }
114
+ });
115
+ }
116
+ return this.delegate.recipe;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Reference a Java recipe by name from a TS recipe:
35
122
  *
36
123
  * ```ts
37
124
  * export function addDependency(options: AddDependencyOptions): Promise<Recipe> {
@@ -39,26 +126,13 @@ import type {RpcRecipe} from "./recipe";
39
126
  * }
40
127
  * ```
41
128
  *
42
- * There is no in-process fallback by design: a Java recipe's editor is the
43
- * single source of truth — we don't reimplement editing recipes in TS. For
44
- * preconditions that need a graceful no-RPC fallback, use the {@link RecipeRef}
129
+ * There is no in-process fallback by design: a Java recipe's editor is the single source of
130
+ * truth. For preconditions that need a graceful no-RPC fallback, use the {@link RecipeRef}
45
131
  * pattern via `usesType` / `usesMethod`, which carries a `localVisitor`.
46
- *
47
- * @throws Error when no active {@link RewriteRpc} connection is registered.
48
132
  */
49
133
  export async function prepareJavaRecipe(
50
134
  id: string,
51
135
  options?: Record<string, any>,
52
- ): Promise<RpcRecipe> {
53
- const rpc = RewriteRpc.get();
54
- if (!rpc) {
55
- throw new Error(
56
- `Cannot prepare Java recipe "${id}": no active RewriteRpc connection.\n` +
57
- " • Tests: spawn one via JavaRpcTestServer.start() — see " +
58
- "@openrewrite/rewrite/test/java-rpc.\n" +
59
- " • Production: the Java host provides one automatically when it " +
60
- "loads the TS recipe artifact."
61
- );
62
- }
63
- return rpc.prepareRecipe(id, options);
136
+ ): Promise<DelegatingRecipe> {
137
+ return new DelegatingRecipe(id, options ?? {});
64
138
  }