/* * Copyright 2026 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://docs.moderne.io/licensing/moderne-source-available-license *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as rpc from "vscode-jsonrpc/node"; import * as fs from "fs"; import * as path from "path"; import {RpcObjectData, RpcSendQueue} from "../queue"; import {ReferenceMap, asRef} from "../../reference"; import {JavaSender} from "../../java/rpc"; import {Type} from "../../java"; import {withMetrics} from "./metrics"; import {lastParsedProject} from "./last-parsed-project"; const JAVA_TYPE_CLASS = "org.openrewrite.java.tree.JavaType$Class"; type ExportedTypes = typeof import("../../javascript/package-exported-types.js")["exportedTypes"]; /** * Handle a DependencyTypes request: resolve one dependency, named by its npm coordinate, against * the most recently parsed project's node_modules, enumerate its public types, and stream them * back terminated by END_OF_OBJECT. Built once, returned in {@code batchSize} slices across the * client's repeated pulls. A {@code null} version names a TypeScript runtime declaration file * (e.g. {@code lib.dom}) instead of an installed package. */ export class DependencyTypes { constructor(private readonly name: string, private readonly version?: string | null) { } static handle(connection: rpc.MessageConnection, batchSize: number, metricsCsv?: string): void { // Keyed by coordinate: holds the remaining slices of an in-flight // response across the client's repeated pulls. Evicted once fully drained. const pending = new Map(); connection.onRequest( new rpc.RequestType("DependencyTypes"), withMetrics( "DependencyTypes", metricsCsv, (context) => async (request) => { const version = request.version ?? null; context.target = version ? `${request.name}@${version}` : request.name; const key = JSON.stringify([request.name, version]); if (!pending.has(key)) { // Dynamic import to break the circular dependency with the javascript module. const {exportedTypes} = await import("../../javascript/package-exported-types.js"); const types = version !== null ? packageTypes(request.name, exportedTypes) : exportedTypes([runtimeLibFile(request.name)], []); const q = new RpcSendQueue(new ReferenceMap(), JAVA_TYPE_CLASS, false); const sender = new JavaSender(); // FQNs this dependency defines, sent before the types so the caller can tell // defined types from references up front, then the types themselves. await q.sendList(types.map(t => Type.FullyQualified.getFullyQualifiedName(t)), undefined, s => s); await q.sendList( types.map(t => asRef(t)), undefined, t => Type.signature(t), async t => { await sender.visitType(t, q); } ); pending.set(key, q.finish()); } const data = pending.get(key)!; const batch = data.splice(0, batchSize); if (data.length === 0) { pending.delete(key); } return batch; } ) ); } } function projectNodeModules(): string { const project = lastParsedProject(); if (!project) { throw new Error("No project has been parsed yet; cannot resolve a dependency coordinate"); } return path.join(project, "node_modules"); } /** The installed package's own types, falling back to its DefinitelyTyped package when it ships none. */ function packageTypes(name: string, exportedTypes: ExportedTypes): Type.FullyQualified[] { const nodeModules = projectNodeModules(); const pkgDir = path.join(nodeModules, ...name.split("/")); if (!fs.existsSync(pkgDir)) { throw new Error(`Package '${name}' is not installed under ${nodeModules}`); } const types = exportedTypes([pkgDir], []); if (types.length > 0) { return types; } // @scope/pkg -> @types/scope__pkg const typesDir = path.join(nodeModules, "@types", name.startsWith("@") ? name.slice(1).replace("/", "__") : name); return fs.existsSync(typesDir) ? exportedTypes([typesDir], []) : types; } /** A runtime declaration file (e.g. lib.dom.d.ts): the project's installed TypeScript first, else the engine's own. */ function runtimeLibFile(name: string): string { for (const lib of [ path.join(projectNodeModules(), "typescript", "lib", `${name}.d.ts`), path.join(path.dirname(require.resolve("typescript")), `${name}.d.ts`), ]) { if (fs.existsSync(lib)) { return lib; } } throw new Error(`No TypeScript declaration file for '${name}'`); }