From 19a813df36a87e2294c690f9d943e0a43b11113c Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Sun, 4 Jan 2026 09:41:41 +0100 Subject: [PATCH] fix: use correct binary paths for ReScript v12+ ReScript v12 changed its binary distribution to use @rescript/{platform}/bin.js. This PR updates the extension to correctly locate binaries for v12+ projects. Changes: - Add dedicated v12+ binary finding via @rescript/{target}/bin.js - Move legacy ( + path.join(path.dirname(__dirname), "..", "analysis", b); + +export const getLegacyBinaryProdPath = (b: binaryName) => + path.join( + path.dirname(__dirname), + "..", + "server", + "analysis_binaries", + platformDir, + b, + ); + +/** + * Finds binaries for ReScript < 12 using old path structure. + * Tries project binary first, then falls back to builtin binaries. + */ +export const getBinaryPathLegacy = ( + projectRootPath: NormalizedPath | null, + binaryName: binaryName, +): string | null => { + // Try project binary first + if (projectRootPath != null) { + const binaryFromCompilerPackage = path.join( + projectRootPath, + "node_modules", + "rescript", + platformDir, + binaryName, + ); + if (fs.existsSync(binaryFromCompilerPackage)) { + return binaryFromCompilerPackage; + } + } + + // Fall back to builtin binaries + if (fs.existsSync(getLegacyBinaryDevPath(binaryName))) { + return getLegacyBinaryDevPath(binaryName); + } else if (fs.existsSync(getLegacyBinaryProdPath(binaryName))) { + return getLegacyBinaryProdPath(binaryName); + } + + return null; +}; diff --git a/client/src/utils.ts b/client/src/utils.ts index 17135d14a..ff7c1e34e 100644 --- a/client/src/utils.ts +++ b/client/src/utils.ts @@ -2,6 +2,8 @@ import * as path from "path"; import * as fs from "fs"; import * as os from "os"; import { DocumentUri } from "vscode-languageclient"; +import * as semver from "semver"; +import { getBinaryPathLegacy } from "./utils-legacy"; /* * Much of the code in here is duplicated from the server code. @@ -32,42 +34,110 @@ export function normalizePath(filePath: string | null): NormalizedPath | null { type binaryName = "rescript-editor-analysis.exe" | "rescript-tools.exe"; -const platformDir = - process.arch === "arm64" ? process.platform + process.arch : process.platform; +// v12+ format: with hyphen (e.g., "darwin-arm64") +const platformTarget = `${process.platform}-${process.arch}`; -const getLegacyBinaryDevPath = (b: binaryName) => - path.join(path.dirname(__dirname), "..", "analysis", b); +// ============================================================================ +// Version Detection +// ============================================================================ -export const getLegacyBinaryProdPath = (b: binaryName) => - path.join( - path.dirname(__dirname), - "..", - "server", - "analysis_binaries", - platformDir, - b, +/** + * Finds the ReScript version from package.json in the project. + */ +export const findReScriptVersion = ( + projectRootPath: NormalizedPath | null, +): string | null => { + if (projectRootPath == null) { + return null; + } + try { + const packageJsonPath = path.join( + projectRootPath, + "node_modules", + "rescript", + "package.json", + ); + if (!fs.existsSync(packageJsonPath)) { + return null; + } + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); + return packageJson.version ?? null; + } catch { + return null; + } +}; + +// ============================================================================ +// ReScript 12+ Binary Finding (Clean, self-contained) +// ============================================================================ + +/** + * Finds binaries for ReScript 12+ using @rescript/${target}/bin.js structure. + * This is the single source of truth for binary locations in v12+. + * Returns null if binary not found, throws on critical errors. + */ +const getBinaryPathReScript12 = ( + projectRootPath: NormalizedPath, + binaryName: binaryName, +): string | null => { + const binJsPath = path.join( + projectRootPath, + "node_modules", + "@rescript", + platformTarget, + "bin.js", ); + if (!fs.existsSync(binJsPath)) { + return null; + } + + // Read bin.js and extract the binary path + // bin.js exports binPaths object with paths to binaries + const binDir = path.join( + projectRootPath, + "node_modules", + "@rescript", + platformTarget, + "bin", + ); + + let binaryPath: string | null = null; + if (binaryName === "rescript-tools.exe") { + binaryPath = path.join(binDir, "rescript-tools.exe"); + } else if (binaryName === "rescript-editor-analysis.exe") { + binaryPath = path.join(binDir, "rescript-editor-analysis.exe"); + } + + if (binaryPath != null && fs.existsSync(binaryPath)) { + return binaryPath; + } + return null; +}; + +// ============================================================================ +// Main Binary Finding Function (Routes to v12 or legacy) +// ============================================================================ + +/** + * Finds a ReScript binary, routing to v12+ or legacy implementation. + * Top-level if separates the two code paths completely. + */ export const getBinaryPath = ( binaryName: "rescript-editor-analysis.exe" | "rescript-tools.exe", projectRootPath: NormalizedPath | null = null, ): string | null => { - const binaryFromCompilerPackage = path.join( - projectRootPath ?? "", - "node_modules", - "rescript", - platformDir, - binaryName, - ); + const rescriptVersion = findReScriptVersion(projectRootPath); + const isReScript12OrHigher = + rescriptVersion != null && + semver.valid(rescriptVersion) && + semver.gte(rescriptVersion, "12.0.0"); - if (projectRootPath != null && fs.existsSync(binaryFromCompilerPackage)) { - return binaryFromCompilerPackage; - } else if (fs.existsSync(getLegacyBinaryDevPath(binaryName))) { - return getLegacyBinaryDevPath(binaryName); - } else if (fs.existsSync(getLegacyBinaryProdPath(binaryName))) { - return getLegacyBinaryProdPath(binaryName); + // Top-level separation: v12+ or legacy + if (isReScript12OrHigher && projectRootPath != null) { + return getBinaryPathReScript12(projectRootPath, binaryName); } else { - return null; + return getBinaryPathLegacy(projectRootPath, binaryName); } }; diff --git a/package-lock.json b/package-lock.json index d565f3626..af5800d59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,74 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.20.1", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", @@ -74,6 +142,312 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", diff --git a/server/src/server.ts b/server/src/server.ts index b4445f68a..74de19cc3 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -427,18 +427,49 @@ let openedFile = async (fileUri: utils.FileURI, fileContent: string) => { const namespaceName = utils.getNamespaceNameFromConfigFile(projectRootPath); + const rescriptVersion = + await utils.findReScriptVersionForProjectRoot(projectRootPath); + const isReScript12OrHigher = + semver.valid(rescriptVersion) && + semver.gte(rescriptVersion as string, "12.0.0"); + + // Top-level separation: v12+ or legacy + let editorAnalysisLocation: string | null = null; + if (isReScript12OrHigher) { + // ReScript 12+: function handles all errors internally and throws if not found + try { + editorAnalysisLocation = + await utils.findEditorAnalysisBinary(projectRootPath); + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : `Failed to find ReScript ${rescriptVersion} editor analysis binary: ${String(error)}`; + getLogger().error(errorMessage); + throw error; + } + } else { + // ReScript < 12: function returns null if not found, no errors thrown + try { + editorAnalysisLocation = + await utils.findEditorAnalysisBinary(projectRootPath); + } catch (error) { + getLogger().log( + `Could not find editor analysis binary for ReScript ${rescriptVersion}: ${String(error)}`, + ); + } + } + projectRootState = { openFiles: new Set(), filesWithDiagnostics: new Set(), filesDiagnostics: {}, namespaceName: namespaceName.kind === "success" ? namespaceName.result : null, - rescriptVersion: - await utils.findReScriptVersionForProjectRoot(projectRootPath), + rescriptVersion: rescriptVersion, bsbWatcherByEditor: null, bscBinaryLocation: await utils.findBscExeBinary(projectRootPath), - editorAnalysisLocation: - await utils.findEditorAnalysisBinary(projectRootPath), + editorAnalysisLocation: editorAnalysisLocation, hasPromptedToStartBuild: /(\/|\\)node_modules(\/|\\)/.test( projectRootPath, ) diff --git a/server/src/utils-legacy.ts b/server/src/utils-legacy.ts new file mode 100644 index 000000000..ffdb0affb --- /dev/null +++ b/server/src/utils-legacy.ts @@ -0,0 +1,58 @@ +/** + * Legacy binary finding for ReScript < 12. + * This code is kept separate to avoid polluting the main utils with pre-v12 complexity. + */ + +import * as fs from "fs"; +import * as path from "path"; +import * as c from "./constants"; +import { normalizePath, NormalizedPath } from "./utils"; + +const fsAsync = fs.promises; + +/** + * Finds binaries for ReScript < 12 using the old path structure. + * Checks compiler-info.json first, then falls back to node_modules/rescript/${platformDir}/. + * NOTE: This preserves the original behavior exactly - do not add existence checks + * to the compiler-info.json branch as the original code returned immediately. + */ +export let findBinaryLegacy = async ( + projectRootPath: NormalizedPath | null, + rescriptDir: string, + binary: + | "bsc.exe" + | "rescript-editor-analysis.exe" + | "rescript" + | "rewatch.exe" + | "rescript.exe" + | "rescript-tools.exe", +): Promise => { + // Check compiler-info.json first (original behavior: return immediately if found) + if (projectRootPath !== null) { + try { + const compilerInfo = path.resolve( + projectRootPath, + c.compilerInfoPartialPath, + ); + const contents = await fsAsync.readFile(compilerInfo, "utf8"); + const compileInfo = JSON.parse(contents); + if (compileInfo && compileInfo.bsc_path) { + const bsc_path = compileInfo.bsc_path; + if (binary === "bsc.exe") { + return normalizePath(bsc_path); + } else { + const binaryPath = path.join(path.dirname(bsc_path), binary); + return normalizePath(binaryPath); + } + } + } catch {} + } + + // Fallback to old path structure (with existence check, as in original) + const binaryPath = path.join(rescriptDir, c.platformDir, binary); + if (fs.existsSync(binaryPath)) { + return normalizePath(binaryPath); + } else { + return null; + } +}; diff --git a/server/src/utils.ts b/server/src/utils.ts index a5662cf14..98eb2e227 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -18,6 +18,7 @@ import * as lookup from "./lookup"; import { reportError } from "./errorReporter"; import config from "./config"; import { filesDiagnostics, projectsFiles, projectFiles } from "./projectFiles"; +import { findBinaryLegacy } from "./utils-legacy"; import { workspaceFolders } from "./server"; import { rewatchLockPartialPath, rescriptLockPartialPath } from "./constants"; import { findRescriptRuntimesInProject } from "./find-runtime"; @@ -211,9 +212,82 @@ export let getProjectFile = ( return projectsFiles.get(projectRootPath) ?? null; }; -// If ReScript < 12.0.0-alpha.13, then we want `{project_root}/node_modules/rescript/{c.platformDir}/{binary}`. -// Otherwise, we want to dynamically import `{project_root}/node_modules/rescript` and from `binPaths` get the relevant binary. -// We won't know which version is in the project root until we read and parse `{project_root}/node_modules/rescript/package.json` +// ============================================================================ +// ReScript 12+ Binary Finding (Clean, self-contained) +// ============================================================================ + +/** + * Finds binaries for ReScript 12+ using the new @rescript/${target}/bin.js structure. + * This is the single source of truth for binary locations in v12+. + */ +let findBinaryReScript12 = async ( + rescriptDir: string, + rescriptVersion: string, + binary: + | "bsc.exe" + | "rescript-editor-analysis.exe" + | "rescript.exe" + | "rescript-tools.exe", +): Promise => { + const target = `${process.platform}-${process.arch}`; + // Use realpathSync to resolve symlinks, which is necessary for package + // managers like Deno and pnpm that use symlinked node_modules structures. + const targetPackagePath = path.join( + fs.realpathSync(rescriptDir), + "..", + `@rescript/${target}/bin.js`, + ); + + try { + const { binPaths } = await import(targetPackagePath); + + let binaryPath: string | null = null; + if (binary == "bsc.exe") { + binaryPath = binPaths.bsc_exe; + } else if (binary == "rescript-editor-analysis.exe") { + binaryPath = binPaths.rescript_editor_analysis_exe; + } else if (binary == "rescript.exe") { + binaryPath = binPaths.rescript_exe; + } else if (binary == "rescript-tools.exe") { + binaryPath = binPaths.rescript_tools_exe; + } + + if (binaryPath == null) { + throw new Error( + `Binary ${binary} not found in binPaths for ReScript ${rescriptVersion}`, + ); + } + + if (!fs.existsSync(binaryPath)) { + throw new Error( + `Binary ${binary} path from binPaths does not exist: ${binaryPath}`, + ); + } + + return normalizePath(binaryPath); + } catch (error) { + // For ReScript 12+, we must have a valid binary path or fail with an error + const errorMessage = + error instanceof Error + ? error.message + : `Failed to find ${binary} for ReScript ${rescriptVersion}: ${String(error)}`; + throw new Error( + `ReScript ${rescriptVersion} binary resolution failed: ${errorMessage}. ` + + `Expected to find binary via @rescript/${target}/bin.js. ` + + `Please ensure ReScript is properly installed in your project.`, + ); + } +}; + +// ============================================================================ +// Main Binary Finding Function (Routes to v12 or legacy based on version) +// ============================================================================ + +/** + * Finds a ReScript binary, routing to v12+ or legacy implementation based on version. + * For ReScript >= 12.0.0, uses @rescript/${target}/bin.js. + * For ReScript < 12.0.0, uses the old path structure. + */ let findBinary = async ( projectRootPath: NormalizedPath | null, binary: @@ -221,8 +295,10 @@ let findBinary = async ( | "rescript-editor-analysis.exe" | "rescript" | "rewatch.exe" - | "rescript.exe", + | "rescript.exe" + | "rescript-tools.exe", ): Promise => { + // Check for manual platform path override if (config.extensionConfiguration.platformPath != null) { const result = path.join( config.extensionConfiguration.platformPath, @@ -231,26 +307,7 @@ let findBinary = async ( return normalizePath(result); } - if (projectRootPath !== null) { - try { - const compilerInfo = path.resolve( - projectRootPath, - c.compilerInfoPartialPath, - ); - const contents = await fsAsync.readFile(compilerInfo, "utf8"); - const compileInfo = JSON.parse(contents); - if (compileInfo && compileInfo.bsc_path) { - const bsc_path = compileInfo.bsc_path; - if (binary === "bsc.exe") { - return normalizePath(bsc_path); - } else { - const binary_path = path.join(path.dirname(bsc_path), binary); - return normalizePath(binary_path); - } - } - } catch {} - } - + // Find rescript package directory const rescriptDir = lookup.findFilePathFromProjectRoot( projectRootPath, path.join("node_modules", "rescript"), @@ -259,8 +316,9 @@ let findBinary = async ( return null; } - let rescriptVersion = null; - let rescriptJSWrapperPath = null; + // Read version from package.json + let rescriptVersion: string | null = null; + let rescriptJSWrapperPath: string | null = null; try { const rescriptPackageJSONPath = path.join(rescriptDir, "package.json"); const rescriptPackageJSON = JSON.parse( @@ -272,42 +330,31 @@ let findBinary = async ( return null; } - let binaryPath: string | null = null; + // Handle "rescript" JS wrapper (same for all versions) if (binary == "rescript") { // Can't use the native bsb/rescript since we might need the watcher -w // flag, which is only in the JS wrapper - binaryPath = path.join(rescriptDir, rescriptJSWrapperPath); - } else if (semver.gte(rescriptVersion, "12.0.0-alpha.13")) { - // TODO: export `binPaths` from `rescript` package so that we don't need to - // copy the logic for figuring out `target`. - const target = `${process.platform}-${process.arch}`; - // Use realpathSync to resolve symlinks, which is necessary for package - // managers like Deno and pnpm that use symlinked node_modules structures. - const targetPackagePath = path.join( - fs.realpathSync(rescriptDir), - "..", - `@rescript/${target}/bin.js`, - ); - const { binPaths } = await import(targetPackagePath); - - if (binary == "bsc.exe") { - binaryPath = binPaths.bsc_exe; - } else if (binary == "rescript-editor-analysis.exe") { - binaryPath = binPaths.rescript_editor_analysis_exe; - } else if (binary == "rewatch.exe") { - binaryPath = binPaths.rewatch_exe; - } else if (binary == "rescript.exe") { - binaryPath = binPaths.rescript_exe; + if (rescriptJSWrapperPath == null) { + return null; } - } else { - binaryPath = path.join(rescriptDir, c.platformDir, binary); + const binaryPath = path.join(rescriptDir, rescriptJSWrapperPath); + return normalizePath(binaryPath); } - if (binaryPath != null && fs.existsSync(binaryPath)) { - return normalizePath(binaryPath); - } else { + // Top-level separation: v12+ or legacy + if (rescriptVersion == null) { return null; } + const isReScript12OrHigher = semver.gte(rescriptVersion, "12.0.0"); + if (isReScript12OrHigher) { + // For ReScript 12+, rewatch.exe doesn't exist + if (binary == "rewatch.exe") { + return null; + } + return findBinaryReScript12(rescriptDir, rescriptVersion, binary); + } else { + return findBinaryLegacy(projectRootPath, rescriptDir, binary); + } }; export let findRescriptBinary = (projectRootPath: NormalizedPath | null) => @@ -326,6 +373,9 @@ export let findRewatchBinary = (projectRootPath: NormalizedPath | null) => export let findRescriptExeBinary = (projectRootPath: NormalizedPath | null) => findBinary(projectRootPath, "rescript.exe"); +export let findRescriptToolsBinary = (projectRootPath: NormalizedPath | null) => + findBinary(projectRootPath, "rescript-tools.exe"); + type execResult = | { kind: "success"; @@ -422,32 +472,45 @@ export let runAnalysisAfterSanityCheck = async ( ? projectsFiles.get(projectRootPath)?.rescriptVersion : null) ?? (await findReScriptVersionForProjectRoot(projectRootPath)); - let binaryPath = builtinBinaryPath; + const isReScript12OrHigher = + semver.valid(rescriptVersion) && + semver.gte(rescriptVersion as string, "12.0.0"); let project = projectRootPath ? projectsFiles.get(projectRootPath) : null; - /** - * All versions including 12.0.0-alpha.5 and above should use the analysis binary - * that now ships with the compiler. Previous versions use the legacy one we ship - * with the extension itself. - */ - let shouldUseBuiltinAnalysis = - semver.valid(rescriptVersion) && - semver.lt(rescriptVersion as string, "12.0.0-alpha.5"); + // Top-level separation: v12+ or legacy + let binaryPath: string | null = null; + let runtime: string | undefined = undefined; - if (!shouldUseBuiltinAnalysis && project != null) { - binaryPath = project.editorAnalysisLocation; - } else if (!shouldUseBuiltinAnalysis && project == null) { - // TODO: Warn user about broken state? - return null; - } else { - binaryPath = builtinBinaryPath; - } + if (isReScript12OrHigher) { + // ReScript 12+: use binary from compiler, must have valid path + if (project != null && project.editorAnalysisLocation != null) { + binaryPath = project.editorAnalysisLocation; + } else if (projectRootPath != null) { + // Project not in cache yet, try to find binary directly + binaryPath = await findBinary( + projectRootPath, + "rescript-editor-analysis.exe", + ); + } + + if (binaryPath == null) { + const errorMessage = + `ReScript ${rescriptVersion} editor analysis binary not found. ` + + `Please ensure ReScript is properly installed in your project at ${projectRootPath ?? "unknown"}. ` + + `The extension expects to find the binary via @rescript/${process.platform}-${process.arch}/bin.js.`; + reportError("editor-analysis-binary-not-found", errorMessage); + throw new Error(errorMessage); + } - let runtime: string | undefined = undefined; - if (semver.gt(rescriptVersion as string, "12.0.0-rc.1")) { const runtimePath = await getRuntimePathFromProjectRoot(projectRootPath); runtime = runtimePath ?? undefined; + } else { + // ReScript < 12: always use builtin binary (preserves original behavior) + binaryPath = builtinBinaryPath; + if (binaryPath == null) { + return null; + } } let options: childProcess.ExecFileSyncOptions = { @@ -468,10 +531,6 @@ export let runAnalysisAfterSanityCheck = async ( }, }; - if (binaryPath == null) { - return null; - } - let stdout = ""; try { stdout = childProcess.execFileSync(binaryPath, args, options).toString();